前言
这次的实战篇,是这个系列的最后一篇。本文综合前几篇的内容,以伪代码为主,帮大家理解Google所推崇的MVVM。
一点点入坑JetPack:ViewModel篇
一点点入坑JetPack:Lifecycle篇
一点点入坑JetPack:LiveData篇
一点点入坑JetPack:实战前戏NetworkBoundResource篇
一点点入坑JetPack(终章):实战MVVM
相信有耐心看到这的小伙伴,完全足以通过伪代码,感受出来以下代码的设计思路。Go~
正文
一、日常业务
上代码之前,我们思考一个小问题。我们平时的业务,很重的一个部分是从一个地方获取数据,然后在UI上展示出来。因此,本节实战部分的背景:从网络获取一批数据,如果网络请求成功,便更新到RecycleView上;如果网络请求不成功,加载本地已有缓存,然后更新到RecycleView上。
是不是很简单的需求,很多小伙伴可能顺手就能写出来:
// 网络请求
loadNetwork(参数, Callback(){
// 请求成功,更新UI
success(data){
recyclerview.setData
}
// 请求失败,读取缓存
error(){
loadDB(参数,Callback(){
// 缓存读取成功,更新UI
success(data){
recyclerview.setData
}
})
}
})
复制代码
非常直观且易阅读。我们在深入想一下,如果其他页面也有这样的需求,是不是也要写一份这个内容?
这里肯定有小伙伴会指出,应该进行封装!没错,还记得上一篇文章提到的NetworkBoundResource
吗?接下来,就让我们通过NetworkBoundResource
,使用MVVM的思想去封装这个业务。
二、走进MVVM
2.1、走进MVVM流程图
针对MVVM官方提供的一张比较清晰的流程图:
2.2、走进MVVM代码
按照官方的推荐,我们需要一个Repository作为整个数据层的管理者。
例如,我们设计一个加载歌曲信息,然后更新到RecycleView上的需求。这个Repository咱们就叫,MusicRepository,表示音乐相关的数据获取交由这个类去管理。
那么这个Repository是什么样子的呢?
1、Repository
MusicRepository
// 这里的三个参数,分别是:线程池,缓存模块,网络模块
class MusicRepository(
val appExecutors: AppExecutors,
val musicDao: MusicDao, // 后文会展开这个类
val service: MusicApiService // 后文会展开这个类(具体的请求模块)
) {
companion object {
val inst: MusicRepository by lazy {
// 这里传入的内容,当然是业务方自己去实现,比如这前业务已经存在的DB/内存缓存模块;封装好的网络请求模块,比如OkHttp/Retrofit等等
MusicRepository(xxx,xxx,xxx)
}
}
// Parameter会在后续中展开
fun querySongs(parameter : Parameter): LiveData> {
return object :
NetworkBoundResource(
appExecutors
) {
override fun saveCallResult(item: MusicResp) {
// 网络请求成功,先存入缓存模块
musicDao.saveDB(item)
}
override fun shouldFetch(data: MusicResp?): Boolean {
return 自己的是否请求网络策略
}
override fun loadFromDb(): LiveData {
return musicDao.getCacheMusicResp(parameter.categoryId)
}
override fun createCall(): LiveData> {
// 调用网络模块的请求实现
return service.querySongs(parameter)
}
}.asLiveData()
}
}
复制代码
接下来咱们挨个展开上述代码中用到的类,MusicDao一个负责我们的Cache的实现类:
MusicDao
object MusicDao {
private val musicStoreSongs: MutableMap<Long, MusicResp> by lazy {
mutableMapOf<Long, MusicResp>()
}
fun updateSongsCache(categoryId: Long, data: MusicResp) {
musicStoreSongs[categoryId] = data
}
fun querySongsCache(categoryId: Long): LiveData {
val cacheSongLiveData = MutableLiveData()
cacheSongLiveData.value = musicStoreSongs[categoryId]
return cacheSongLiveData
}
}
复制代码
这里仅仅是实现了一套内存缓存。基于此我们还可以实现自己的数据库缓存,或者内存+数据库的二级缓存。而这一切的实现并不会对外边的逻辑产生影响,做到了实现的隔离。
接下来,咱们来看看网络请求的实现类:MusicApiService
这里涉及了协程的内容,建议没有相关基础的小伙伴,可以看一看我之前写过的文章。
总是在聊线程Thread,试试协程吧!
MusicApiService
object MusicApiService {
override fun querySongs(parameter : Parameter): LiveData> {
val liveData = MutableLiveData>()
CoroutineScope(FastMain).launch {
val resp = resp = withContext(BuzzApiPool) {
// 这里对应的是业务方自己的网络实现封装
val np = NetWorkManager.getInstance().networkProvider
val builder = Uri.parse("服务端的请求接口")
.buildUpon()
builder.appendQueryParameter("category_id", parameter.categoryId)
try {
// 自己封装的get请求
val json = np.networkClient.get(builder.toString())
// 这里封装的是Gson把String转成JavaBean的方法
val data: MusicResp = fromServerResp(json)
data
} catch (e: Exception) {
MusicResp(e)
}
if (resp.isSuccess)) {
liveData.postValue(ApiSuccessResponse(resp))
} else {
liveData.postValue(
ApiErrorResponse(resp.exception ?: RuntimeException("unknown_error"))
)
}
}
return liveData
}
}
复制代码
有了Repository之后,我们则需要考虑一下ViewModel了。就叫MusicViewModel
2、ViewModel
class MusicViewModel :ViewModel(){
// Parameter 伪码
var parameter = MutableLiveData()
val data : LiveData> = Transformations.switchMap(parameter) { parameter->
MusicRepository.inst.querySongs(parameter)
}
}
复制代码
3、Activity/Fragment
ViewModel这样就够了,接下来就是我们的UI,这里就叫MusicActivity
吧。
class MusicActivity : AppCompatActivity(){
private lateinit var musicViewModel: MusicViewModel
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.xxx)
musicViewModel = ViewModelProviders.of(this).get(MusicViewModel::class.java)
musicViewModel.data.observe(this, Observer { musicResp->
// 这里监听的数据就是MusicRepository返回的MusicResp
adapter.setData(musicResp)
}
// 通过LiveData通知MusicRepository进行网络请求
musicViewModel.parameter.value=Parameter(categoryId = xx) //本次请求的参数
}
}
复制代码
到这里,我们最基本的使用,就完成了。
对于UI层来说:
- 它只需要在自己需要请求数据的时候通过MusicViewModel给“parameter”这个LiveData赋一个真正的请求参数就可以了。
Transformations.switchMap(参数)
会收到变换然后执行MusicRepository.inst.querySongs(请求参数)
,之后的所有逻辑全部交由MusicRepository
去处理。- 至于怎么加载网络,怎么处理缓存,都是有各个独立的模块实现的。
- 此外UI层在监听
musicViewModel.data
的结果,更新UI即可。
这样你会发现,对于Activity/Fragment来说,它就只是View层了,一点逻辑操作都没有。
当然这是理想状态,毕竟PM拥有无穷的想象力,什么样的需求都会存在。
2.3、存在问题
我猜理解清楚这套设计的小伙伴,一定会之处问题所在。那就是:
1、性能问题
adapter.setData(musicResp)
,这就意味着,每次数据回调回来RecycleView都会更新,这样就产生了很多无用的刷新。 而且这里是监听这个数据对象,如果想进行局部刷新,那么Activity/Fragment中势必要做很多额外的逻辑操作...
没错!这是一个严重的问题,但实际上Google早在很久之前就提供了一个类DiffUtil
,这个类可以说完美的帮我们在这套设计里,搞定了RecycleView空刷的性能消耗。
如果有必要,下篇文章可以聊一聊
DiffUtil
和Immutable、Mutable的理念
2、额外的业务逻辑
毕竟有些时候,我们没办法这么直来直去的加载数据。更多的时候,我们需要在业务回来时进行一系列的额外代码:比如数据的变换、逻辑的判断...
-
数据变换:这类操作,可以使用函数式编程的思想,很方便的在ViewModel中完成并通过LiveData通知给observe方。
-
逻辑的判断:这部分内容,并不属于MVVM(数据驱动)的部分。所以至于它还需要仁者见仁智者见智的封装...
想了很久,还是觉得在此就停下实战篇的内容。因为我以为这已经够了,如果能消化这整个系列的内容,我相信该怎么使用JetPack,小伙伴们心中已经有了自己的想法~
当然,小伙伴们如果有什么更骚的操作,欢迎留言交流呦~
尾声
JetPack系列的文章,到此便告一段落了。不知道一路追过来的朋友们是否有收获。
下一个长篇系列会是什么内容,暂时还没有想好。大家有啥感兴趣的,可以留言给点建议~