MVVM Chart
It comes to MVVM
architecture, have resorted to the official organization chart, architecture diagram can help us better understand, as follows:
In practice, according to the architectural components paging
of use and understand, I'll Chart extended to the following:
At the background color of 3 paging
components it needs more used.
MVVM and MVP of difference
MVP
In V
layers and P
layers holding each other's references in the V
layer calls P
the logic layer, P
layer callback V
corresponding method layer of updates UI
.
In MVVM
the upper depends only on the immediately lower, can not hold a reference to cross-layer, that View
layer calls ViewModel
the post-processing data, and how to update yourself?
The answer lies in ViewModel
the LiveData
, which is a type of observable data, the View
observer layers Observer
of data needed to subscribe, when the data changes, the observer Observer
callback method onChanged()
will receive new data, which can be updated UI
.
LiveData
Related code is as follows:
//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 framework to resolve
The whole structure interpreted as follows:
View
Layer callsViewModel
to get dataViewModel
CallsRepository
to get dataRepository
It is a data warehouse, based on actual business, and throughDao
access to local databases orRetrofit
access the server.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
Logic is well understood that, repoResult
in accordance with pageSize
the change, and other data according to repoResult
changes in the last View
layer of these data it is observed.
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
On the PagedList
configuration page the amount of data, parameters such as initial loading, and finally packed into a data structure Listing
to return, this structure is as follows:
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
)
And the data source from 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>
}
Here sourceLivaData
will BaseItemKeyedDataSource
as a value, and BaseItemKeyedDataSource
is the place to get the real data:
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>)
}
Subclass only need to rewrite the parent class onLoadInitial()
and onLoadAfter()
method can perform a refresh logic and a load more.
Here achieved retry] [[Loading and logic, is loaded, loading fails] of these three states, the three states using data structures mentioned previously RequestState
, but the data is not successfully loaded here RequestState
in , where RequestState
only said loading state. How that data to update it?
Let's look at a BaseItemKeyedDataSource
subclass of it:
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)
})
}
}
It can be seen before and WeChatRepository
the similar data is retrieved from the server, but the data is acquired by the callback.onResult()
method returns to the View
layer.
In View
layer side, the list [Retry] button is encapsulated BasePagingAdapter
in accordance with the observed networkState
display, with hidden dynamic setting button, the relevant code is as follows:
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)
}
}
According to these wrapper classes, subclasses implement them in business, you can easily use the paging
assembly it! ! !
Here, MVVM
the theory and practice of architecture have been opened!