MVVM 架構解析及 Jetpack 架構組件的使用

本文首發於 binaryshao的博客java

MVVM 架構圖

談到 MVVM 架構,不得不祭出官方的架構圖,架構圖能幫助咱們更好地理解,以下所示: react

在這裏插入圖片描述
在實踐中,根據對架構組件 paging 的使用和理解,我將架構圖擴展成下面這樣:

在這裏插入圖片描述
有背景顏色的3處是 paging 組件須要多用到的。

MVVM 和 MVP 的區別

MVPV 層和 P 層互相持有對方的引用,在V 層調用 P 層邏輯後,P 層回調V 層的相應方法更新 UIandroid

而在 MVVM 中,上層只依賴直接下層,不能跨層持有引用,那 View 層調用 ViewModel 處理數據後,又如何更新本身呢?git

答案就在 ViewModel 中的 LiveData,這是一種可觀察的數據類型,在 View 層中觀察者 Observer 對須要的數據進行訂閱,當數據發生變化後,觀察者 Observer 的回調方法 onChanged() 中會收到新的數據,從而能夠更新 UIgithub

LiveData 的相關代碼以下:數據庫

//package androidx.lifecycle.LiveData;

……
……
……

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

@SuppressWarnings("WeakerAccess") /* synthetic access */
void dispatchingValue(@Nullable ObserverWrapper initiator) {
    if (mDispatchingValue) {
        mDispatchInvalidated = true;
        return;
    }
    mDispatchingValue = true;
    do {
        mDispatchInvalidated = false;
        if (initiator != null) {
            considerNotify(initiator);
            initiator = null;
        } else {
            for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}

private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}
複製代碼

MVVM 架構解析

整個架構解析以下:服務器

  1. View 層調用 ViewModel 獲取數據
  2. ViewModel 調用 Repository 獲取數據
  3. Repository 是數據倉庫,根據實際業務,再經過 Dao 訪問本地數據庫或者 Retrofit 訪問服務器。
  4. ViewModel 中的 LiveData 類型數據獲得更新
  5. View 層的觀察者 Observer 的回調方法 onChanged() 中收到新的數據,更新 UI
  6. 若是須要使用 paging 組件,就多了上圖中的3處調用

Jetpack 架構組件

JetpackGoogle 爲咱們提供的架構組件,對於這些組件,我有如下理解和使用心得:網絡

paging
  • 適用於列表頁面,能夠配置每頁加載的數據量和預加載距離
  • 須要使用 PagedListAdapterPagedList
  • 加載下一頁的邏輯就在 PagedListAdapter 調用 getItem() 時,這裏會調用 PagedListloadAround() 方法
  • 相關參數要求:mEnablePlaceholderstruemPrefetchDistance 大於 0
DataBinding

適用於數據繁雜的頁面,能夠減小大量 java 代碼,在列表頁面沒必要使用。數據結構

Navigation
  • 適用於能觸發兩個明確頁面之間跳轉的操做
  • 不適用不能肯定從哪一個頁面來或去往哪一個頁面的操做
ViewModel
  • 管理 ActivityFragment 的數據
  • 建立於ActivityFragment 內,頁面被銷燬前,ViewModel 會一直存在
  • 若是因配置變化致使頁面銷燬,ViewModel 不會銷燬,它會被用於新的頁面實例
  • 通常在 ViewModel 中配合 LiveData 使用
  • 通常用 ViewModelProviders 獲取 ViewModelProvider,再用它的 get() 方法獲取 ViewModel
  • get() 方法中會調用 Factorycreate() 方法建立 ViewModel
  • 建立的 ViewModel 被存入 ViewModelStoreHashMap 中,以便下次直接獲取,不用再建立
  • ViewModelStore 是經過 ActivityFragment 獲取的
  • ComponentActivity 的構造函數中有這麼一段代碼
getLifecycle().addObserver(new GenericLifecycleObserver() {
      @Override
      public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
          if (event == Lifecycle.Event.ON_DESTROY) {
              if (!isChangingConfigurations()) {
                  getViewModelStore().clear();
              }
          }
      }
  });
複製代碼

可見當不是配置變化致使 Activity 銷燬時,會調用 ViewModelStoreclear() 方法:架構

public final void clear() {
      for (ViewModel vm : mMap.values()) {
          vm.clear();
      }
      mMap.clear();
  }
複製代碼

這裏會調用 ViewModelclear() 方法,其中又會調用 onCleared()方法,咱們能夠在這個方法中取消訂閱,以防內存泄漏。

MVVM 案例實戰

下面根據個人開源項目 WanAndroid-MVVM 進一步講解 MVVM 架構的運用,如下全部代碼均來自於該項目。

不一樣的 UI 狀態

首先對於數據加載,通常有【加載中、加載成功、加載失敗】這3種狀態, UI 上須要有對應的變化。

不一樣於 MVPP 層回調 V 層的相應方法更新 UI 的方式, MVVMView 層只能經過觀察數據的方式來更新 UI

因此須要一種數據結構來表示不一樣的數據加載狀態,並在 View 層對其進行觀察和響應,定義這種數據結構以下:

package com.sbingo.wanandroid_mvvm.base

/** * Author: Sbingo666 * Date: 2019/4/12 */

enum class Status {
    LOADING,
    SUCCESS,
    ERROR,
}

data class RequestState<out T>(val status: Status, val data: T?, val message: String? = null) {
    companion object {
        fun <T> loading(data: T? = null) = RequestState(Status.LOADING, data)

        fun <T> success(data: T? = null) = RequestState(Status.SUCCESS, data)

        fun <T> error(msg: String? = null, data: T? = null) = RequestState(Status.ERROR, data, msg)
    }

    fun isLoading(): Boolean = status == Status.LOADING
    fun isSuccess(): Boolean = status == Status.SUCCESS
    fun isError(): Boolean = status == Status.ERROR
}
複製代碼

能夠看到,RequestState 對應了3種數據加載狀態,接着看它的具體使用:

package com.sbingo.wanandroid_mvvm.repository

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.sbingo.wanandroid_mvvm.base.RequestState
import com.sbingo.wanandroid_mvvm.data.http.HttpManager
import com.sbingo.wanandroid_mvvm.model.Chapter
import com.sbingo.wanandroid_mvvm.utils.asyncSubscribe

/** * Author: Sbingo666 * Date: 2019/4/22 */
class WeChatRepository(private val httpManager: HttpManager) {

    fun getWXChapters(): LiveData<RequestState<List<Chapter>>> {
        val liveData = MutableLiveData<RequestState<List<Chapter>>>()
        //數據加載中
        liveData.value = RequestState.loading()	
        httpManager.wanApi.getWXChapters()
            .asyncSubscribe({
           		 //數據加載成功
                liveData.postValue(RequestState.success(it.data))
            }, {
            	//數據加載失敗
                liveData.postValue(RequestState.error(it.message))
            })
        return liveData
    }
}
複製代碼

這裏將 RequestState 做爲 LiveData 的泛型參數,這樣 View 層就能夠對這個 LiveData 進行觀察了。

爲了簡化代碼,統一處理重複邏輯,我將觀察代碼寫入了 base 中:

protected fun <T> handleData(liveData: LiveData<RequestState<T>>, action: (T) -> Unit) =
    liveData.observe(this, Observer { result ->
        if (result.isLoading()) {
            showLoading()
        } else if (result?.data != null && result.isSuccess()) {
            finishLoading()
            action(result.data)
        } else {
            finishLoading()
        }
    })

fun showLoading() {
}

fun finishLoading() {
}
複製代碼

根據本身的業務需求,方便地實現 showLoading()finishLoading() 的邏輯,數據處理就在每一個頁面傳入的 action 中。

到這裏,完整的數據加載顯示流程就走通了!!!

異步加載數據

本項目中使用了 RxJava2 來異步加載數據,調用的代碼很簡單。

若是對線程切換的原理感興趣,能夠看我以前的一篇文章:【源碼分析】RxJava 1.2.2 實現簡單事件流的原理

但每一個調用的地方都要異步切換也挺麻煩的,所以我對 Observable 作了一個擴展,以下:

package com.sbingo.wanandroid_mvvm.utils

import com.sbingo.wanandroid_mvvm.data.http.RxHttpObserver
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers

/** * Author: Sbingo666 * Date: 2019/4/23 */

fun <T> Observable<T>.async(): Observable<T> {
    return this.subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
}

fun <T> Observable<T>.asyncSubscribe(onNext: (T) -> Unit, onError: (Throwable) -> Unit) {
    this.async()
        .subscribe(object : RxHttpObserver<T>() {
            override fun onNext(it: T) {
                super.onNext(it)
                onNext(it)
            }

            override fun onError(e: Throwable) {
                super.onError(e)
                onError(e)
            }
        })
}
複製代碼

這兩個方法均可以使用,具體就看是否想用 RxHttpObserver 這個自定義的觀察者咯。

若是使用 asyncSubscribe() 方法,調用方只需傳入數據加載成功和失敗的邏輯,很是簡單,就像這樣:

httpManager.wanApi.getWXChapters()
    .asyncSubscribe({
        liveData.postValue(RequestState.success(it.data))
    }, {
        liveData.postValue(RequestState.error(it.message))
    })
複製代碼

統一處理接口數據

剛纔說到自定義的觀察者 RxHttpObserver ,這又是啥呢?

package com.sbingo.wanandroid_mvvm.data.http

import com.sbingo.wanandroid_mvvm.R
import com.sbingo.wanandroid_mvvm.WanApplication
import com.sbingo.wanandroid_mvvm.utils.ExecutorUtils
import com.sbingo.wanandroid_mvvm.utils.NetUtils
import com.sbingo.wanandroid_mvvm.utils.ToastUtils
import io.reactivex.Observer
import io.reactivex.disposables.Disposable

abstract class RxHttpObserver<T> : Observer<T> {

    override fun onSubscribe(d: Disposable) {
        if (!NetUtils.isConnected(WanApplication.instance)) {
            onError(RuntimeException(WanApplication.instance.getString(R.string.network_error)))
        }
    }

    override fun onError(e: Throwable) {
        e.message?.let {
            ExecutorUtils.main_thread(Runnable { ToastUtils.show(it) })
        }
    }

    override fun onNext(it: T) {
        //業務失敗
        val result = it as? HttpResponse<*>
        if (result?.errorCode != 0) {
            onError(
                RuntimeException(
                    if (result?.errorMsg.isNullOrBlank())
                        WanApplication.instance.getString(R.string.business_error)
                    else {
                        result?.errorMsg
                    }
                )
            )
        }
    }

    override fun onComplete() {
    }
}
複製代碼

這個自定義的觀察者,主要乾了三件事:

  1. 在網絡請求前,判斷網絡是否鏈接,沒有鏈接就調用錯誤處理方法。
  2. 根據 errorCode 的值判斷業務處理是否成功,失敗就調用錯誤處理方法。
  3. 在錯誤處理方法中向用戶展現錯誤。

加入 paging 組件

以前提到過,若是加入了 paging 組件,架構流程略微不一樣。

paging 組件主要用於列表頁面,根據列表頁面的特性,我對其進行了一些封裝,主要封裝邏輯以下:

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel


/** * Author: Sbingo666 * Date: 2019/4/12 */
open class BasePagingViewModel<T>(repository: BasePagingRepository<T>) : ViewModel() {

    private val pageSize = MutableLiveData<Int>()
    private val repoResult = Transformations.map(pageSize) {
        repository.getData(it)
    }
    val pagedList = Transformations.switchMap(repoResult) { it.pagedList }
    val networkState = Transformations.switchMap(repoResult) { it.networkState }
    val refreshState = Transformations.switchMap(repoResult) { it.refreshState }

    fun refresh() {
        repoResult.value?.refresh?.invoke()
    }

    fun setPageSize(newSize: Int = 10): Boolean {
        if (pageSize.value == newSize)
            return false
        pageSize.value = newSize
        return true
    }

    fun retry() {
        repoResult.value?.retry?.invoke()
    }
}
複製代碼

BasePagingViewModel 中的邏輯很好理解,repoResult 根據 pageSize 變化,其餘數據又根據repoResult 變化,最後在 View 層對這些數據進行觀察就能夠了。

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.Transformations
import androidx.paging.Config
import androidx.paging.toLiveData

/** * Author: Sbingo666 * Date: 2019/4/12 */
abstract class BasePagingRepository<T> {

    fun getData(pageSize: Int): Listing<T> {

        val sourceFactory = createDataBaseFactory()
        val pagedList = sourceFactory.toLiveData(
            config = Config(
                pageSize = pageSize,
                enablePlaceholders = false,
                initialLoadSizeHint = pageSize * 2
            )
        )
        val refreshState = Transformations.switchMap(sourceFactory.sourceLivaData) { it.refreshStatus }
        val networkStatus = Transformations.switchMap(sourceFactory.sourceLivaData) { it.networkStatus }

        return Listing(
            pagedList,
            networkStatus,
            refreshState,
            refresh = {
                sourceFactory.sourceLivaData.value?.invalidate()
            },
            retry = {
                sourceFactory.sourceLivaData.value?.retryFailed()
            }
        )
    }

    abstract fun createDataBaseFactory(): BaseDataSourceFactory<T>
}
複製代碼

BasePagingRepository 中對 PagedList 配置了每頁數據量大小,初始加載量等參數,最後包裝成數據結構 Listing 返回,這種結構以下:

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.sbingo.wanandroid_mvvm.base.RequestState


/** * Author: Sbingo666 * Date: 2019/4/12 */
data class Listing<T>(
    //數據
    val pagedList: LiveData<PagedList<T>>,
    //上拉加載更多狀態
    val networkState: LiveData<RequestState<String>>,
    //下拉刷新狀態
    val refreshState: LiveData<RequestState<String>>,
    //刷新邏輯
    val refresh: () -> Unit,
    //重試邏輯,刷新或加載更多
    val retry: () -> Unit
)

複製代碼

而數據源來自 BaseDataSourceFactory:

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.MutableLiveData
import androidx.paging.DataSource

/** * Author: Sbingo666 * Date: 2019/4/12 */
abstract class BaseDataSourceFactory<T> : DataSource.Factory<Int,T>() {

    val sourceLivaData = MutableLiveData<BaseItemKeyedDataSource<T>>()

    override fun create(): BaseItemKeyedDataSource<T> {
        val dataSource: BaseItemKeyedDataSource<T> = createDataSource()
        sourceLivaData.postValue(dataSource)
        return dataSource
    }

    abstract fun createDataSource(): BaseItemKeyedDataSource<T>

}
複製代碼

這裏的 sourceLivaDataBaseItemKeyedDataSource 做爲值,而 BaseItemKeyedDataSource纔是真正獲取數據的地方:

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.MutableLiveData
import androidx.paging.ItemKeyedDataSource
import com.sbingo.wanandroid_mvvm.base.RequestState
import com.sbingo.wanandroid_mvvm.utils.ExecutorUtils


/** * Author: Sbingo666 * Date: 2019/4/12 */
abstract class BaseItemKeyedDataSource<T> : ItemKeyedDataSource<Int, T>() {
    private var retry: (() -> Any)? = null
    private var retryExecutor = ExecutorUtils.NETWORK_IO

     val networkStatus by lazy {
        MutableLiveData<RequestState<String>>()
    }

    val refreshStatus by lazy {
        MutableLiveData<RequestState<String>>()
    }

    fun retryFailed() {
        val preRetry = retry
        retry = null
        preRetry.let {
            retryExecutor.execute {
                it?.invoke()
            }
        }
    }

	//初始加載(包括刷新)時,系統回調此方法
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<T>) {
        refreshStatus.postValue(RequestState.loading())
        onLoadInitial(params, callback)
    }

	//加載更多時,系統回調此方法
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<T>) {
        networkStatus.postValue(RequestState.loading())
        onLoadAfter(params, callback)
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<T>) {
    }

    fun refreshSuccess() {
        refreshStatus.postValue(RequestState.success())
        retry = null
    }

    fun networkSuccess() {
        retry = null
        networkStatus.postValue(RequestState.success())
    }

    fun networkFailed(msg: String?, params: LoadParams<Int>, callback: LoadCallback<T>) {
        networkStatus.postValue(RequestState.error(msg))
        retry = {
            loadAfter(params, callback)
        }
    }

    fun refreshFailed(msg: String?, params: LoadInitialParams<Int>, callback: LoadInitialCallback<T>) {
        refreshStatus.postValue(RequestState.error(msg))
        retry = {
            loadInitial(params, callback)
        }
    }


    override fun getKey(item: T) = setKey(item)

    abstract fun setKey(item: T): Int

    abstract fun onLoadAfter(params: LoadParams<Int>, callback: LoadCallback<T>)

    abstract fun onLoadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<T>)
}
複製代碼

子類只須要複寫父類的 onLoadInitial()onLoadAfter() 方法就能執行刷新和加載更多的邏輯了。

這裏實現了【重試】的邏輯和【加載中、加載成功、加載失敗】這3種狀態,這3種狀態使用了以前提到的數據結構 RequestState,不過加載成功後數據並不會在這裏的 RequestState 中,這裏的 RequestState 只表示加載狀態。那數據怎麼更新呢?

咱們來看一個 BaseItemKeyedDataSource 的子類吧:

package com.sbingo.wanandroid_mvvm.paging.source

import com.sbingo.wanandroid_mvvm.base.paging.BaseItemKeyedDataSource
import com.sbingo.wanandroid_mvvm.data.http.HttpManager
import com.sbingo.wanandroid_mvvm.model.Article
import com.sbingo.wanandroid_mvvm.utils.asyncSubscribe

/** * Author: Sbingo666 * Date: 2019/4/23 */
class WXDataSource(private val httpManager: HttpManager, private val wxId: Int) : BaseItemKeyedDataSource<Article>() {

    var pageNo = 1

    override fun setKey(item: Article) = item.id

    override fun onLoadAfter(params: LoadParams<Int>, callback: LoadCallback<Article>) {
        httpManager.wanApi.getWXArticles(wxId, pageNo)
            .asyncSubscribe({
                pageNo += 1
                networkSuccess()
                callback.onResult(it.data?.datas!!)
            }, {
                networkFailed(it.message, params, callback)
            })
    }

    override fun onLoadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Article>) {
        httpManager.wanApi.getWXArticles(wxId, pageNo)
            .asyncSubscribe({
                pageNo += 1
                refreshSuccess()
                callback.onResult(it.data?.datas!!)
            }, {
                refreshFailed(it.message, params, callback)
            })
    }
}
複製代碼

能夠看到和以前 WeChatRepository 中相似,數據也是從服務器上獲取的,只不過獲取的數據是經過 callback.onResult() 方法返回給 View 層的。

View 層這邊,列表的【重試】按鈕是封裝在 BasePagingAdapter 中的, 根據觀察到的 networkState,動態設置按鈕的顯示與隱藏,相關代碼以下:

private fun hasFooter() =
    if (requestState == null)
        false
    else {
        !requestState?.isSuccess()!!
    }

override fun getItemViewType(position: Int): Int {
    return if (hasFooter() && position == itemCount - 1) {
        TYPE_FOOTER
    } else {
        TYPE_ITEM
    }
}

override fun getItemCount(): Int {
    return super.getItemCount() + if (hasFooter()) 1 else 0
}
    
fun setRequestState(newRequestState: RequestState<Any>) {
    val previousState = this.requestState
    val hadExtraRow = hasFooter()
    this.requestState = newRequestState
    val hasExtraRow = hasFooter()
    if (hadExtraRow != hasExtraRow) {
        if (hadExtraRow) {
            notifyItemRemoved(super.getItemCount())
        } else {
            notifyItemInserted(super.getItemCount())
        }
    } else if (hasExtraRow && previousState != newRequestState) {
        notifyItemChanged(itemCount - 1)
    }
}
複製代碼

根據這些封裝類,在業務中實現它們的子類,就能輕鬆使用 paging 組件啦!!!

到這裏,MVVM 架構的理論與實踐都已打通!

相關文章
相關標籤/搜索