MVVM架构下参数变更后重载数据的LiveData用法及SwitchMap实现
现有实现的问题根源
你现在遇到的观察者多次调用、必须手动加触发方法才能拿到数据的问题,本质是三层的数据流设计错位:
Repository层把spotlights作为类成员变量全局持有,所有网络请求的结果都会往同一个LiveData里写入,既没有按请求参数做结果隔离,也没有取消上一个未完成请求的机制,很容易出现旧请求结果覆盖新请求结果的竞态问题。ViewModel层反复替换spotslightsLiveData的实例引用,但没有提供一个固定不变的LiveData供页面绑定观察,导致页面的观察者很容易和实际数据流脱节,必须靠手动反复解绑、重绑才能收到回调。- 页面层手动
removeObservers再重新观察的操作完全是冗余的,因为LiveData本身已经和viewLifecycleOwner绑定,会在页面销毁时自动移除观察者,手动反复操作很容易出现观察者被多绑、重复触发的问题。
该场景完全适配SwitchMap的使用
SwitchMap的核心特性就是上游触发值变化时,自动切换到对应映射的下游数据流,自动废弃上一个数据流的结果,永远把最新触发值对应的结果传递给观察者,刚好匹配你“根据传入的segmentId、segmentType动态发起网络请求,把最新的请求结果给到页面”的场景。
完全可以通过SwitchMap把整个数据流理顺,从根源上解决你现在遇到的所有问题,具体实现分三步:
第一步:修正Repository层的实现
去掉Repository里全局持有spotlights成员变量的逻辑,把请求逻辑封装成传入参数、返回单次请求对应LiveData的方法,让单次请求的结果和请求本身做绑定,不再全局共享同一个LiveData:
class SpotlightRepository(val application: NewsApplication) { fun getSpotLight(segmentId: String, segmentType: String): LiveData<List<SpotlightModel>?> { // 每次请求生成独立的LiveData实例,避免全局共享带来的结果覆盖 val resultLiveData = MutableLiveData<List<SpotlightModel>?>() mProvider.getSpotLight( segmentType = segmentType, adType = TYPE_SPOT_LIGHT, segmentId = segmentId, topics = application.profileManger.topicsIds, publishers = application.profileManger.publisherIds, debug = application.debugFlag, deviceId = application.idfa ).enqueue(object : Callback<List<SpotlightModel>> { override fun onResponse(call: Call<List<SpotlightModel>>, response: Response<List<SpotlightModel>>) { resultLiveData.postValue(if (response.isSuccessful) response.body() else null) } override fun onFailure(call: Call<List<SpotlightModel>>, t: Throwable) { resultLiveData.postValue(null) } }) return resultLiveData } }
第二步:ViewModel层用SwitchMap打通参数到请求结果的链路
在ViewModel内设置一个存储请求参数的触发器LiveData,通过Transformations.switchMap把参数和Repository的请求结果做映射,对外暴露固定不变的可观察LiveData,不需要再手动替换LiveData实例:
class SpotlightViewModel(val application: NewsApplication) : ViewModel() { // 触发器:存储当前需要请求的segment参数对,参数更新就自动触发新的请求 private val requestTrigger = MutableLiveData<Pair<String, String>>() private val repository = SpotlightRepository(application) // SwitchMap映射:参数变化时自动切换到对应请求的LiveData,自动丢弃旧请求的结果 val spotlightsLiveData: LiveData<List<SpotlightModel>?> = Transformations.switchMap(requestTrigger) { params -> val (segmentId, segmentType) = params repository.getSpotLight(segmentId, segmentType) } // 对外提供的更新请求参数方法,不需要手动调用repository的请求逻辑 fun updateRequest(segmentId: String, segmentType: String) { // 相同参数不重复触发,避免无意义的网络请求 if (requestTrigger.value != segmentId to segmentType) { requestTrigger.value = segmentId to segmentType } } }
注:如果引入了androidx.lifecycle的ktx扩展库,也可以用更简洁的
switchMap语法替代Transformations.switchMap,两者逻辑完全一致。
第三步:页面层简化观察逻辑
因为spotlightsLiveData是ViewModel内固定不变的实例,只需要在页面初始化时绑定一次观察者即可,后续所有请求结果更新都会通过同一个LiveData回调,不需要再反复解绑、重绑:
// 页面初始化时绑定一次观察者即可 override fun onViewCreated() { super.onViewCreated() spotlightViewModel.spotlightsLiveData.observe(viewLifecycleOwner) { spotlightList -> // 统一处理列表数据更新 newNewsAdapter?.spotLights = spotlightList?.takeIf { it.isNotEmpty() } newNewsAdapter?.notifyItemChanged(0) } } // 切换请求参数时直接调用ViewModel方法即可 fun getSpotlights(s_id: String, s_type: String) { spotlightViewModel?.updateRequest(s_id, s_type) }
优化后的效果
- 观察者只绑定固定的LiveData实例,不会出现多绑、重复触发的问题。
- 快速切换不同segment参数时,SwitchMap会自动丢弃上一个未完成请求的结果,只会把最新一次请求的结果推送给页面,彻底解决数据竞态问题。
- 去掉了所有手动替换LiveData、手动解绑重绑观察者的冗余逻辑,整个MVVM数据流是单向固定的,符合架构设计的规范。
内容的提问来源于stack exchange,提问作者hagar Mohamad
相关产品推荐
相关产品推荐

