從0搭建一個(gè)實(shí)用的MVVM框架

結(jié)合Jetpack,構(gòu)建快速開發(fā)的MVVM框架。

項(xiàng)目使用Jetpack:LiveData、ViewModel、Lifecycle、Navigation組件。

支持動(dòng)態(tài)加載多狀態(tài)布局:加載中、成功、失敗、標(biāo)題;

支持快速生成ListActivity、ListFragment;

支持使用插件快速生成適用于本框架的Activity、Fragment、ListActivity、ListFragment。

前言

隨著Google對(duì)Jetpack的完善,對(duì)于開發(fā)者來(lái)說(shuō),MVVM顯得越來(lái)越高效與方便。

對(duì)于使用MVVM的公司來(lái)說(shuō),都有一套自己的MVVM框架,但是我發(fā)現(xiàn)有些只是對(duì)框架進(jìn)行非常簡(jiǎn)單的封裝,導(dǎo)致在開發(fā)過(guò)程中會(huì)出現(xiàn)很多沒必要的冗余代碼。

這篇文章主要就是分享如何從0搭建一個(gè)高效的MVVM框架。

基于MVVM進(jìn)行快速開發(fā), 上手即用。(重構(gòu)已完成,正在編寫SampleApp)

對(duì)基礎(chǔ)框架進(jìn)行模塊分離, 分為 MVVM Library--MVVM Navigation Library--MVVM Network Library
可基于業(yè)務(wù)需求使用 MVVM Library 、MVVM Navigation Library、MVVM Network Library

已開發(fā)一鍵生成代碼模板, 創(chuàng)建適用于本框架的Activity和Fragment.
具體查看AlvinMVVMPlugin_4_3

如何集成

To get a Git project into your build:

Step 1. Add the JitPack repository to your build file

Add it in your root build.gradle at the end of repositories:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Step 2. Add the dependency

dependencies {
    // BaseMVVM 同時(shí)集成了 MVVM、MVVM Network、MVVM Navigation
    implementation 'com.github.Chen-Xi-g.MVVMFramework:base_mvvm:Tag'
    // MVVM 基類
    implementation 'com.github.Chen-Xi-g.MVVMFramework:mvvm_framework:Tag'
    // MVVM Network 只負(fù)責(zé)網(wǎng)絡(luò)處理
    implementation 'com.github.Chen-Xi-g.MVVMFramework:mvvm_network:Tag'
    // MVVM Navigation 組件抽離
    implementation 'com.github.Chen-Xi-g.MVVMFramework:mvvm_navigation:Tag'
}

依賴引入后,需要初始化依賴,下面是模塊化初始化流程。

合并集成

合并集成只需要引入BaseMVVM即可,內(nèi)部 默認(rèn)集成了MVVM、MVVMNetwork、MVVMNavigation,只需要實(shí)現(xiàn)com.alvin.base_mvvm.base.IBaseMVVM接口就可以直接使用。

1.實(shí)現(xiàn)IBaseMVVM接口

建議在你的Application類,實(shí)現(xiàn)IBaseMVVM,并且需要在合適的位置進(jìn)行配置和初始化相關(guān)參數(shù),可以在這里配置網(wǎng)絡(luò)請(qǐng)求框架的參數(shù)和UI全局參數(shù)。比如攔截器多域名,全局ActivityFragment屬性。

完整代碼:

class SampleApplication2 : Application(), IBaseMVVM {
    override fun onCreate() {
        super.onCreate()
        // 使用默認(rèn)配置
        initBaseMVVM(this, "https://www.wanandroid.com/")
        // 自定義配置
        initBaseMVVM(
            this,
            "https://www.wanandroid.com/",
            BaseActivitySetting(),
            BaseFragmentSetting(),
            isDebug = BuildConfig.DEBUG,
            timeUnit = TimeUnit.SECONDS,
            timeout = 30,
            retryOnConnection = true,
            domain = {
                Constant.domainList.forEach { map ->
                    map.forEach {
                        if (it.key.isNotEmpty() && it.value.isNotEmpty()) {
                            put(it.key, it.value)
                        }
                    }
                }
            },
            // 是否打印
            hideVerticalLine = true,
            // 請(qǐng)求標(biāo)識(shí)
            requestTag = "Request 請(qǐng)求參數(shù)",
            // 響應(yīng)標(biāo)識(shí)
            responseTag = "Response 響應(yīng)結(jié)果",
            // 攔截器
            ResponseInterceptor(),
            ParameterInterceptor()
        )
    }
}

對(duì)于ViewModel的網(wǎng)絡(luò)請(qǐng)求已實(shí)現(xiàn)響應(yīng)的擴(kuò)展函數(shù),參考單獨(dú)集成2的使用。

單獨(dú)集成

1.實(shí)現(xiàn)IMVVM、INetWork接口

建議在你的Application類,實(shí)現(xiàn)IMVVM、INetWork,并且需要在合適的位置進(jìn)行配置和初始化相關(guān)參數(shù),可以在這里配置網(wǎng)絡(luò)請(qǐng)求框架的參數(shù)和UI全局參數(shù)。比如攔截器多域名,全局ActivityFragment屬性。

完整代碼:

class SampleApplication : Application(), IMVVMApplication, INetWorkApplication {

    override fun onCreate() {
        super.onCreate()
        // 初始化MVVM框架
        initMVVM(this, BaseActivitySetting(), BaseFragmentSetting(), BuildConfig.DEBUG)
        /* 兩種配置網(wǎng)絡(luò)請(qǐng)求,選擇其一即可 */
        // 初始化網(wǎng)絡(luò)請(qǐng)求,默認(rèn)配置
        initNetwork(baseUrl = "https://www.wanandroid.com")
        // 初始化網(wǎng)絡(luò)請(qǐng)求, 自定義配置
        initNetwork(
            // 基礎(chǔ)url
            "https://www.wanandroid.com",
            // 時(shí)間單位
            TimeUnit.SECONDS,
            // 時(shí)間
            30,
            // 是否重試
            true,
            // 多域名配置
            domain = {
                Constant.domainList.forEach { map ->
                    map.forEach {
                        if (it.key.isNotEmpty() && it.value.isNotEmpty()) {
                            put(it.key, it.value)
                        }
                    }
                }
            },
            // 是否隱藏網(wǎng)絡(luò)請(qǐng)求中的豎線
            true,
            // 請(qǐng)求標(biāo)識(shí)
            "Request 請(qǐng)求參數(shù)",
            // 響應(yīng)表示
            "Response 響應(yīng)結(jié)果",
            // 是否Debug
            BuildConfig.DEBUG,
            // 攔截器
            ResponseInterceptor(),
            ParameterInterceptor()
        )
    }
}

2.創(chuàng)建ViewModel擴(kuò)展函數(shù)

所有模塊需要依賴的base模塊創(chuàng)建ViewModel相關(guān)的擴(kuò)展函數(shù)VMKxt和Json實(shí)體類殼BaseEntity

/**
 * 過(guò)濾服務(wù)器結(jié)果,失敗拋異常
 * @param block 請(qǐng)求體方法,必須要用suspend關(guān)鍵字修飾
 * @param success 成功回調(diào)
 * @param error 失敗回調(diào) 可不傳
 * @param isLoading 是否顯示 Loading 布局
 * @param loadingMessage 加載框提示內(nèi)容
 */
fun <T> BaseViewModel.request(
    block: suspend () -> BaseResponse<T>,
    success: (T?) -> Unit,
    error: (ResponseThrowable) -> Unit = {},
    isLoading: Boolean = false,
    loadingMessage: String? = null
): Job {
    // 開始執(zhí)行請(qǐng)求
    httpCallback.beforeNetwork.postValue(
        // 執(zhí)行Loading邏輯
        LoadingEntity(
            isLoading,
            loadingMessage?.isNotEmpty() == true,
            loadingMessage ?: ""
        )
    )
    return viewModelScope.launch {
        kotlin.runCatching {
            //請(qǐng)求體
            block()
        }.onSuccess {
            // 網(wǎng)絡(luò)請(qǐng)求成功, 結(jié)束請(qǐng)求
            httpCallback.afterNetwork.postValue(false)
            //校驗(yàn)請(qǐng)求結(jié)果碼是否正確,不正確會(huì)拋出異常走下面的onFailure
            kotlin.runCatching {
                executeResponse(it) { coroutine ->
                    success(coroutine)
                }
            }.onFailure { error ->
                // 請(qǐng)求時(shí)發(fā)生異常, 執(zhí)行失敗回調(diào)
                val responseThrowable = ExceptionHandle.handleException(error)
                httpCallback.onFailed.value = responseThrowable.errorMsg ?: ""
                responseThrowable.errorLog?.let { errorLog ->
                    LogUtil.e(errorLog)
                }
                // 執(zhí)行失敗的回調(diào)方法
                error(responseThrowable)
            }
        }.onFailure { error ->
            // 請(qǐng)求時(shí)發(fā)生異常, 執(zhí)行失敗回調(diào)
            val responseThrowable = ExceptionHandle.handleException(error)
            httpCallback.onFailed.value = responseThrowable.errorMsg ?: ""
            responseThrowable.errorLog?.let { errorLog ->
                LogUtil.e(errorLog)
            }
            // 執(zhí)行失敗的回調(diào)方法
            error(responseThrowable)
        }
    }
}

/**
 * 不過(guò)濾服務(wù)器結(jié)果
 * @param block 請(qǐng)求體方法,必須要用suspend關(guān)鍵字修飾
 * @param success 成功回調(diào)
 * @param error 失敗回調(diào) 可不傳
 * @param isLoading 是否顯示 Loading 布局
 * @param loadingMessage 加載框提示內(nèi)容
 */
fun <T> BaseViewModel.requestNoCheck(
    block: suspend () -> T,
    success: (T) -> Unit,
    error: (ResponseThrowable) -> Unit = {},
    isLoading: Boolean = false,
    loadingMessage: String? = null
): Job {
    // 開始執(zhí)行請(qǐng)求
    httpCallback.beforeNetwork.postValue(
        // 執(zhí)行Loading邏輯
        LoadingEntity(
            isLoading,
            loadingMessage?.isNotEmpty() == true,
            loadingMessage ?: ""
        )
    )
    return viewModelScope.launch {
        runCatching {
            //請(qǐng)求體
            block()
        }.onSuccess {
            // 網(wǎng)絡(luò)請(qǐng)求成功, 結(jié)束請(qǐng)求
            httpCallback.afterNetwork.postValue(false)
            //成功回調(diào)
            success(it)
        }.onFailure { error ->
            // 請(qǐng)求時(shí)發(fā)生異常, 執(zhí)行失敗回調(diào)
            val responseThrowable = ExceptionHandle.handleException(error)
            httpCallback.onFailed.value = responseThrowable.errorMsg ?: ""
            responseThrowable.errorLog?.let { errorLog ->
                LogUtil.e(errorLog)
            }
            // 執(zhí)行失敗的回調(diào)方法
            error(responseThrowable)
        }
    }
}

/**
 * 請(qǐng)求結(jié)果過(guò)濾,判斷請(qǐng)求服務(wù)器請(qǐng)求結(jié)果是否成功,不成功則會(huì)拋出異常
 */
suspend fun <T> executeResponse(
    response: BaseResponse<T>,
    success: suspend CoroutineScope.(T?) -> Unit
) {
    coroutineScope {
        when {
            response.isSuccess() -> {
                success(response.getResponseData())
            }
            else -> {
                throw ResponseThrowable(
                    response.getResponseCode(),
                    response.getResponseMessage(),
                    response.getResponseMessage()
                )
            }
        }
    }
}

以上代碼封裝了快速的網(wǎng)絡(luò)請(qǐng)求擴(kuò)展函數(shù),并且可以根據(jù)自己的情況,選擇脫殼或者不脫殼的回調(diào)處理。 調(diào)用示例:

/**
 * 加載列表數(shù)據(jù)
 */
fun getArticleListData(page: Int, pageSize: Int) {
    request(
        {
            filterArticleList(page, pageSize)
        }, {
            // 成功操作
            it?.let {
                _articleListData.postValue(it.datas)
            }
        }
    )
}

完成上面的操作,你就可以進(jìn)入愉快的開發(fā)工作了。

引入一鍵生成代碼插件(可選)

每次創(chuàng)建Activity、Fragment、ListActivity、ListFragment都是重復(fù)的工作,為了可以更高效的開發(fā),減少這些枯燥的操作,特地編寫的快速生成MVVM代碼的插件,該插件只適用于當(dāng)前MVVM框架,具體使用請(qǐng)前往AlvinMVVMPlugin。集成后你就可以開始像創(chuàng)建EmptyActivity這樣創(chuàng)建MVVMActivity。

框架結(jié)構(gòu)

mvvm

該組件對(duì)Activity和Fragment進(jìn)行常用屬性封裝

  • base包下封裝了MVVM的基礎(chǔ)組件。
    • activity實(shí)現(xiàn)DataBinding + ViewModel的封裝,以及一些其他功能。
    • adapter實(shí)現(xiàn)DataBinding + Adapter的封裝。
    • fragment實(shí)現(xiàn)DataBinding + ViewModel的封裝,以及一些其他功能。
    • livedata實(shí)現(xiàn)LiveData的基礎(chǔ)功能封裝,如基本數(shù)據(jù)類型的非空返回值。
    • view_model實(shí)現(xiàn)BaseViewModel的處理。
  • help包下封裝了組件的輔助類,在BaseApplication中進(jìn)行全局Actiivty、Fragment屬性賦值。
  • manager包下封裝了對(duì)Activity的管理。
  • utils包下封裝了LogUtil工具類,通過(guò)BaseApplication進(jìn)行初始化。

Activity封裝

  1. AbstractActivityActivity的抽象基類,這個(gè)類里面的方法適用于全部Activity的需求。
    該類中封裝了所有Activity必須實(shí)現(xiàn)的抽象方法。
  2. BaseActivity封裝了基礎(chǔ)的Activity功能,主要用來(lái)初始化Activity公共功能:DataBinding的初始化、沉浸式狀態(tài)欄、AbstractActivity抽象方法的調(diào)用、屏幕適配、空白區(qū)域隱藏軟鍵盤。具體功能可以自行新增。
  3. BaseDialogActivity只負(fù)責(zé)顯示Dialog Loading彈窗,一般在提交請(qǐng)求或本地流處理時(shí)使用。也可以擴(kuò)展其他的Dialog,比如時(shí)間選擇器之類。
  4. BaseContentViewActivity是對(duì)布局進(jìn)行初始化操作的Activity,他是我們的核心。這里處理了每個(gè)Activity的每個(gè)狀態(tài)的布局,一般情況下有:
    • TitleLayout 公共標(biāo)題
    • ContentLayout 主要的內(nèi)容布局,使我們需要程序內(nèi)容的主要容器。
    • ErrorLayout 當(dāng)網(wǎng)絡(luò)請(qǐng)求發(fā)生錯(cuò)誤,需要對(duì)用戶進(jìn)行友好的提示。
    • LoadingLayout 正在加載數(shù)據(jù)的布局,給用戶一個(gè)良好的體驗(yàn),避免首次進(jìn)入頁(yè)面顯示的布局沒有數(shù)據(jù)。
  5. BaseVMActivity實(shí)現(xiàn)ViewModeActivity基類,通過(guò)泛型對(duì)ViewModel進(jìn)行實(shí)例化。并且通過(guò)BaseViewModel進(jìn)行公共操作。
  6. BaseMVVMActivity 所有Activity最終需要繼承的MVVM類,通過(guò)傳入DataBindingViewModel的泛型進(jìn)行初始化操作,在構(gòu)造參數(shù)中還需要獲取Layout布局
  7. BaseListActivity適用于列表的Activity,分頁(yè)操作、上拉加載、下拉刷新、空布局、頭布局、底布局封裝。

Fragment封裝

根據(jù)你的需要進(jìn)行不同的封裝,我比較傾向于和Activity具有相同功能的封裝,也就是Activity封裝的功能我Fragment也要有。這樣在使用Navigation的時(shí)候可以減少ActivityFragment的差異。這里直接參考Activity的封裝

Adapter封裝

每個(gè)項(xiàng)目中肯定會(huì)有列表的頁(yè)面,所以還需要對(duì)Adapter進(jìn)行DataBinding適配,這里使用的Adapter是BRVAH

abstract class BaseBindingListAdapter<T, DB : ViewDataBinding>(
    @LayoutRes private val layoutResId: Int
) : BaseQuickAdapter<T, BaseViewHolder>(layoutResId) {

    abstract fun convert(holder: BaseViewHolder, item: T, dataBinding: DB?)

    override fun convert(holder: BaseViewHolder, item: T) {
        convert(holder, item, DataBindingUtil.bind(holder.itemView))
    }
}

LiveData封裝

LiveData在使用的時(shí)候會(huì)出現(xiàn)數(shù)據(jù)倒灌的情況,用簡(jiǎn)單的話來(lái)描述數(shù)據(jù)倒灌:A訂閱1月1日新聞信息,B訂閱1月15日新聞信息,但是B在1月15日同時(shí)收到了1月1日的信息,這明顯不符合我們生活中的邏輯,所以需要對(duì)LiveData進(jìn)行封裝,詳細(xì)的可以查看KunMinXUnPeek-LiveData。

Navigation封裝

通過(guò)重寫 FragmentNavigator 將原來(lái)的 FragmentTransaction.replace() 方法替換為 hide()/Show()

ViewModel封裝

BaseViewModel中封裝一個(gè)網(wǎng)絡(luò)請(qǐng)求需要用的LiveData,下面是一個(gè)簡(jiǎn)單的示例

open class BaseViewModel : ViewModel() {

    // 默認(rèn)的網(wǎng)絡(luò)請(qǐng)求LiveData
    val httpCallback: HttpCallback by lazy { HttpCallback() }

    inner class HttpCallback {

        /**
         * 請(qǐng)求發(fā)生錯(cuò)誤
         *
         * String = 網(wǎng)絡(luò)請(qǐng)求異常
         */
        val onFailed by lazy { StringLiveData() }

        /**
         * 請(qǐng)求開始
         *
         * LoadingEntity 顯示loading的實(shí)體類
         */
        val beforeNetwork by lazy { EventLiveData<LoadingEntity>() }

        /**
         * 請(qǐng)求結(jié)束后框架自動(dòng)對(duì) loading 進(jìn)行處理
         *
         * false 關(guān)閉 loading or Dialog
         * true 不關(guān)閉 loading or Dialog
         */
        val afterNetwork by lazy { BooleanLiveData() }
    }
}

輔助類封裝

大部分的ActivityFragment樣式基本相同,比如布局中的TitleLayoutLoadingLayout這些都是統(tǒng)一樣式。所以可以封裝全局的輔助類來(lái)對(duì)Activity中的屬性進(jìn)行抽離。

  • 定義接口ISettingBaseActivity添加抽離的方法,并且賦于默認(rèn)值。
  • 定義接口ISettingBaseFragment添加抽離的方法,并且賦于默認(rèn)值。
  • 創(chuàng)建ISettingBaseActivityISettingBaseFragment的實(shí)現(xiàn)類,進(jìn)行默認(rèn)的自定義操作。
  • 創(chuàng)建GlobalMVVMBuilder進(jìn)行賦值

管理類封裝

通過(guò)Lifecycle結(jié)合AppManager對(duì)Activity的進(jìn)出棧管理。

mvvm_navigation

分離Navigation,通過(guò)重寫 FragmentNavigator 將原來(lái)的 FragmentTransaction.replace() 方法替換為 hide()/Show()。

mvvm_network

使用 Retrofit + OkHttp + Moshi 對(duì)網(wǎng)絡(luò)請(qǐng)求進(jìn)行封裝,使用密封類自定義異常處理。

鳴謝

該框架參考以下優(yōu)秀開源項(xiàng)目,特此鳴謝. 不分先后按首字母排序.

如何聯(lián)系我(How to contact me)

QQ: 1217056667

郵箱(Email): a912816369@gmail.com

小站: https://me.minlukj.com

License

   Copyright 2022 高國(guó)峰

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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