一点点入坑JetPack(终章):实战MVVM

3,684 阅读6分钟

前言

这次的实战篇,是这个系列的最后一篇。本文综合前几篇的内容,以伪代码为主,帮大家理解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<Resource<MusicResp>> {
        return object :
            NetworkBoundResource<MusicResp, MusicResp>(
                appExecutors
            ) {

            override fun saveCallResult(item: MusicResp) {
                // 网络请求成功,先存入缓存模块
                musicDao.saveDB(item)
            }

            override fun shouldFetch(data: MusicResp?): Boolean {
                return 自己的是否请求网络策略
            }

            override fun loadFromDb(): LiveData<MusicResp> {
                return musicDao.getCacheMusicResp(parameter.categoryId)
            }

            override fun createCall(): LiveData<ApiResponse<MusicResp>> {
	            // 调用网络模块的请求实现
                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<MusicResp> {
        val cacheSongLiveData = MutableLiveData<MusicResp>()
        cacheSongLiveData.value = musicStoreSongs[categoryId]
        return cacheSongLiveData
    }
}

这里仅仅是实现了一套内存缓存。基于此我们还可以实现自己的数据库缓存,或者内存+数据库的二级缓存。而这一切的实现并不会对外边的逻辑产生影响,做到了实现的隔离。

接下来,咱们来看看网络请求的实现类:MusicApiService

这里涉及了协程的内容,建议没有相关基础的小伙伴,可以看一看我之前写过的文章。

总是在聊线程Thread,试试协程吧!

MusicApiService


object MusicApiService {
    
    override fun querySongs(parameter : Parameter): LiveData<ApiResponse<MusicResp>> {
        val liveData = MutableLiveData<ApiResponse<MusicResp>>()
        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<Parameter>()
	val data : LiveData<Resource<MusicResp>> = 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系列的文章,到此便告一段落了。不知道一路追过来的朋友们是否有收获。

下一个长篇系列会是什么内容,暂时还没有想好。大家有啥感兴趣的,可以留言给点建议~

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:咸鱼正翻身