結(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。
前言
隨著
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ù)。比如攔截器和多域名,全局Activity和Fragment屬性。
完整代碼:
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ù)。比如攔截器和多域名,全局Activity和Fragment屬性。
完整代碼:
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封裝
-
AbstractActivity是Activity的抽象基類,這個(gè)類里面的方法適用于全部Activity的需求。
該類中封裝了所有Activity必須實(shí)現(xiàn)的抽象方法。 -
BaseActivity封裝了基礎(chǔ)的Activity功能,主要用來(lái)初始化Activity公共功能:DataBinding的初始化、沉浸式狀態(tài)欄、AbstractActivity抽象方法的調(diào)用、屏幕適配、空白區(qū)域隱藏軟鍵盤。具體功能可以自行新增。 -
BaseDialogActivity只負(fù)責(zé)顯示Dialog Loading彈窗,一般在提交請(qǐng)求或本地流處理時(shí)使用。也可以擴(kuò)展其他的Dialog,比如時(shí)間選擇器之類。 -
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ù)。
-
-
BaseVMActivity實(shí)現(xiàn)ViewMode的Activity基類,通過(guò)泛型對(duì)ViewModel進(jìn)行實(shí)例化。并且通過(guò)BaseViewModel進(jìn)行公共操作。 -
BaseMVVMActivity所有Activity最終需要繼承的MVVM類,通過(guò)傳入DataBinding和ViewModel的泛型進(jìn)行初始化操作,在構(gòu)造參數(shù)中還需要獲取Layout布局 -
BaseListActivity適用于列表的Activity,分頁(yè)操作、上拉加載、下拉刷新、空布局、頭布局、底布局封裝。
Fragment封裝
根據(jù)你的需要進(jìn)行不同的封裝,我比較傾向于和Activity具有相同功能的封裝,也就是Activity封裝的功能我Fragment也要有。這樣在使用Navigation的時(shí)候可以減少Activity和Fragment的差異。這里直接參考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ì)的可以查看KunMinX的UnPeek-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() }
}
}
輔助類封裝
大部分的Activity和Fragment樣式基本相同,比如布局中的TitleLayout、LoadingLayout這些都是統(tǒng)一樣式。所以可以封裝全局的輔助類來(lái)對(duì)Activity中的屬性進(jìn)行抽離。
- 定義接口
ISettingBaseActivity添加抽離的方法,并且賦于默認(rèn)值。 - 定義接口
ISettingBaseFragment添加抽離的方法,并且賦于默認(rèn)值。 - 創(chuàng)建
ISettingBaseActivity和ISettingBaseFragment的實(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)目,特此鳴謝. 不分先后按首字母排序.
- AndroidUtilCode 安卓工具類庫(kù)
- AVLoadingIndicatorView 加載Loading動(dòng)畫
- BRVAH 萬(wàn)能Adapter
- DKVideoPlayer 安卓視頻播放器
- JetPackMvvm 基于MVVM模式集成JetPack玩Android項(xiàng)目
-
material-dialogs
一個(gè)漂亮、流暢、可擴(kuò)展的對(duì)話框API,適用于Kotlin和Android。 - RecyclerViewDivider 一個(gè)為RecyclerView配置分隔符的庫(kù)。
- SmartRefreshLayout Android智能下拉刷新框架
-
saf-logginginterceptor
Android項(xiàng)目中,OKHttp的日志的攔截器 - Square Square公司的 Retrofit + OkHttp + Moshi
- UnPeek-LiveData 解決LiveData數(shù)據(jù)倒灌
如何聯(lián)系我(How to contact me)
QQ: 1217056667
郵箱(Email): a912816369@gmail.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.