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;
……
……
……
@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 架构解析
整个架构解析如下:
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 案例实战
下面根据我的开源项目 WanAndroid-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<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() {
}
}
这个自定义的观察者,主要干了三件事:
- 在网络请求前,判断网络是否连接,没有连接就调用错误处理方法。
- 根据
errorCode
的值判断业务处理是否成功,失败就调用错误处理方法。 - 在错误处理方法中向用户展示错误。
加入 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>
}
这里的 sourceLivaData
将 BaseItemKeyedDataSource
作为值,而 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
架构的理论与实践都已打通!