MVVM 架构图

谈到 MVVM 架构,不得不祭出官方的架构图,架构图能帮助我们更好地理解,如下所示:
MVVM 架构解析及 Jetpack 架构组件的使用-LMLPHP
在实践中,根据对架构组件 paging 的使用和理解,我将架构图扩展成下面这样:

MVVM 架构解析及 Jetpack 架构组件的使用-LMLPHP
有背景颜色的3处是 paging 组件需要多用到的。

MVVM 和 MVP 的区别

MVPV 层和 P 层互相持有对方的引用,在V 层调用 P 层逻辑后,P 层回调V 层的相应方法更新 UI

而在 MVVM 中,上层只依赖直接下层,不能跨层持有引用,那 View 层调用 ViewModel 处理数据后,又如何更新自己呢?

答案就在 ViewModel 中的 LiveData,这是一种可观察的数据类型,在 View 层中观察者 Observer 对需要的数据进行订阅,当数据发生变化后,观察者 Observer 的回调方法 onChanged() 中会收到新的数据,从而可以更新 UI

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 架构的理论与实践都已打通!

05-25 01:02