Fragment返回栈恢复时LiveData Observe重复触发回调问题
问题背景
- 应用基于MainActivity搭建,通过导航组件关联两个页面:ListFragment(列表页)、DetailListFragment(详情页)
- 列表页在
onCreateView生命周期中通过ViewModel发起服务端数据请求,观察ViewModel状态变化,请求成功后将数据填充至RecyclerView - 点击列表项跳转至详情页展示对应内容
- 异常表现:从详情页返回列表页时,ViewModel的状态观察回调会被重复触发,不符合预期。
现有实现代码
ListFragment
class ListFragment : BaseFragment(), ListClickListener { private lateinit var _observeListViewModel: Observer<BaseViewModel.State> lateinit var listViewModel: ListViewModel private lateinit var adapter: ListAdapter private var _binding: ListBinding? = null private val binding get() = _binding!! override fun onDestroyView() { super.onDestroyView() _binding = null listViewModel.state.removeObserver(_observeListViewModel) } private var itemsData = ArrayList<ListResponseDtoListModel>() @SuppressLint("PrivateResource") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = ListBinding.inflate(inflater, container, false) listViewModel = ViewModelProvider(this).get(ListViewModel::class.java) adapter = ListAdapter(itemsData, this) val llm = LinearLayoutManager(requireActivity()) binding.rv.setHasFixedSize(true) binding.rv.layoutManager = llm binding.rv.adapter = adapter // 观察状态并拉取列表 _observeListViewModel = Observer<BaseViewModel.State> { observeListViewModel(it) } listViewModel.state.observe(viewLifecycleOwner, _observeListViewModel) listViewModel.getList(requireContext()) return binding.root } private fun observeListViewModel(state: BaseViewModel.State?) { when (state) { BaseViewModel.State.Loading -> { loadingState() } is BaseViewModel.State.Error -> { errorState() } is BaseViewModel.State.Success -> { val data = state.data as ListModel if (data.status == KopraMobile().SUCCESS) { if (data.content!!.listResponseDtoList.size == 0) { nodataState() } else data.content?.listResponseDtoList.let { successState(it) } } else errorState() } is BaseViewModel.State.SessionTimeout -> { errorState() (parentFragment as BaseFragment).logOut() } is BaseViewModel.State.ErrorResponse -> { errorState() } else -> {} } } private fun successState(it: Any) { // 处理成功状态逻辑 } private fun loadingState() { // 处理加载中逻辑 } private fun nodataState() { // 处理空数据逻辑 } private fun errorState() { // 处理错误逻辑 } override fun onItemClicked(dashboardItem: ListResponseDtoListModel?) { findNavController().navigate(R.id.action_listFragment_to_detailListFragment) } }
DetailListFragment
class DetailListFragment : BaseFragment(){ private var _binding: DetailListBinding? = null private val binding get() = _binding!! override fun onDestroyView() { super.onDestroyView() _binding = null } private var itemsData = ArrayList<DetailListResponseDtoListModel>() @SuppressLint("PrivateResource") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = DetailListBinding.inflate(inflater, container, false) binding.incToolbar1.header.text = "Detail" binding.incToolbar1.back.setImageResource(com.google.android.material.R.drawable.material_ic_keyboard_arrow_previous_black_24dp) binding.incToolbar1.back.setOnClickListener { findNavController().popBackStack() } return binding.root } }
ListViewModel
class ListViewModel : BaseViewModel() { fun getList(context: Context) { _state.postValue(State.Loading) job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch { try { val response = NetworkApi().getListApi().getList( BuildConfig.APPLICATION_ID, Prefs.getPublicAuthorization(context) ) withContext(Dispatchers.Main) { _state.postValue( if (response.isSuccessful) { State.Success( response.headers(), response.body() ) } else { State.ErrorResponse( response.headers(), response.errorBody() ) } ) } } catch (throwble: Throwable) { _state.postValue( State.Error("Error : ${throwble.message.toString()} ") ) } } } }
BaseViewModel
open class BaseViewModel : ViewModel() { sealed class State { object Loading : State() data class Success(val headers: Headers, val data: Any?) : State() data class ErrorResponse(val headers: Headers, val errorResponse: ResponseErrorModel) : State() data class Error(val message: String?) : State() data class SessionTimeout(val sessionTimeout: String?) : State() } var job: Job? = null val exceptionHandler = CoroutineExceptionHandler { _, throwable -> _state.postValue(State.Error("Exception handled: ${throwable.localizedMessage}")) } val _state = MutableLiveData<State>() val state: LiveData<State> get() = _state override fun onCleared() { super.onCleared() job?.cancel() } }
问题原因
重复回调是LiveData的默认设计特性叠加现有实现逻辑导致的:
- LiveData本身存在数据重放机制:当观察者从非活跃状态切回活跃状态、或重新绑定观察者时,LiveData会自动向观察者推送最后一次存储的数据
- 当前使用
ViewModelProvider(this)在ListFragment作用域创建ViewModel,跳转详情页时ListFragment的视图被销毁,但ViewModel实例未被回收,_state中始终持有最后一次请求的Success状态值 - 从详情页返回列表页时,ListFragment重新执行
onCreateView,重新注册观察者,LiveData检测到新的活跃观察者后,会把之前存储的最后一次状态重新推送,触发回调重复执行 - 每次
onCreateView中无差别调用getList()方法,即使没有LiveData重放,也会导致重复发起网络请求。
修复方案
方案1:事件包装类处理一次性状态(最推荐)
网络请求的加载、成功、失败状态都属于一次性消费事件,不应该被重复推送,可以通过通用事件包装类标记事件消费状态,避免重复回调:
- 新增通用Event包装类
open class Event<out T>(private val content: T) { var hasBeenHandled = false private set // 仅在事件未被消费时返回内容,同时标记为已消费 fun getContentIfNotHandled(): T? { return if (hasBeenHandled) { null } else { hasBeenHandled = true content } } // 主动获取内容,不标记消费状态 fun peekContent(): T = content }
- 修改BaseViewModel中的LiveData类型,所有状态都用Event包裹
val _state = MutableLiveData<Event<State>>() val state: LiveData<Event<State>> get() = _state
- 调整所有
postValue调用,给State套上Event
// 示例 _state.postValue(Event(State.Loading)) _state.postValue(Event(State.Success(response.headers(), response.body())))
- 修改观察逻辑,仅处理未被消费的事件
_observeListViewModel = Observer<Event<BaseViewModel.State>> { event -> event.getContentIfNotHandled()?.let { state -> observeListViewModel(state) } }
方案2:调整请求触发时机,避免重复拉取
如果不想修改状态结构,可以在触发请求前增加判断,只有当状态为空(也就是从未加载过数据)时才发起请求:
listViewModel.state.observe(viewLifecycleOwner, _observeListViewModel) // 仅在没有历史数据时拉取接口 if (listViewModel.state.value == null) { listViewModel.getList(requireContext()) }
如果需要在多页面间共享列表数据,可以把ViewModel的作用域提升到导航图或宿主Activity级别,保证数据实例在整个导航流程中唯一,不会因为Fragment视图重建重复初始化。
方案3:使用SingleLiveEvent替代普通MutableLiveData
可以直接使用Android架构组件实践中常用的SingleLiveEvent替换现有MutableLiveData,这个类本身的设计就是只发送一次更新,观察者重新订阅时不会重放历史数据,天然适配一次性请求事件场景。
内容的提问来源于stack exchange,提问作者Amay Diam
相关产品推荐
相关产品推荐

