Android jetpack LiveData (三) 粘性数据(数据倒灌)问题分析及解决方案

发布时间:2026/5/26 9:56:26

Android jetpack LiveData (三) 粘性数据(数据倒灌)问题分析及解决方案 粘性数据问题分析及解决方案粘性数据出现底层原理实际出现场景1、导航事件一次性的界面跳转2、 数据加载状态与一次性错误3、多观察者场景下的意外消费解决粘性数据使用 SingleLiveEventGoogle 官方示例使用 Kotlin Flow SharedFlowSharedFlow简介具体代码1、定义SharedFlow在 Activity/Fragment 中收集事件总结我们在上一篇文章源码分析的时候讲过LiveData 谷歌官方专门设计了这个粘性数据防止数据丢失。例如在页面跳转或配置变更后重复接收旧数据。这里我们再从底层原理开始分析解释为什么先执行setValue后面再observe仍能收到数据最后再提供解决方案。LiveData 的设计初衷是持有并分发状态state而不是事件event。状态是可重复消费的例如界面上的列表数据而事件应该只被处理一次。在实际开发中开发者经常把事件也用 LiveData 来表达于是粘性就导致了上述问题。粘性数据出现底层原理在上一篇文章中我们可以知道每一个LiveData对象都有一个mVersion默认值是-1。在setValue时会调用mVersion此时mVersion是0。在observe方法里面最终会调用上面图片中的判断。当页面配置信息变化或者页面跳转时会相当于第一次执行observe方法。观察者的mLastVersion与LiveData的mVersion不匹配判断无法进入return。就会调用到onChanged响应在observe之前设置的数据了。这个数据我们就称之为粘性数据。具体也可以去看上一篇文章Android jetpack LiveData (二) 原理篇实际出现场景1、导航事件一次性的界面跳转场景在 ViewModel 中通过 LiveData 控制导航例如用户点击按钮后跳转到另一个页面。java// ViewModelMutableLiveDataBooleannavigateToDetailnew MutableLiveData();publicvoidonButtonClick(){navigateToDetail.setValue(true);}// ActivityviewModel.navigateToDetail.observe(this,aBoolean-{startActivity(newIntent(this,DetailActivity.class));});问题当配置变更如旋转屏幕导致 Activity 重建时新的 Activity 会重新注册观察者。由于 LiveData 持有最新的 true 值重建后的 Activity 会立即收到该值从而再次触发导航导致页面重复打开。用户可能只想在按钮点击时跳转一次却因为屏幕旋转而意外再次跳转。粘性带来的后果本应是一次性的事件变成了可重复消费的状态破坏了预期的一次性行为。2、 数据加载状态与一次性错误场景ViewModel 使用 LiveData 表示数据加载状态例如 LOADING、SUCCESS、ERROR。enumStatus{LOADING,SUCCESS,ERROR}MutableLiveDataStatusstatusnew MutableLiveData();publicvoidloadData(){status.setValue(LOADING);// 网络请求...status.setValue(SUCCESS);}// ActivityviewModel.status.observe(this,s-{switch(s){case LOADING:showProgress();break;case SUCCESS:showContent();break;case ERROR:showError();break;}});问题当网络请求出错时status 被设置为 ERRORActivity 显示错误提示。如果此时用户旋转屏幕新的 Activity 会立即收到 ERROR 状态再次显示错误提示尽管错误其实已经处理过或用户已经看到过。粘性带来的后果错误状态被重复消费导致体验不佳。3、多观察者场景下的意外消费场景一个 LiveData 被多个 Fragment 观察用于页面间通信。例如一个 LiveData 用于传递选中的商品 ID。java// 在 Activity 中共享的 LiveDataSharedViewModel sharedVMnewViewModelProvider(requireActivity()).get(SharedViewModel.class);sharedVM.selectedProduct.observe(getViewLifecycleOwner(),productId-{// 更新当前 Fragment 的 UI});问题假设 FragmentA 设置了一个商品 IDFragmentB 同时观察该 LiveData。当 FragmentB 首次创建时它会立即收到之前 FragmentA 设置的值这可能并不是 FragmentB 期望的行为——它可能只想在用户真正操作后接收新值而不是初始状态。粘性带来的后果通信变得不可控新打开的 Fragment 会被“旧数据”污染。解决粘性数据解决这些场景的核心思路是将事件与状态分离采用适合事件的一次性通信机制使用 SingleLiveEventGoogle 官方示例确保事件只会被一个观察者消费一次。改用 Kotlin 协程的 SharedFlowreplay0在 Kotlin 项目中更灵活地处理事件。采用通道Channel或回调接口对于非常明确的一次性通信直接使用接口回调或事件总线谨慎使用。使用 SingleLiveEventGoogle 官方示例SingleLiveEvent 是一个自定义 LiveData只允许一个观察者收到事件且一旦消费后不会重复发送。classSingleLiveEventT:MutableLiveDataT(){privatevalpendingAtomicBoolean(true)// 标记事件是否待处理overridefunobserve(owner:LifecycleOwner,observer:ObserverinT){super.observe(owner){t-if(pending.compareAndSet(true,false)){// 仅首次接收生效observer.onChanged(t)}}}overridefunsetValue(t:T?){pending.set(true)// 发布新事件时重置标记super.setValue(t)}overridefunpostValue(t:T?){pending.set(true)// 发布新事件时重置标记super.postValue(t)}}原理内部通过 AtomicBoolean 标记是否有未消费的事件。每次设置值时将标记设为 true但在分发时只有第一个观察者能将标记从 true 置为 false 并收到事件后续观察者即使注册时得到该值也会因标记已被置为 false 而收不到。优点使用方式与普通 LiveData 相同对观察者透明。缺点仅支持一个观察者收到事件适合导航、Snackbar 等一次性事件。如果多个观察者都需要消费同一个事件则无法满足。使用 Kotlin Flow SharedFlowKotlin 协程的 SharedFlow 可以配置 replay 参数控制新订阅者是否收到之前发送的数据。// 创建一个没有 replay 的 SharedFlowprivateval_eventMutableSharedFlowEventType(replay0)valevent:SharedFlowEventType_event// 发送事件suspendfunsendEvent(data:EventType){_event.emit(data)}// 在 Activity/Fragment 中收集lifecycleScope.launch{viewModel.event.collect{event-// 处理事件}}优点灵活可精确控制 replay 数量、缓冲等。缺点需要引入协程学习成本略高。SharedFlow简介SharedFlow是 Kotlin 协程库提供的一种热流hot stream它可以有多个收集者collector并且支持配置重放replay、缓冲buffer 和 溢出策略onBufferOverflow。关键属性replay当一个新的收集者开始收集时可以收到之前发出的多少个历史值。默认值为 0。extraBufferCapacity额外缓冲区大小用于暂存来不及处理的值。onBufferOverflow缓冲区溢出时的处理策略挂起、丢弃最新、丢弃最旧。对于解决黏性问题我们只需将 replay 设为 0新订阅者就不会收到之前已发送的事件从而实现了非黏性事件流。为什么 SharedFlow 能解决黏性问题LiveData 的黏性源于新观察者注册时会立即收到当前持有的最新值因为内部版本号比较导致。而 SharedFlow 通过 replay0 配置新订阅者不会收到之前已发射的事件只有订阅之后发射的事件才会被收到。这正符合我们对“一次性事件”的期望。在实际架构中应明确区分哪些 LiveData 用于状态可以粘性哪些用于事件不能粘性从而避免因粘性数据引发的问题。官方建议区分状态与事件Google 建议将状态State和事件Event分开管理状态可重复消费适合使用 LiveData/StateFlow带replay。事件仅应消费一次适合使用SingleLiveEvent、SharedFlow(replay0)或Channel。具体代码1、定义SharedFlow通常我们在 ViewModel 内部使用 MutableSharedFlow 来发送事件对外暴露不可变的 SharedFlow。classMainViewModel:ViewModel(){// 事件定义sealedclassUiEvent{objectNavigateToProfile:UiEvent()dataclassShowMessage(valtext:String):UiEvent()}// 创建MutableSharedFlowprivateval_uiEventMutableSharedFlowUiEvent(replay0,extraBufferCapacity1,onBufferOverflowBufferOverflow.DROP_OLDEST)// 对外暴露valuiEvent_uiEvent.asSharedFlow()funonProfileClick(){viewModelScope.launch{//emit 是一个挂起函数需在协程中调用。_uiEvent.emit(UiEvent.NavigateToProfile)}}funonSaveClick(){viewModelScope.launch{// 执行保存操作..._uiEvent.emit(UiEvent.ShowMessage(保存成功))}}}emit是一个挂起函数需在协程中调用。如果不需要背压处理可以省略缓冲区参数但emit可能会挂起直到有消费者收集。通常建议设置一点缓冲区如extraBufferCapacity 1以避免发送方意外挂起。溢出策略可根据需求选择DROP_OLDEST丢弃最旧的、DROP_LATEST丢弃最新的、SUSPEND挂起发送方。在 Activity/Fragment 中收集事件正确收集 SharedFlow 需要考虑生命周期安全避免在后台时浪费资源或引发崩溃。官方推荐使用 repeatOnLifecyclelifecycle-runtime-ktx 2.4.0或 Flow.flowWithLifecycle。使用 repeatOnLifecycle最安全classMainFragment:Fragment(){privatevalviewModel:MainViewModelbyviewModels()overridefunonViewCreated(view:View,savedInstanceState:Bundle?){super.onViewCreated(view,savedInstanceState)// 收集事件viewLifecycleOwner.lifecycleScope.launch{viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED){viewModel.uiEvent.collect{event-when(event){isMainViewModel.UiEvent.NavigateToProfile-{findNavController().navigate(R.id.profileFragment)}isMainViewModel.UiEvent.ShowMessage-{Snackbar.make(requireView(),event.text,Snackbar.LENGTH_SHORT).show()}}}}}// 按钮点击binding.profileButton.setOnClickListener{viewModel.onProfileClick()}binding.saveButton.setOnClickListener{viewModel.onSaveClick()}}}总结LiveData 粘性数据在实际场景中导致的问题根源在于将一次性事件当作了可重复消费的状态来使用。常见场景包括导航控制、提示消息、错误状态显示以及跨页面通信等。开发者需要识别这些场景并采用适当的事件处理机制来避免粘性带来的副作用。

相关新闻