【Android 架構之MVVM】MVVM架構的搭建

文章展示的項目地址:https://gitee.com/QingDian_Fan/MVVMDemo

前言:

自從官方mvpSample出來後,鬧得熱火朝天的mvp,小編也未能倖免加入MVP大坑中, MVP 架構在安卓界非常流行,幾乎已經成爲主流框架,它讓業務邏輯 和 UI操作相對獨立,使得代碼結構更清晰。但是Presenter層與View層交互頻繁,使的Presenter層特別的臃腫,尤其是在一個界面要進行多次的網絡交互時,而且一旦View層需要改變時,Presenter也需要跟着進行相應的改變.將項目架構切換到MVVM,整個項目將會變得清爽多了.

介紹:

對於MVVM,最經典的解釋不過是這張圖了:
在這裏插入圖片描述
Model-View-ViewModel:
View便是這裏的activity和fragment,主要負責UI界面的展示,不參與任何邏輯和數據的處理
Viewmodel 主要負責業務邏輯和數據處理,本身不持有 View 層引用,通過 LiveData 向 View 層發送數據(liveData對數據具有監聽作用)
Model:便是指這裏的Repository ,主要負責從本地數據庫或者遠程服務器來獲取數據,Repository統一了數據的入口,獲取到數據,將數據發送給ViewModel
我在這個框架中沒有使用到圖中Room數據庫,大家可以參考Room學習瞭解.

對比:

MVP的優點

  • 分離了視圖邏輯和業務邏輯,降低了耦合
  • Activity只處理生命週期的任務,代碼變得更加簡潔
  • 視圖邏輯和業務邏輯分別抽象到了View和Presenter的接口中去,提高代碼的可閱讀性
  • Presenter被抽象成接口,可以有多種具體的實現,所以方便進行單元測試
  • 把業務邏輯抽到Presenter中去,避免後臺線程引用着Activity導致Activity的資源無法被系統回收從而引起內存泄露和OOM

以上就是MVP的優點了,但是對於一個框架咱們不能只瞭解它的優點,還需要知道他的缺點,利於在項目開發中更好的去避免不必要的麻煩和調試一些BUG.下面就說下MVP的缺點

MVP缺點:

  • Presenter中除了邏輯以外,還有大量的View->Model,Model->View的邏輯操作,造成 Presenter臃腫,維護困難。
  • 對UI的渲染放在了Presenter中,所以UI和Presenter的交互會過於頻繁。
  • Presenter過多地渲染了UI,往往會使得它與特定的UI的交互頻繁。一旦UI變動,Presenter也需要變
  • 接口暴增,可以說代碼量成倍增長,交互都需要通過接口傳遞信息,讓人無法忍受.

看完MVP的優缺點之後,下面咱們去了解一下MVVM架構,

MVVM的優點:

  • 雙向綁定技術,當Model變化時,View-Model會自動更新,View也會自動變化。很好做到數據的一致性,不用擔心,在模塊的這一塊數據是這個值,在另一塊就是另一個值了。所以 MVVM模式有些時候又被稱作:model-view-binder模式。
  • View的功能進一步的強化,具有控制的部分功能,若想無限增強它的功能,甚至控制器的全部功幾乎都可以遷移到各個View上(不過這樣不可取,那樣View幹了不屬於它職責範圍的事情)。View可以像控制器一樣具有自己的View-Model.
  • 由於控制器的功能大都移動到View上處理,大大的對控制器進行了瘦身。不用再爲看到龐大的控制器邏輯而發愁了。
  • 可以對View或ViewController的數據處理部分抽象出來一個函數處理model。這樣它們專職頁面佈局和頁面跳轉,它們必然更一步的簡化。

MVVM的缺點:

  • 數據綁定增加Bug調試難度,
  • 複雜的頁面,model也會很大,雖然使用方便了也很容易保證了數據的一致性,當時長期持有,不利於釋放內存,
  • 數據雙向綁定不利於View重用

也可以說MVVM的缺點基本上就是Databinding的缺點,其次我感覺DataBinding污染了xml.考慮到這些問題在此架構中我並沒有使用DatBinding.

MVVM架構分層代碼:

首先這次的網絡結構我是採用 Kotlin +協程+retrofit 進行搭建的,相信好多朋友會好奇爲什麼不適用rxjava+retrofit呢?
我感覺rxjava+retrofit進行網絡請求有點大材小用了,並且使用協程+retrofit的一定程度上可以減少Apk體積.

1.首先我們看下View層
在這裏不管是Fragment還是Activity我都分爲兩層BaseActivityBaseVMActivity.
BaseVMActivity繼承BaseActivity,BaseActivity我在這裏主要是處理一些較爲基本的操:動態權限的申請以及狀態欄的處理.如果頁面不需要進行刀劍MVVM框架,可自行繼承BaseActivity,互不影響.

abstract class BaseViewModelActivity<VM : BaseViewModel> : BaseActivity() {

protected lateinit var viewModel: VM

override fun onCreate(savedInstanceState: Bundle?) {
    initVM()
    super.onCreate(savedInstanceState)
    startObserve()
}

private fun initVM() {
    providerVMClass()?.let {
        viewModel = ViewModelProviders.of(this).get(it)
        lifecycle.addObserver(viewModel)
    }
}

open fun providerVMClass(): Class<VM>? = null

private fun startObserve() {
    //處理一些通用異常,比如網絡超時等
    viewModel.run {
        getError().observe(this@BaseViewModelActivity, Observer {
            requestError(it)
        })
        getFinally().observe(this@BaseViewModelActivity, Observer {
            requestFinally(it)
        })
    }
}

open fun requestFinally(it: Int?) {

}

open fun requestError(it: Exception?) {
    //處理一些已知異常
    it?.run {
        when (it) {
            is TimeoutCancellationException -> showToast("請求超時")
            is BaseRepository.TokenInvalidException -> showToast("登陸超時")
            is UnknownHostException -> showToast("沒有網絡")
            is HttpException -> showToast("網絡錯誤")
            is JSONException ->  showToast("解析錯誤")
            is ConnectException -> showToast("連接失敗")
            is ServerException -> showToast(it.message.toString())
        }
    }
}

override fun onDestroy() {
    super.onDestroy()
    lifecycle.removeObserver(viewModel)
}
}

在providerVMClass方法中通過BaseViewModel子類泛型類型參數獲取Class,在通過 ViewModelProviders.of(this).get(it)實例化ViewModel

2.其次再看下BaseViewModel層

open class BaseViewModel : ViewModel(), LifecycleObserver {

    private val error by lazy { MutableLiveData<Exception>() }

    private val finally by lazy { MutableLiveData<Int>() }

    //運行在UI線程的協程
    fun launchUI(block: suspend CoroutineScope.() -> Unit) = viewModelScope.launch {
        try {
            withTimeout(5000){
                block()
            }
        } catch (e: Exception) {
            error.value = e
        } finally {
            finally.value = 200
        }
    }

    /**
     * 請求失敗,出現異常
     */
    fun getError(): LiveData<Exception> {
        return error
    }

    /**
     * 請求完成,在此處做一些關閉操作
     */
    fun getFinally(): LiveData<Int> {
        return finally
    }

    }

網絡請求必須在子線程中進行,這是Android開發常理,使用協程進行網絡請求在代碼上可以讓異步代碼看起來是同步執行,這很大得提高了代碼得可讀性,不過理解掛起的確需要時間。BaseViewModel中最終得事情就是要搭建關於協程對於Retrofit網絡請求代碼塊得try…catch。
正常開發一般不建議直接通過ViewModel獲取網絡數據,這裏我們將工作交給一個新的模塊Repository。Repository只負責數據處理,提供乾淨的api,方便切換數據來源。

3.然後再看看BaseRepository層
BaseRepository中內容相對簡單,主要是獲取ApiService和網絡請求訂閱容器,方便管理網絡請求

open class BaseRepository {
    suspend fun <T : Any> request(call: suspend () -> ResponseData<T>): ResponseData<T> {
        return withContext(Dispatchers.IO) { call.invoke() }.apply {
            //這兒可以對返回結果errorCode做一些特殊處理,比如上傳參數錯誤等,可以通過拋出異常的方式實現
            //例:當上傳參數錯誤時,後臺返回errorCode 爲 1001,下面代碼實現,再到baseActivity通過觀察error來處理
            when (errorCode) {
                1001 -> throw ParameterException()
                0 -> Log.e("請求狀態值:$errorCode", "請求成功" );
            }
        }
    }

    class ParameterException(msg: String = "Parameter error") : Exception(msg)
}
4.到這我們的各層的基類已經看得差不多了,就讓我們用一個登陸模塊來坐下實戰吧:

4-1.LoginActivity:

class LoginActivity : BaseViewModelActivity<LoginViewModel>(), View.OnClickListener {


    private lateinit var data: loginData

    override fun getLayoutId(): Int = R.layout.activity_login

    override fun providerVMClass(): Class<LoginViewModel>? = LoginViewModel::class.java

    override fun initView() {

    }

    override fun initData() {
        login.setOnClickListener(this)
        viewModel.getLogin().observe(this, Observer {
            if (it.errorCode == 0) {
                showToast("登錄成功")

            } else {
                showToast("賬號或密碼錯誤")
            }
        })
    }

    override fun onClick(v: View?) {
        when (v!!.id) {
            R.id.login -> {
                val name = username.text.toString()
                val pwd = password.text.toString()

                viewModel.loginDatas(name, pwd);

            }
        }
    }

}

LoginActivity中只有UI初始化,發請網絡請求意圖以及數據觀察更新UI
4-2.LoginViewModel:

class LoginViewModel : BaseViewModel() {
    
    private  var data:MutableLiveData<ResponseData<loginData>> = MutableLiveData()

    private val repository = ArticleRepository()

    fun getLogin(): LiveData<ResponseData<loginData>> {

        return data
    }

     fun loginDatas(name: String, pwd: String)= launchUI {
        val result = repository.loginDatas(name, pwd)
       data.postValue(result)
    }
}

LoginViewModel中持有數據觀察容器LiveData和真正發起網絡請求動作,在接收到服務端返回的數據通過
data.postValue(result)通知Observer數據的更改,此處需注意的是,setValue方法只能在主線程中調用,postValue可以在任何線程中調用,如果是在後臺子線程中更新LiveData的值,必須調用postValue。
4-3.LoginRepository :

class LoginRepository : BaseRepository() {

    suspend fun loginDatas(userName: String, passWord: String): ResponseData<LoginData> = request {
        RetrofitClient.reqApi.login(userName, passWord)
    }
}

最後我們的LoginRepository 中就提供數據,此處只提供了網絡層的數據,在實際應用中可拆分爲本地數據和網絡數據,可根據項目需求自行處理
到此咱們一個簡單業務代碼就完成了,效果圖奉上:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章