Kotlin 和 JetPack 的項(xiàng)目實(shí)戰(zhàn)(一)

搭建基于 MVVM 的項(xiàng)目框架


前言

從谷歌在 2017 年的 Google IO 宣布 Kotlin 成為 Android 開(kāi)發(fā)的官方語(yǔ)言開(kāi)始,已經(jīng)過(guò)去將近 2 年了,Kotlin 越來(lái)越被開(kāi)發(fā)者所關(guān)注,在 Github 的開(kāi)源項(xiàng)目中使用這門(mén)語(yǔ)言的也呈上升趨勢(shì)。

雖然批評(píng)的聲音也不少,說(shuō) Kotlin 只不過(guò)是語(yǔ)法糖的,拿來(lái)跟 Java 8/9/10 對(duì)比表示不過(guò)如此的,但是針對(duì) Android 開(kāi)發(fā)而言,這門(mén)語(yǔ)言是有生產(chǎn)力的,具體我在項(xiàng)目中可能會(huì)插入一些個(gè)人感受。

1. 淺談 MVP 和 MVVM

  • MVP

公司大概 1 年半前開(kāi)始改為用 MVP 模式來(lái)開(kāi)發(fā)代碼,相比曾經(jīng)上千行的 Activity 代碼,實(shí)在進(jìn)步了不少,V (View) 和 P (Presenter) 之間通過(guò)接口來(lái)互相訪問(wèn)與操作,一定程度抽象了代碼邏輯,確實(shí)有利于維護(hù)
基本上代碼目錄類(lèi)似這個(gè)

MVP目錄

Model 層用了 Retrofit 和 RxJava 進(jìn)行網(wǎng)絡(luò)的或者本地的數(shù)據(jù)獲取,比較穩(wěn)定,就不進(jìn)行對(duì)比了,因?yàn)橐矝](méi)區(qū)別

其中的 MainContract 代碼可能是這樣子寫(xiě)的

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

    interface Presenter extends BasePresenter{
        void loadAd();
        Adverts getAppAdvert() ;
    }
}

暫且不管我們?cè)?BaseView 和 BasePresenter 里做了什么操作,大致上看方法就是一個(gè)獲取廣告數(shù)據(jù)然后把廣告 List 傳遞到 View 進(jìn)行 UI 操作的功能。

之后讓 MainActivity 去實(shí)現(xiàn) View 接口 而 MainPresenter 去實(shí)現(xiàn) Presenter 接口,在初始化時(shí),互相都持有了對(duì)方的接口實(shí)例。

隨著生命周期的變化,可能出現(xiàn) NPE,或者內(nèi)存泄露,這確實(shí)也是我們上一個(gè)項(xiàng)目上線測(cè)試后出現(xiàn)的最多 Bug,添加了不少判空條件,更加加深了我去嘗試其它設(shè)計(jì)模式的愿望。

  • MVVM

時(shí)隔一年,谷歌在 2018 年的 Google IO 中發(fā)布了 JetPack 支持包,主打眾多開(kāi)發(fā)庫(kù)隨意添加使用,互不干擾,還順便把 v7 和 v4 支持包全改了個(gè)包名叫 androidx , 如何遷移到 androidx 可以之后再談。

jetpack官方介紹

為了完成 MVVM 的設(shè)計(jì),挑選了其中的 LiveData 和 ViewModel 進(jìn)行使用。

LiveData 其實(shí)跟 RxJava 一樣屬于觀察者模式的第三方庫(kù),一定程度上來(lái)說(shuō)是重復(fù)的,奈何各有優(yōu)勢(shì),所以在數(shù)據(jù)處理中繼續(xù)使用 Retrofit 和 RxJava 這套搭配,而在 UI 操作上添加了 LiveData 用于通知 V 端進(jìn)行頁(yè)面的刷新。

  • LiveData 優(yōu)勢(shì)和劣勢(shì)
優(yōu)勢(shì):
1. 綁定生命周期,不會(huì)內(nèi)存泄露,放心把數(shù)據(jù)交給他保管
2. 默認(rèn)只在 Activity 和 Fragment 在 started 或 resumed 2 種狀態(tài)時(shí)通知 UI 更新數(shù)據(jù)
3. 當(dāng) UI 處于started 或 resumed 狀態(tài)外,但是還沒(méi)銷(xiāo)毀之前,一直會(huì)接收更新數(shù)據(jù),在 UI 處于可見(jiàn)狀態(tài)時(shí),只會(huì)通知最新的數(shù)據(jù)到 UI。
4. 屏幕旋轉(zhuǎn)重建后的 View 仍然能利用之前數(shù)據(jù)。
5. 以及其它。

劣勢(shì):
1. MutableLiveData 只能將完整的新數(shù)據(jù)作為值覆蓋舊數(shù)據(jù)才會(huì)通知觀察者,也就是說(shuō)利用 getValue() 方法對(duì)舊數(shù)據(jù)進(jìn)行微小修改也沒(méi)辦法觸發(fā)通知。

畢竟是實(shí)戰(zhàn)中發(fā)現(xiàn)的優(yōu)勢(shì)劣勢(shì),總結(jié)得不完全,其實(shí)也并不想長(zhǎng)長(zhǎng)寫(xiě)一大段干澀的字,請(qǐng)多包涵。

插播一個(gè) kt 語(yǔ)言很有意思的實(shí)例構(gòu)造方法,在 AbsFragment 主要是做了一個(gè)為頁(yè)面添加頂部操作欄的功能

image

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

  • 構(gòu)建 AbsFragment 基礎(chǔ)類(lèi)
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 實(shí)例中獲取自定義 titleBar 所需要顯示的數(shù)據(jù),以及默認(rèn)值
            }
        }
        initView()
    }
    /**默認(rèn)初始化頁(yè)面功能方法 */
    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)
    }
}

可能初入門(mén) kt 的朋友不太了解它的 lambda 怎么寫(xiě),舉個(gè)栗子

fun <T> lock(body: () -> T): T {
return body()
}

以上方法要求返回泛型 T ,直接返回從參數(shù)中得到的 body 函數(shù) "()" 空括號(hào)代表函數(shù)無(wú)參數(shù),"
-> T "代表函數(shù)將會(huì)返回 泛型 T
對(duì)使用函數(shù) lock 的人來(lái)說(shuō)

//大括號(hào)內(nèi)就是所填入的 body 函數(shù)
lock<String>(body = { "" })
//kt 約定,只有一個(gè) Lambda 表達(dá)式的方法應(yīng)該將大括號(hào)移到小括號(hào)外側(cè),于是變成以下
lock<String>() { "" }
// 其實(shí)空的小括號(hào)也可以省略,尖括號(hào)內(nèi)的泛型也由于 kt 語(yǔ)言的自動(dòng)推斷功能,會(huì)根據(jù)大括號(hào)內(nèi)的返回值自動(dòng)變化,故又可以省略
lock { "" }

回到 initTitleBar 這個(gè)方法,返回的是一個(gè) kt 的擴(kuò)展函數(shù)

/**
 * 創(chuàng)建類(lèi)型安全的構(gòu)建器的方法
 */
fun <T : Any> initAny(any: T, init: T.() -> Unit): T {
    any.init()
    return any
}

跟上面的例子很像,但"T.() -> Unit" 是?個(gè)帶接收者的函數(shù)類(lèi)型。這意味著我
們需要向函數(shù)傳遞?個(gè) T 類(lèi)型的實(shí)例,并且我們可以在函數(shù)內(nèi)部調(diào)?該實(shí)例的成員。

關(guān)于 TitleBar 方法很簡(jiǎn)單,DslMarker 注解暫時(shí)不談

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

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

@DslMarker
annotation class TitleBarMarker

//抽象,避免內(nèi)部實(shí)例被直接操作
@TitleBarMarker
interface TitleHelper {

    @TitleBarMarker
    fun title(text: String)
    ...
}

所以 AbsFragment 的子類(lèi)實(shí)現(xiàn)類(lèi)似這樣子,只調(diào)用想要不同于默認(rèn)值的部分方法

override fun getTitleBar() = initTitleBar {
    title("分類(lèi)")
}

插播結(jié)束

  • 構(gòu)建 BaseFragment 基礎(chǔ)類(lèi)

我希望在 BaseFragment 中實(shí)現(xiàn)一些基礎(chǔ)的監(jiān)聽(tīng)者模式,基本只用到 ViewModel 和 LiveData 2個(gè)庫(kù)來(lái)完成

那先從 ViewModel 說(shuō)起

abstract class BaseViewModel : ViewModel() {
    /** 顯示布局里的數(shù)據(jù)加載view */
    private val _showLoadingView = MutableLiveData<Boolean>()
    /** LiveData只有g(shù)et方法 */
    val showLoadingView: LiveData<Boolean>
        get() = _showLoadingView
    /** 調(diào)用set方法即可決定是否顯示布局里的數(shù)據(jù)加載view */
    fun setLoadingView(loading: Boolean) {
        if (_showLoadingView.value != loading) {
            _showLoadingView.value = loading
        }
    }
    /* 基本上就是在初始化頁(yè)面需要請(qǐng)求的數(shù)據(jù)的時(shí)候,調(diào)用此方法 */
    abstract fun sendRequest()
}

略微簡(jiǎn)化來(lái)下代碼,就變成如上到代碼,MutableLiveData 的公共方法有 setValue() 和 postValue() , 而他的父類(lèi) LiveData 的 setValue() 是個(gè) protected 方法 ,可以對(duì)外隱藏賦值操作,一定程度上讓數(shù)據(jù)操作完全局限在 ViewModel 中。

再來(lái)說(shuō)說(shuō) 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...
                }
            }
        })
    }
    
    /** 需要時(shí)子類(lèi)返回loading頁(yè)面 */
    protected open fun getLoadingView(): View? {
        return null
    }
}

interface BaseViewImp<T> {

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

    /** 當(dāng)需要給viewModel傳參時(shí),返回ViewModel的工廠*/
    fun getModelFactory(): ViewModelProvider.Factory? {
        return null
    }
}

幾個(gè) kotlin 語(yǔ)法我啰嗦幾句,var lateinit 只能說(shuō)是提示編譯器,這個(gè)變量不要因?yàn)闆](méi)有初始化就給我報(bào)錯(cuò),我會(huì)在使用前擇期初始化,但是到運(yùn)行時(shí)忘記初始化了,也只有乖乖接收 NPE 錯(cuò)誤的選擇了。

let方法是前值非空就執(zhí)?代碼的簡(jiǎn)寫(xiě)

getModel() 返回 BaseViewModel 的子類(lèi) Class,而因?yàn)?ViewModel 初始化的特殊性,他是由 Fragemnt 或者 Activity 創(chuàng)建并且保管的,傳參數(shù)需要通過(guò)實(shí)現(xiàn) ViewModelProvider.Factory 接口來(lái)完成,例如以下這個(gè)類(lèi):

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

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

參數(shù) novelId 就傳遞到了類(lèi) DownloadViewModel(val novelId : String) 中啦


以上是一個(gè)我在項(xiàng)目中構(gòu)思的簡(jiǎn)易 MVVM 框架,為了便于介紹,刪除了不少代碼,如果按照這些步驟有什么覺(jué)得不好的,歡迎交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容