文章展示的項目地址: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我都分爲兩層BaseActivity和BaseVMActivity.
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 中就提供數據,此處只提供了網絡層的數據,在實際應用中可拆分爲本地數據和網絡數據,可根據項目需求自行處理
到此咱們一個簡單業務代碼就完成了,效果圖奉上: