Kotlin 和 JetPack 的專案實戰(一)

Einsteinford發表於2019-02-27

搭建基於 MVVM 的專案框架


前言

從谷歌在 2017 年的 Google IO 宣佈 Kotlin 成為 Android 開發的官方語言開始,已經過去將近 2 年了,Kotlin 越來越被開發者所關注,在 Github 的開源專案中使用這門語言的也呈上升趨勢。

雖然批評的聲音也不少,說 Kotlin 只不過是語法糖的,拿來跟 Java 8/9/10 對比表示不過如此的,但是針對 Android 開發而言,這門語言是有生產力的,具體我在專案中可能會插入一些個人感受。

1. 淺談 MVP 和 MVVM

  • MVP

公司大概 1 年半前開始改為用 MVP 模式來開發程式碼,相比曾經上千行的 Activity 程式碼,實在進步了不少,V (View) 和 P (Presenter) 之間通過介面來互相訪問與操作,一定程度抽象了程式碼邏輯,確實有利於維護 基本上程式碼目錄類似這個

MVP目錄

Model 層用了 Retrofit 和 RxJava 進行網路的或者本地的資料獲取,比較穩定,就不進行對比了,因為也沒區別

其中的 MainContract 程式碼可能是這樣子寫的

public class MainContract {
    interface View extends BaseView{
        void showAd(Adverts adverts);
    }

    interface Presenter extends BasePresenter{
        void loadAd();
        Adverts getAppAdvert() ;
    }
}
複製程式碼

暫且不管我們在 BaseView 和 BasePresenter 裡做了什麼操作,大致上看方法就是一個獲取廣告資料然後把廣告 List 傳遞到 View 進行 UI 操作的功能。

之後讓 MainActivity 去實現 View 介面 而 MainPresenter 去實現 Presenter 介面,在初始化時,互相都持有了對方的介面例項。

隨著生命週期的變化,可能出現 NPE,或者記憶體洩露,這確實也是我們上一個專案上線測試後出現的最多 Bug,新增了不少判空條件,更加加深了我去嘗試其它設計模式的願望。

  • MVVM

時隔一年,谷歌在 2018 年的 Google IO 中釋出了 JetPack 支援包,主打眾多開發庫隨意新增使用,互不干擾,還順便把 v7 和 v4 支援包全改了個包名叫 androidx , 如何遷移到 androidx 可以之後再談。

jetpack官方介紹

為了完成 MVVM 的設計,挑選了其中的 LiveData 和 ViewModel 進行使用。

LiveData 其實跟 RxJava 一樣屬於觀察者模式的第三方庫,一定程度上來說是重複的,奈何各有優勢,所以在資料處理中繼續使用 Retrofit 和 RxJava 這套搭配,而在 UI 操作上新增了 LiveData 用於通知 V 端進行頁面的重新整理。

  • LiveData 優勢和劣勢
優勢:
1. 繫結生命週期,不會記憶體洩露,放心把資料交給他保管
2. 預設只在 Activity 和 Fragment 在 started 或 resumed 2 種狀態時通知 UI 更新資料
3. 當 UI 處於started 或 resumed 狀態外,但是還沒銷燬之前,一直會接收更新資料,在 UI 處於可見狀態時,只會通知最新的資料到 UI。
4. 螢幕旋轉重建後的 View 仍然能利用之前資料。
5. 以及其它。

劣勢:
1. MutableLiveData 只能將完整的新資料作為值覆蓋舊資料才會通知觀察者,也就是說利用 getValue() 方法對舊資料進行微小修改也沒辦法觸發通知。
複製程式碼

畢竟是實戰中發現的優勢劣勢,總結得不完全,其實也並不想長長寫一大段乾澀的字,請多包涵。

插播一個 kt 語言很有意思的例項構造方法,在 AbsFragment 主要是做了一個為頁面新增頂部操作欄的功能

Kotlin 和 JetPack 的專案實戰(一)

有興趣可以看這一部分,不然跳過以下一大段

  • 構建 AbsFragment 基礎類
abstract class AbsFragment : Fragment() {
    private var titleBar: TitleBar? = null
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        if (getContentViewLayoutID() != 0) {
            titleBar = getTitleBar()
            return if (titleBar != null) {
            //略,在新建的 RelativeLayout 頂部新增自定義 titleBar,再新增主佈局 layout
            } else {
                inflater.inflate(getContentViewLayoutID(), container, false)
            }
        }
        return super.onCreateView(inflater, container, savedInstanceState)
    }
    
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        if (titleBar != null) {
            titleBar!!.apply {
            //略,從 TitleBar 例項中獲取自定義 titleBar 所需要顯示的資料,以及預設值
            }
        }
        initView()
    }
    /**預設初始化頁面功能方法 */
    protected abstract fun initView()

    /** 返回佈局layout*/
    protected abstract fun getContentViewLayoutID(): Int

    /**
     * 是否新增title欄
     * 不新增 返回 null
     * 需要新增 返回 [initTitleBar] 方法
     */
    protected abstract fun getTitleBar(): TitleBar?
    
    /** 就是這一段比較有趣 */
    protected fun initTitleBar(init: TitleHelper.() -> Unit): TitleBar {
        return initAny(TitleBar(), init)
    }
}
複製程式碼

可能初入門 kt 的朋友不太瞭解它的 lambda 怎麼寫,舉個栗子

fun <T> lock(body: () -> T): T {
return body()
}
複製程式碼

以上方法要求返回泛型 T ,直接返回從引數中得到的 body 函式 "()" 空括號代表函式無引數," -> T "代表函式將會返回 泛型 T 對使用函式 lock 的人來說

//大括號內就是所填入的 body 函式
lock<String>(body = { "" })
//kt 約定,只有一個 Lambda 表示式的方法應該將大括號移到小括號外側,於是變成以下
lock<String>() { "" }
// 其實空的小括號也可以省略,尖括號內的泛型也由於 kt 語言的自動推斷功能,會根據大括號內的返回值自動變化,故又可以省略
lock { "" }
複製程式碼

回到 initTitleBar 這個方法,返回的是一個 kt 的擴充套件函式

/**
 * 建立型別安全的構建器的方法
 */
fun <T : Any> initAny(any: T, init: T.() -> Unit): T {
    any.init()
    return any
}
複製程式碼

跟上面的例子很像,但"T.() -> Unit" 是⼀個帶接收者的函式型別。這意味著我 們需要向函式傳遞⼀個 T 型別的例項,並且我們可以在函式內部調⽤該例項的成員。

關於 TitleBar 方法很簡單,DslMarker 註解暫時不談

class TitleBar : TitleHelper {
    var title: String = ""

    override fun title(text: String) {
        title = text
    }
    ...
}

@DslMarker
annotation class TitleBarMarker

//抽象,避免內部例項被直接操作
@TitleBarMarker
interface TitleHelper {

    @TitleBarMarker
    fun title(text: String)
    ...
}
複製程式碼

所以 AbsFragment 的子類實現類似這樣子,只呼叫想要不同於預設值的部分方法

override fun getTitleBar() = initTitleBar {
    title("分類")
}
複製程式碼

插播結束

  • 構建 BaseFragment 基礎類

我希望在 BaseFragment 中實現一些基礎的監聽者模式,基本只用到 ViewModel 和 LiveData 2個庫來完成

那先從 ViewModel 說起

abstract class BaseViewModel : ViewModel() {
    /** 顯示佈局裡的資料載入view */
    private val _showLoadingView = MutableLiveData<Boolean>()
    /** LiveData只有get方法 */
    val showLoadingView: LiveData<Boolean>
        get() = _showLoadingView
    /** 呼叫set方法即可決定是否顯示佈局裡的資料載入view */
    fun setLoadingView(loading: Boolean) {
        if (_showLoadingView.value != loading) {
            _showLoadingView.value = loading
        }
    }
    /* 基本上就是在初始化頁面需要請求的資料的時候,呼叫此方法 */
    abstract fun sendRequest()
}
複製程式碼

略微簡化了下程式碼,就變成如上到程式碼,MutableLiveData 的公共方法有 setValue() 和 postValue() , 而他的父類 LiveData 的 setValue() 是個 protected 方法 ,可以對外隱藏賦值操作,一定程度上讓資料操作完全侷限在 ViewModel 中。

再來說說 BaseFragment

abstract class BaseFragment<T : BaseViewModel> : AbsFragment(), BaseViewImp<T> {
    lateinit var viewModel: T
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        viewModel = if (getModelFactory() == null) {
            ViewModelProviders.of(this).get(getModel())
        } else {
            ViewModelProviders.of(this, getModelFactory()).get(getModel())
        }

        return super.onCreateView(inflater, container, savedInstanceState)
    }
    
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        observeLoadingView()
    }
    
    private fun observeLoadingView() {
        viewModel.showLoadingView.observe(viewLifecycleOwner, Observer { showLoad ->
                getLoadingView().let { loadingView ->
                    loadingView?.visibility = if (isLoading) View.VISIBLE else View.GONE
                    dosth...
                }
            }
        })
    }
    
    /** 需要時子類返回loading頁面 */
    protected open fun getLoadingView(): View? {
        return null
    }
}

interface BaseViewImp<T> {

    /** 返回ViewModel的類*/
    fun getModel(): Class<T>

    /** 當需要給viewModel傳參時,返回ViewModel的工廠*/
    fun getModelFactory(): ViewModelProvider.Factory? {
        return null
    }
}
複製程式碼

幾個 kotlin 語法我囉嗦幾句,var lateinit 只能說是提示編譯器,這個變數不要因為沒有初始化就給我報錯,我會在使用前擇期初始化,但是到執行時忘記初始化了,也只有乖乖接收 NPE 錯誤的選擇了。

let方法是前值非空就執⾏程式碼的簡寫

getModel() 返回 BaseViewModel 的子類 Class,而因為 ViewModel 初始化的特殊性,他是由 Fragemnt 或者 Activity 建立並且保管的,傳引數需要通過實現 ViewModelProvider.Factory 介面來完成,例如以下這個類:

class DownloadFactory(
        val novelId: String
) : ViewModelProvider.NewInstanceFactory() {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return DownloadViewModel(novelId) as T
    }
}
複製程式碼

引數 novelId 就傳遞到了類 DownloadViewModel(val novelId : String) 中啦


以上是一個我在專案中構思的簡易 MVVM 框架,為了便於介紹,刪除了不少程式碼,如果按照這些步驟有什麼覺得不好的,歡迎交流

相關文章