本文首发于
MVVM 架构图
谈到 MVVM
架构,不得不祭出官方的架构图,架构图能帮助我们更好地理解,如下所示:
paging
的使用和理解,我将架构图扩展成下面这样: 有背景颜色的3处是 paging
组件需要多用到的。 MVVM 和 MVP 的区别
MVP
中 V
层和 P
层互相持有对方的引用,在V
层调用 P
层逻辑后,P
层回调V
层的相应方法更新 UI
。
而在 MVVM
中,上层只依赖直接下层,不能跨层持有引用,那 View
层调用 ViewModel
处理数据后,又如何更新自己呢?
答案就在 ViewModel
中的 LiveData
,这是一种可观察的数据类型,在 View
层中观察者 Observer
对需要的数据进行订阅,当数据发生变化后,观察者 Observer
的回调方法 onChanged()
中会收到新的数据,从而可以更新 UI
。
LiveData
的相关代码如下:
//package androidx.lifecycle.LiveData;………………@MainThreadprotected 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, 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 架构解析
整个架构解析如下:
View
层调用ViewModel
获取数据ViewModel
调用Repository
获取数据Repository
是数据仓库,根据实际业务,再通过Dao
访问本地数据库或者Retrofit
访问服务器。ViewModel
中的LiveData
类型数据得到更新View
层的观察者Observer
的回调方法onChanged()
中收到新的数据,更新UI
。- 如果需要使用
paging
组件,就多了上图中的3处调用
Jetpack 架构组件
Jetpack
是 Google
为我们提供的架构组件,对于这些组件,我有以下理解和使用心得:
paging
- 适用于列表页面,可以配置每页加载的数据量和预加载距离
- 需要使用
PagedListAdapter
和PagedList
- 加载下一页的逻辑就在
PagedListAdapter
调用getItem()
时,这里会调用PagedList
的loadAround()
方法 - 相关参数要求:
mEnablePlaceholders
为true
或mPrefetchDistance
大于 0
DataBinding
适用于数据繁杂的页面,可以减少大量 java
代码,在列表页面不必使用。
Navigation
- 适用于能触发两个明确页面之间跳转的操作
- 不适用不能确定从哪个页面来或去往哪个页面的操作
ViewModel
- 管理
Activity
或Fragment
的数据 - 创建于
Activity
或Fragment
内,页面被销毁前,ViewModel
会一直存在 - 如果因配置变化导致页面销毁,
ViewModel
不会销毁,它会被用于新的页面实例 - 一般在
ViewModel
中配合LiveData
使用 - 一般用
ViewModelProviders
获取ViewModelProvider
,再用它的get()
方法获取ViewModel
- 在
get()
方法中会调用Factory
的create()
方法创建ViewModel
- 创建的
ViewModel
被存入ViewModelStore
的HashMap
中,以便下次直接获取,不用再创建 ViewModelStore
是通过Activity
或Fragment
获取的- 在
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
销毁时,会调用 ViewModelStore
的 clear()
方法:
public final void clear() { for (ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }复制代码
这里会调用 ViewModel
的 clear()
方法,其中又会调用 onCleared()
方法,我们可以在这个方法中取消订阅,以防内存泄漏。
MVVM 案例实战
下面根据我的开源项目 进一步讲解 MVVM
架构的运用,以下所有代码均来自于该项目。
不同的 UI 状态
首先对于数据加载,一般有【加载中、加载成功、加载失败】这3种状态, UI
上需要有对应的变化。
不同于 MVP
中 P
层回调 V
层的相应方法更新 UI
的方式, MVVM
中 View
层只能通过观察数据的方式来更新 UI
。
所以需要一种数据结构来表示不同的数据加载状态,并在 View
层对其进行观察和响应,定义这种数据结构如下:
package com.sbingo.wanandroid_mvvm.base/** * Author: Sbingo666 * Date: 2019/4/12 */enum class Status { LOADING, SUCCESS, ERROR,}data class RequestState(val status: Status, val data: T?, val message: String? = null) { companion object { fun loading(data: T? = null) = RequestState(Status.LOADING, data) fun success(data: T? = null) = RequestState(Status.SUCCESS, data) fun 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.repositoryimport androidx.lifecycle.LiveDataimport androidx.lifecycle.MutableLiveDataimport com.sbingo.wanandroid_mvvm.base.RequestStateimport com.sbingo.wanandroid_mvvm.data.http.HttpManagerimport com.sbingo.wanandroid_mvvm.model.Chapterimport com.sbingo.wanandroid_mvvm.utils.asyncSubscribe/** * Author: Sbingo666 * Date: 2019/4/22 */class WeChatRepository(private val httpManager: HttpManager) { fun getWXChapters(): LiveData
>> { val liveData = MutableLiveData
>>() //数据加载中 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 funhandleData(liveData: LiveData >, 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
来异步加载数据,调用的代码很简单。
如果对线程切换的原理感兴趣,可以看我之前的一篇文章:
但每个调用的地方都要异步切换也挺麻烦的,因此我对 Observable
做了一个扩展,如下:
package com.sbingo.wanandroid_mvvm.utilsimport com.sbingo.wanandroid_mvvm.data.http.RxHttpObserverimport io.reactivex.Observableimport io.reactivex.android.schedulers.AndroidSchedulersimport io.reactivex.schedulers.Schedulers/** * Author: Sbingo666 * Date: 2019/4/23 */funObservable .async(): Observable { return this.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())}fun Observable .asyncSubscribe(onNext: (T) -> Unit, onError: (Throwable) -> Unit) { this.async() .subscribe(object : RxHttpObserver () { 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.httpimport com.sbingo.wanandroid_mvvm.Rimport com.sbingo.wanandroid_mvvm.WanApplicationimport com.sbingo.wanandroid_mvvm.utils.ExecutorUtilsimport com.sbingo.wanandroid_mvvm.utils.NetUtilsimport com.sbingo.wanandroid_mvvm.utils.ToastUtilsimport io.reactivex.Observerimport io.reactivex.disposables.Disposableabstract class RxHttpObserver: Observer { 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() { }}复制代码
这个自定义的观察者,主要干了三件事:
- 在网络请求前,判断网络是否连接,没有连接就调用错误处理方法。
- 根据
errorCode
的值判断业务处理是否成功,失败就调用错误处理方法。 - 在错误处理方法中向用户展示错误。
加入 paging 组件
之前提到过,如果加入了 paging
组件,架构流程略微不同。
paging
组件主要用于列表页面,根据列表页面的特性,我对其进行了一些封装,主要封装逻辑如下:
package com.sbingo.wanandroid_mvvm.base.pagingimport androidx.lifecycle.MutableLiveDataimport androidx.lifecycle.Transformationsimport androidx.lifecycle.ViewModel/** * Author: Sbingo666 * Date: 2019/4/12 */open class BasePagingViewModel(repository: BasePagingRepository ) : ViewModel() { private val pageSize = MutableLiveData () 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.pagingimport androidx.lifecycle.Transformationsimport androidx.paging.Configimport androidx.paging.toLiveData/** * Author: Sbingo666 * Date: 2019/4/12 */abstract class BasePagingRepository{ fun getData(pageSize: Int): Listing { 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 }复制代码
BasePagingRepository
中对 PagedList
配置了每页数据量大小,初始加载量等参数,最后包装成数据结构 Listing
返回,这种结构如下:
package com.sbingo.wanandroid_mvvm.base.pagingimport androidx.lifecycle.LiveDataimport androidx.paging.PagedListimport com.sbingo.wanandroid_mvvm.base.RequestState/** * Author: Sbingo666 * Date: 2019/4/12 */data class Listing( //数据 val pagedList: LiveData >, //上拉加载更多状态 val networkState: LiveData >, //下拉刷新状态 val refreshState: LiveData >, //刷新逻辑 val refresh: () -> Unit, //重试逻辑,刷新或加载更多 val retry: () -> Unit)复制代码
而数据源来自 BaseDataSourceFactory
:
package com.sbingo.wanandroid_mvvm.base.pagingimport androidx.lifecycle.MutableLiveDataimport androidx.paging.DataSource/** * Author: Sbingo666 * Date: 2019/4/12 */abstract class BaseDataSourceFactory: DataSource.Factory () { val sourceLivaData = MutableLiveData >() override fun create(): BaseItemKeyedDataSource { val dataSource: BaseItemKeyedDataSource = createDataSource() sourceLivaData.postValue(dataSource) return dataSource } abstract fun createDataSource(): BaseItemKeyedDataSource }复制代码
这里的 sourceLivaData
将 BaseItemKeyedDataSource
作为值,而 BaseItemKeyedDataSource
才是真正获取数据的地方:
package com.sbingo.wanandroid_mvvm.base.pagingimport androidx.lifecycle.MutableLiveDataimport androidx.paging.ItemKeyedDataSourceimport com.sbingo.wanandroid_mvvm.base.RequestStateimport com.sbingo.wanandroid_mvvm.utils.ExecutorUtils/** * Author: Sbingo666 * Date: 2019/4/12 */abstract class BaseItemKeyedDataSource: ItemKeyedDataSource () { private var retry: (() -> Any)? = null private var retryExecutor = ExecutorUtils.NETWORK_IO val networkStatus by lazy { MutableLiveData >() } val refreshStatus by lazy { MutableLiveData >() } fun retryFailed() { val preRetry = retry retry = null preRetry.let { retryExecutor.execute { it?.invoke() } } } //初始加载(包括刷新)时,系统回调此方法 override fun loadInitial(params: LoadInitialParams , callback: LoadInitialCallback ) { refreshStatus.postValue(RequestState.loading()) onLoadInitial(params, callback) } //加载更多时,系统回调此方法 override fun loadAfter(params: LoadParams , callback: LoadCallback ) { networkStatus.postValue(RequestState.loading()) onLoadAfter(params, callback) } override fun loadBefore(params: LoadParams , callback: LoadCallback ) { } fun refreshSuccess() { refreshStatus.postValue(RequestState.success()) retry = null } fun networkSuccess() { retry = null networkStatus.postValue(RequestState.success()) } fun networkFailed(msg: String?, params: LoadParams , callback: LoadCallback ) { networkStatus.postValue(RequestState.error(msg)) retry = { loadAfter(params, callback) } } fun refreshFailed(msg: String?, params: LoadInitialParams , callback: LoadInitialCallback ) { 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 , callback: LoadCallback ) abstract fun onLoadInitial(params: LoadInitialParams , callback: LoadInitialCallback )}复制代码
子类只需要复写父类的 onLoadInitial()
和 onLoadAfter()
方法就能执行刷新和加载更多的逻辑了。
这里实现了【重试】的逻辑和【加载中、加载成功、加载失败】这3种状态,这3种状态使用了之前提到的数据结构 RequestState
,不过加载成功后数据并不会在这里的 RequestState
中,这里的 RequestState
只表示加载状态。那数据怎么更新呢?
我们来看一个 BaseItemKeyedDataSource
的子类吧:
package com.sbingo.wanandroid_mvvm.paging.sourceimport com.sbingo.wanandroid_mvvm.base.paging.BaseItemKeyedDataSourceimport com.sbingo.wanandroid_mvvm.data.http.HttpManagerimport com.sbingo.wanandroid_mvvm.model.Articleimport com.sbingo.wanandroid_mvvm.utils.asyncSubscribe/** * Author: Sbingo666 * Date: 2019/4/23 */class WXDataSource(private val httpManager: HttpManager, private val wxId: Int) : BaseItemKeyedDataSource() { var pageNo = 1 override fun setKey(item: Article) = item.id override fun onLoadAfter(params: LoadParams , callback: LoadCallback ) { httpManager.wanApi.getWXArticles(wxId, pageNo) .asyncSubscribe({ pageNo += 1 networkSuccess() callback.onResult(it.data?.datas!!) }, { networkFailed(it.message, params, callback) }) } override fun onLoadInitial(params: LoadInitialParams , callback: LoadInitialCallback ) { 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) { 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
架构的理论与实践都已打通!