还是基于上面的MVVM结构来谈一下对该结构深入一点的理解
- 仓库层的理解
刚开始看MVVM的时候ViewModel层比较好理解一些,就是作为View和Mode的桥接,将视图和数据解耦,但是对仓库层的理解就没那么好了
第一次写仓库层的时候是直接在ViewModel层调用数据加载,比如下面这段代码:
class LiveInfoViewMode : ViewModel() {
private var liveData: MutableLiveData<Result<LiveInfo>>? = null
fun getLiveInfo(anchorId: String): MutableLiveData<Result<LiveInfo>> {
if (liveData == null) {
liveData = MutableLiveData()
}
liveData?.postValue(Result.loading())
val liveApi = LiveApi()
liveApi.getLiveData(anchorId, object : IHttpClient.ICallback {
override fun onSuccess(bytes: ByteArray) {
val liveInfo = …//解析数据
liveData?.postValue(Result.success(liveInfo))
}
override fun onError(e: IHttpClient.RequestException) {
e.printStackTrace()
liveData?.postValue(Result.error("${e.message}", -10001))
}
})
return liveData!!
}
}
有没有发现一个问题?
我们知道ViewModel层是主要处理业务相关逻辑的,因此这块要保证后面底层数据实现改变时不会影响到这边,不然业务的修改会很麻烦,而且容易出问题。
但是上面的写法却违背了这一条,试想一下,如果后面getLiveInfo的实现不再是从http获取数据,而是改成了从本地数据库获取呢,这时候需要修改的点有:
- liveApi获取数据的实现方式以及返回数据结构的重新构建
- 所有用到liveApi的ViewModel实现都需要更换
缺点就是我们只是要改个数据获取方式就需要动到所有的ViewModel,与我们的初衷解耦相背离
基于上面的例子,对仓库层的设计有了新的理解:
仓库层应该是对外提供统一的接口,具体的实现由具体注册的实现类来实现,这样即使你的数据获取方式一直在变,仓库层对外接口不变,那么每次只需要更改注册的实现类就行了,不会影响到业务层的逻辑
下面是新的设计:
class LiveRoomViewModelFactory(private val repo: ILiveRoomRepo = LiveRoomRepo()) :
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return LiveRoomViewMode(repo) as T
}
}
class LiveRoomViewMode(private val repo: ILiveRoomRepo) : ViewModel() {
fun getLiveInfo(anchorId: String, gameId: String): MutableLiveData<Result<List<LiveInfo>>> {
return repo.getRelatedLiveAnchor(anchorId, gameId, pageNum, 1).getLiveData()
}
}
interface ILiveRoomRepo {
fun getRelatedLiveAnchor(anchorId: String, gameId: String, pageNum: Int, direction: Int): Deferred<List<LiveInfo>>
}
viewModel = ViewModelProviders.of(this, LiveRoomViewModelFactory()).get(LiveRoomViewMode::class.java)
/**
* repo层返回的数据结构,之所以用Deferred是考虑到一个task含有多个async协程,然后要等
* 所有async结束后再调用complete来通知
*/
fun <T> asyncDataCall(type: Int = TYPE_NETWORK, dataCall: (() -> T?)): Deferred<T> {
val deferred = CompletableDeferred<T>()
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
// ...
}
}
val executor =
if (type == TYPE_NETWORK) AppExecutors.getInstance().networkIO() else AppExecutors.getInstance().diskIO()
executor.execute {
try {
// 请求成功
val res = dataCall.invoke()
if (res == null) {
deferred.completeExceptionally(Throwable("data is null"))
} else {
deferred.complete(res)
}
} catch (e: Throwable) {
// 请求失败
deferred.completeExceptionally(e)
}
}
return deferred
}
/**
* Deferred拓展,返回UI层使用的LiveData<Result<T>>
*/
fun <T> Deferred<T>.getLiveData(): MutableLiveData<Result<T>> {
val result = MutableLiveData<Result<T>>()
// 数据loading状态
result.value = Result.loading()
GlobalScope.launch {
try {
// 协程发起异步数据请求,挂起协程,等待deferred的complete调用
val response = await()
// 请求成功
withContext(Dispatchers.Main) {
// 数据success状态
result.value = Result.success(response)
}
} catch (e: Throwable) {
// 请求失败
withContext(Dispatchers.Main) {
// 请求数据过程出错,数据error状态
result.value = Result.error("${e.message}", -10001)
}
}
}
return result
}
新的仓库层设计是每个ViewModel有一个repo接口,ViewModel只认repo提供的接口,具体的实现由创建ViewModel时选择的工厂来决定
再回到之前的例子,假如这时候我需要修改接口的实现,我只要修改LiveRoomRepo里面的实现就行了,返回的数据格式不变,数据结构解析都在LiveRoomRepo里面,ViewModel层都不需要更改,是不是就做到解耦了,而且测试起来也比较方便,只需要测试接口实现就行了,不需要再测试ViewModel层的逻辑
- ViewModel层的优化
MVVM里面使用ViewModel的好处是做到数据响应式编程,将UI和数据解耦,同时ViewModel还和Activity的生命周期绑定到一起,还能做到数据共享,但是ViewModel和Activity生命周期绑定这点也诞生了一个问题:
如果我的数据获取频率非常高,那岂不是每次要不断注册到Activity生命周期以及取消注册,这样性能肯定不会好,那怎么办呢?
官方也考虑到这个问题,所以提供了一套Transform API来解决这个问题,那这个又是什么东西呢,简单来说就是加了层代理,UI层只要注册这个代理一次就行了,数据获取时会通过另一个LiveData来通知这个代理,说了这么多,来看一下具体实现吧:
class LiveRoomViewMode(private val repo: ILiveRoomRepo) : ViewModel() {
/**
* Transformations输入LiveData
*/
private val heartTransInput = MutableLiveData<HeartObject>()
/**
* Transformations转换后的LiveData
*/
private var heartTransData: LiveData<Result<HeartPollRsp>>? = null
/**
* 频繁调用可通过Transformations来转换数据层的LiveData,
* 这样UI层只需要注册转换后的LiveData就行了,不需要每次更新注册的LiveData
*/
fun getHeartOfTransform(): LiveData<Result<HeartPollRsp>> {
heartTransData ?: let {
heartTransData =
Transformations.switchMap<HeartObject, Result<HeartPollRsp>>(heartTransInput) { heartObject ->
repo.heartBeatPoll(
heartObject.anchorId,
heartObject.playBillId,
heartObject.moduleId,
heartObject.lastSucTime
).getLiveData()
}
}
return heartTransData!!
}
/**
* 更新转换后的LiveData的mSource,UI层在注册之后每次更新参数调用该函数就行了
*/
fun setHeartTransformValue(heartObject: HeartObject) {
heartTransInput.value = heartObject
}
class HeartObject(var anchorId: String, var playBillId: String, var moduleId: Int, var lastSucTime: Long)
}
业务层调用注册ViewModel:
abstract class UiObserver<T> : Observer<Result<T>> {
override fun onChanged(t: Result<T>) {
when (t.status) {
Result.LOADING -> loading()
Result.SUCCESS -> {
if (t.data == null) {
error(10001, "null data")
} else {
success(t.data!!)
}
}
Result.ERROR -> error(t.errorCode, t.message ?: "default error msg")
else -> throw IllegalStateException("lack of handle logic for status:${t.status}")
}
}
open fun loading() {}
open fun success(data: T) {}
open fun error(code: Int, msg: String) {}
}
val observer = object : UiObserver<HeartPollRsp>() {
override fun loading() {
viewModel.loading = true
Log.d(TAG, "loading page:${viewModel.pageNum}")
}
override fun success(data: HeartPollRsp) {
Log.d(TAG, "$data")
}
override fun error(code: Int, msg: String) {
viewModel.loading = false
Log.e(TAG, "error msg:$msg code:$code")
}
}
viewModel.getHeartOfTransform().observe(fragmentActivity as LifecycleOwner, observer)
可以看到我们返回给UI层的不再是简单的LiveData了,而是通过Transformations.switchMap变换后的LiveData,那这个变换做了什么呢,继续看一下:
@MainThread
public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> source,
@NonNull final Function<X, LiveData<Y>> switchMapFunction) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
LiveData<Y> mSource;
@Override
public void onChanged(@Nullable X x) {
LiveData<Y> newLiveData = switchMapFunction.apply(x);
if (mSource == newLiveData) {
return;
}
if (mSource != null) {
result.removeSource(mSource);
}
mSource = newLiveData;
if (mSource != null) {
result.addSource(mSource, new Observer<Y>() {
@Override
public void onChanged(@Nullable Y y) {
result.setValue(y);
}
});
}
}
});
return result;
}
简单介绍一下,result是我们最终返回的LiveData,UI层只要注册一次就行了,然后result会监听source的变化,source其实就是我们原先的参数,每次心跳请求都会触发source的onChanged,在source的onChanged里会调用仓库层的接口获取新的LiveData,这个LiveData数据变化时会通知result,这样result就能同步UI层了
说了这么多,其实你也发现了,Transform其实是用两个LiveData来为原先的仓库层数据做代理,所以也不是那么复杂的。
上面那样做的好处就是UI层只要注册一次LiveData就行了,后面数据变化会间接通知这个LiveData,这样不会触发Activity生命周期的反复注册与取消,减少不必要的开销
- 总结
MVVM仓库层的设计不仅是针对这个模式的,像MVP、MVC等模式也是同样原理,目的都是为了解耦,小型项目可能没有太大感触,但是项目越到后期,这种体会就会越深,没有解耦的话一个小的修改就会触发很多改动以及测试,浪费了很大人力资源,因此每个项目在开始阶段就要设计好整体架构才行!