Android Jetpack之MVVM使用及封裝
Android開發架構
如果開發過程中大家各自為戰,沒有統一規範,久而久之,專案程式碼會變得混亂且後續難以維護。當使用統一的架構模式後,有很多的好處,如:
- 統一開發規範,使得程式碼整潔、規範,後續易於維護及擴充套件
- 提高開發效率(尤其在團隊人員較多時)
- 模組單一職責,使得模組專注自己內部(面向物件),模組間解耦
總之,開發架構是前人總結出來的一套行之有效的開發模式,目的是達到高內聚,低耦合的效果,使得專案程式碼更健壯、易維護。
Android中常見的架構模式有MVC(Model-View-Controller)
、MVP(Model-View-Presenter)
、MVVM(Model-View-ViewModel)
MVC
MVC(Model-View-Controller)
是比較早期的架構模式,模式整體也比較簡單。
MVC模式將程式分成了三個部分:
- Model模型層:業務相關的資料(如網路請求資料、本地資料庫資料等)及其對資料的處理
- View檢視層:頁面檢視(通過XML佈局編寫檢視層),負責接收使用者輸入、發起資料請求及展示結果頁面
- Controller控制器層:M與V之間的橋樑,負責業務邏輯
MVC特點:
- 簡單易用:上圖表述了資料整個流程:
View
接收使用者操作,通過Controller
去處理業務邏輯,並通過Model
去獲取/更新資料,然後Model層
又將最新的資料傳回View
- 架構簡單的另一面往往是對應的副作用:由於XML佈局能力弱,我們的View層的很多操作都是寫在Activity/Fragment中,同時,Controller、Model層的程式碼也大都寫在Activity/Fragment中,這就會導致一個問題,當業務邏輯比較複雜時,Activity/Fragment中的程式碼量會很大,其違背了類單一職責,不利於後續擴充套件及維護。尤其是後期你剛接手的專案,一個Activity/Fragment類中的程式碼動輒上千行程式碼,那感覺著實酸爽:
當然,如果業務很簡單,使用MVC模式還是一種不錯的選擇。
MVP
MVP(Model-View-Presenter)
MVP各模組職責如下:
- Model模型:業務相關的資料(如網路請求資料、本地資料庫資料等)及其對資料的處理
- View檢視:頁面檢視(Activity/Fragment),負責接收使用者輸入、發起資料請求及展示結果頁面
- Presenter:M與V之間的橋樑,負責業務邏輯
MVP特點: View
層接收使用者操作,並通過持有的Presenter
去處理業務邏輯,請求資料;接著Presenter
層通過Model
去獲取資料,然後Model
又將最新的資料傳回Presenter
層,Presenter
層又持有View
層的引用,進而將資料傳給View
層進行展示。
MVP相比MVC的幾處變化:
- View層與Model層不再互動,而是通過Presenter去進行聯絡
- 本質上MVP是面向介面程式設計,Model/View/Presenter每層的職責分工明確,當業務複雜時,整個流程邏輯也是很清晰的
當然,MVP
也不是十全十美的,MVP
本身也存在以下問題:
View
層會抽象成IView
介面,並在IView
中宣告一些列View
相關的方法;同樣的,Presenter
會被抽象成IPresenter
介面及其一些列方法,每當實現一個功能時,都需要編寫多個介面及其對應的方法,實現起來相對比較繁瑣,而且每次有改動時,對應的介面方法也基本都會再去改動。View
層與Presenter
層相互持有,當View
層關閉時,由於Presenter
層不是生命週期感知的,可能會導致記憶體洩漏甚至是崩潰。
ps:如果你的專案中使用了RxJava
,可以使用 AutoDispose 自動解綁。
MVVM
MVVM(Model-View-ViewModel)
,架構圖如下:
MVVM各職責如下:
- Model模型:業務相關的資料(如網路請求資料、本地資料庫資料等)及其對資料的處理
- View檢視:頁面檢視(Activity/Fragment),負責接收使用者輸入、發起資料請求及展示結果頁面
- ViewModel:M與V之間的橋樑,負責業務邏輯
MVVM特點:
- View層接收使用者操作,並通過持有的ViewModel去處理業務邏輯,請求資料;
- ViewModel層通過Model去獲取資料,然後Model又將最新的資料傳回ViewModel層,到這裡,ViewModel與Presenter所做的事基本是一樣的。但是ViewModel不會也不能持有View層的引用,而是View層會通過觀察者模式監聽ViewModel層的資料變化,當有新資料時,View層能自動收到新資料並重新整理介面。
UI驅動 vs 資料驅動
MVP
中,Presenter
中需要持有View
層的引用,當資料變化時,需要主動呼叫View
層對應的方法將資料傳過去並進行UI重新整理,這種可以認為是UI驅動;而MVVM
中,ViewModel
並不會持有View
層的引用,View
層會監聽資料變化,當ViewModel
中有資料更新時,View
層能直接拿到新資料並完成UI更新,這種可以認為是資料驅動,顯然,MVVM
相比於MVP
來說更加解耦。
MVVM的具體實現
上面介紹了MVC/MVP/MVVM
的各自特點,其中MVC/MVP
的具體使用方式,本文不再展開實現,接下來主要聊一下MVVM
的使用及封裝,MVVM
也是官方推薦的架構模式。
Jetpack MVVM
Jetpack
是官方推出的一系列元件庫,使用元件庫開發有很多好處,如:
- 遵循最佳做法:採用最新的設計方法構建,具有向後相容性,可以減少崩潰和記憶體洩漏
- 消除樣板程式碼:開發者可以更好地專注業務邏輯
- 減少不一致:可以在各種Android版本中執行,相容性更好。
為了實現上面的MVVM
架構模式,Jetpack
提供了多個元件來實現,具體來說有Lifecycle、LiveData、ViewModel(這裡的ViewModel是MVVM中ViewModel層的具體實現)
,其中Lifecycle
負責生命週期相關;LiveData
賦予類可觀察,同時還是生命週期感知的(內部使用了Lifecycle
);ViewModel
旨在以注重生命週期的方式儲存和管理介面相關的資料,針對這幾個庫的詳細介紹及使用方式不再展開,有興趣的可以參見前面的文章:
通過這幾個庫,就可以實現MVVM
了,官方也釋出了MVVM
的架構圖:
其中Activity/Fragment
為View
層,ViewModel+LiveData
為ViewModel
層,為了統一管理網路資料及本地資料資料,又引入了Repository
中間管理層,本質上是為了更好地管理資料,為了簡單把他們統稱為Model
層吧。
使用舉例
- View層程式碼:
//MvvmExampleActivity.kt
class MvvmExampleActivity : BaseActivity() {
private val mTvContent: TextView by id(R.id.tv_content)
private val mBtnQuest: Button by id(R.id.btn_request)
private val mToolBar: Toolbar by id(R.id.toolbar)
override fun getLayoutId(): Int {
return R.layout.activity_wan_android
}
override fun initViews() {
initToolBar(mToolBar, "Jetpack MVVM", true)
}
override fun init() {
//獲取ViewModel例項,注意這裡不能直接new,因為ViewModel的生命週期比Activity長
mViewModel = ViewModelProvider(this).get(WanViewModel::class.java)
mBtnQuest.setOnClickListener {
//請求資料
mViewModel.getWanInfo()
}
//ViewModel中的LiveData註冊觀察者並監聽資料變化
mViewModel.mWanLiveData.observe(this) { list ->
val builder = StringBuilder()
for (index in list.indices) {
//每條資料進行折行顯示
if (index != list.size - 1) {
builder.append(list[index])
builder.append("\n\n")
} else {
builder.append(list[index])
}
}
mTvContent.text = builder.toString()
}
}
}
- ViewModel層程式碼:
//WanViewModel.kt
class WanViewModel : ViewModel() {
//LiveData
val mWanLiveData = MutableLiveData<List<WanModel>>()
//loading
val loadingLiveData = SingleLiveData<Boolean>()
//異常
val errorLiveData = SingleLiveData<String>()
//Repository中間層 管理所有資料來源 包括本地的及網路的
private val mWanRepo = WanRepository()
fun getWanInfo(wanId: String = "") {
//展示Loading
loadingLiveData.postValue(true)
viewModelScope.launch(Dispatchers.IO) {
try {
val result = mWanRepo.requestWanData(wanId)
when (result.state) {
State.Success -> mWanLiveData.postValue(result.data)
State.Error -> errorLiveData.postValue(result.msg)
}
} catch (e: Exception) {
error(e.message ?: "")
} finally {
loadingLiveData.postValue(false)
}
}
}
}
- Repository層(Model層)程式碼:
class WanRepository {
//請求網路資料
suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> {
val service = RetrofitUtil.getService(DrinkService::class.java)
val baseData = service.getBanner()
if (baseData.code == 0) {
//正確
baseData.state = State.Success
} else {
//錯誤
baseData.state = State.Error
}
return baseData
}
}
這裡只通過Retrofit
請求了網路資料 玩Android 開放API,如果需要新增本地資料,只需要在方法裡新增本地資料處理即可,即 Repository是資料的管理中間層,對資料進行統一管理,ViewModel層中不需要關心資料的來源,大家各司其職即可,符合單一職責,程式碼可讀性更好,同時也更加解耦。在View
層點選按鈕請求資料,執行結果如下:
以上就完成了一次網路請求,相比於MVP
,MVVM
既不用宣告多個介面及方法,同時ViewModel
也不會像Presenter
那樣去持有View
層的引用,而是生命週期感知的,MVVM
方式更加解耦。
封裝
上一節介紹了Jetpack MVVM的使用例子,可以看到有一些程式碼邏輯是可以抽離出來封裝到公共部分的,那麼本節就嘗試對其做一次封裝。
首先,請求資料時可能會展示Loading
,請求完後可能是空資料、錯誤資料,對應下面的IStatusView
介面宣告:
interface IStatusView {
fun showEmptyView() //空檢視
fun showErrorView(errMsg: String) //錯誤檢視
fun showLoadingView(isShow: Boolean) //展示Loading檢視
}
因為ViewModel是在Activity中初始化的,所以可以封裝成一個Base類:
abstract class BaseMvvmActivity<VM : BaseViewModel> : BaseActivity(), IStatusView {
protected lateinit var mViewModel: VM
protected lateinit var mView: View
private lateinit var mLoadingDialog: LoadingDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mLoadingDialog = LoadingDialog(this, false)
mViewModel = getViewModel()!!
init()
registerEvent()
}
/**
* 獲取ViewModel 子類可以複寫,自行初始化
*/
protected open fun getViewModel(): VM? {
//當前物件超類的Type
val type = javaClass.genericSuperclass
//ParameterizedType表示引數化的型別
if (type != null && type is ParameterizedType) {
//返回此型別實際型別引數的Type物件陣列
val actualTypeArguments = type.actualTypeArguments
val tClass = actualTypeArguments[0]
return ViewModelProvider(this).get(tClass as Class<VM>)
}
return null
}
override fun showLoadingView(isShow: Boolean) {
if (isShow) {
mLoadingDialog.showDialog(this, false)
} else {
mLoadingDialog.dismissDialog()
}
}
override fun showEmptyView() {
......
}
//錯誤檢視 並且可以重試
override fun showErrorView(errMsg: String) {
.......
}
private fun registerEvent() {
//接收錯誤資訊
mViewModel.errorLiveData.observe(this) { errMsg ->
showErrorView(errMsg)
}
//接收Loading資訊
mViewModel.loadingLiveData.observe(this, { isShow ->
showLoadingView(isShow)
})
}
abstract fun init()
}
Base
類中初始化ViewModel
,還可以通過官方activity-ktx
、fragment-ktx
擴充套件庫,初始化方式:val model: VM by viewModels()
。
子類中繼承如下:
class MvvmExampleActivity : BaseMvvmActivity<WanViewModel>() {
private val mTvContent: TextView by id(R.id.tv_content)
private val mBtnQuest: Button by id(R.id.btn_request)
private val mToolBar: Toolbar by id(R.id.toolbar)
override fun getLayoutId(): Int {
return R.layout.activity_wan_android
}
override fun initViews() {
initToolBar(mToolBar, "Jetpack MVVM", true)
}
override fun init() {
mBtnQuest.setOnClickListener {
//請求資料
mViewModel.getWanInfo()
}
/**
* 這裡使用了擴充套件函式,等同於mViewModel.mWanLiveData.observe(this) {}
*/
observe(mViewModel.mWanLiveData) { list ->
val builder = StringBuilder()
for (index in list.indices) {
//每條資料進行折行顯示
if (index != list.size - 1) {
builder.append(list[index])
builder.append("\n\n")
} else {
builder.append(list[index])
}
}
mTvContent.text = builder.toString()
}
}
}
我們把ViewModel的初始化放到了父類裡進行,程式碼看上去更簡單了。監聽資料變化mViewModel.mWanLiveData.observe(this) {}
方式改成observe(mViewModel.mWanLiveData) {}
方式,少傳了一個LifecycleOwner,其實這是一個擴充套件函式,如下:
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, observer: (t: T) -> Unit) {
liveData.observe(this, { observer(it) })
}
ps:我們初始化View控制元件時,如 private val mBtnQuest: Button by id(R.id.btn_request)
,依然使用了擴充套件函式,如下:
fun <T : View> Activity.id(id: Int) = lazy {
findViewById<T>(id)
}
不用像寫java程式碼中那樣時刻要想著判空,同時只會在使用時才會進行初始化,很實用!
說回來,接著是ViewModel層的封裝,BaseViewModel.kt
:
abstract class BaseViewModel : ViewModel() {
//loading
val loadingLiveData = SingleLiveData<Boolean>()
//異常
val errorLiveData = SingleLiveData<String>()
/**
* @param request 正常邏輯
* @param error 異常處理
* @param showLoading 請求網路時是否展示Loading
*/
fun launchRequest(
showLoading: Boolean = true,
error: suspend (String) -> Unit = { errMsg ->
//預設異常處理,子類可以進行覆寫
errorLiveData.postValue(errMsg)
}, request: suspend () -> Unit
) {
//是否展示Loading
if (showLoading) {
loadStart()
}
//使用viewModelScope.launch開啟協程
viewModelScope.launch(Dispatchers.IO) {
try {
request()
} catch (e: Exception) {
error(e.message ?: "")
} finally {
if (showLoading) {
loadFinish()
}
}
}
}
private fun loadStart() {
loadingLiveData.postValue(true)
}
private fun loadFinish() {
loadingLiveData.postValue(false)
}
}
擴充套件一下: 1、上面執行網路請求時,使用viewModelScope.launch來啟動協程,引入方式:
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
這樣就可以直接在ViewModel中啟動協程並且當ViewModel生命週期結束時協程也會自動關閉,避免使用GlobalScope.launch { }
或MainScope().launch { }
還需自行關閉協程, 當然,如果是在Activity/Fragment、liveData中使用協程,也可以按需引入:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
具體可以參見官方的 將 Kotlin 協程與生命週期感知型元件一起使用 這篇文章。
2、另外細心的讀者可能觀察到,上面我們的Loading、Error資訊監聽都是用的SingleLiveData,把這個類打程式碼貼一下:
/**
* 多個觀察者存在時,只有一個Observer能夠收到資料更新
* https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java
*/
class SingleLiveData<T> : MutableLiveData<T>() {
companion object {
private const val TAG = "SingleLiveEvent"
}
private val mPending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner) { t ->
//如果expect為true,那麼將值update為false,方法整體返回true,
//即當前Observer能夠收到更新,後面如果還有訂閱者,不能再收到更新通知了
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
override fun setValue(@Nullable value: T?) {
//AtomicBoolean中設定的值設定為true
mPending.set(true)
super.setValue(value)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
}
可以看到SingleLiveData還是繼承自MutableLiveData,區別是當多個觀察者存在時,只有一個Observer能夠收到資料更新,本質上是在observe()時通過CAS加了限制,註釋已經很詳細了,不再贅述。
子類中繼承如下:
class WanViewModel : BaseViewModel() {
//LiveData
val mWanLiveData = MutableLiveData<List<WanModel>>()
//Repository中間層 管理所有資料來源 包括本地的及網路的
private val mWanRepo = WanRepository()
fun getWanInfo(wanId: String = "") {
launchRequest {
val result = mWanRepo.requestWanData(wanId)
when (result.state) {
State.Success -> mWanLiveData.postValue(result.data)
State.Error -> errorLiveData.postValue(result.msg)
}
}
}
}
最後是對Model層的封裝,BaseRepository.kt
:
open class BaseRepository {
suspend fun <T : Any> executeRequest(
block: suspend () -> BaseData<T>
): BaseData<T> {
val baseData = block.invoke()
if (baseData.code == 0) {
//正確
baseData.state = State.Success
} else {
//錯誤
baseData.state = State.Error
}
return baseData
}
}
資料基類BaseData.kt
:
class BaseData<T> {
@SerializedName("errorCode")
var code = -1
@SerializedName("errorMsg")
var msg: String? = null
var data: T? = null
var state: State = State.Error
}
enum class State {
Success, Error
}
子類中繼承如下:
class WanRepository : BaseRepository() {
suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> {
val service = RetrofitUtil.getService(DrinkService::class.java)
return executeRequest {
service.getBanner()
}
}
}
到此,基本上就完成了。