DetailFragment的ViewModel是否应在实例间共享?附修复咨询
商店详情Fragment切换时旧数据残留问题排查与修复
问题背景
我实现了一个商店列表页面StoreFeedFragment,点击列表项会通过replace()切换到StoreDetailFragment展示对应商店详情,所有Fragment切换逻辑在MainActivity中处理。近期遇到一个bug:
- 点击商店1,详情页正常显示商店1信息;
- 返回列表页后点击商店2,详情页先显示商店1的旧数据,之后才更新为商店2的信息,不符合预期。
相关核心代码如下:
MainActivity代码
class MainActivity : AppCompatActivity() { @Inject lateinit var storeItemViewModelFactory: ViewModelFactory<StoreItemViewModel> private val storeItemViewModel: StoreItemViewModel by lazy { storeItemViewModelFactory.get<StoreItemViewModel>(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) TCApplication.getAppComponent().inject(this) setContentView(R.layout.activity_main) if (savedInstanceState == null) { val storeFeedFragment = StoreFeedFragment() supportFragmentManager.beginTransaction() .replace(R.id.container, storeFeedFragment, StoreFeedFragment.TAG) .commit() } storeItemViewModel.selectedItem.observe(this) { event -> event.getContentIfNotHandled()?.apply { val fragment = StoreDetailFragment.newInstance(id) supportFragmentManager.beginTransaction() .replace(R.id.container, fragment) .addToBackStack("feed_to_item_tag") .commit() } } } }
Event事件包装类
class Event<T>(private val content: T) { var hasBeenHandled = false private set fun getContentIfNotHandled(): T? { return if (hasBeenHandled) null else { hasBeenHandled = true content } } }
StoreDetailFragment与ViewModel代码
class StoreDetailFragment : Fragment() { @Inject lateinit var viewModelFactory: ViewModelFactory<StoreDetailViewModel> private val viewmodel: StoreDetailViewModel by lazy { viewModelFactory.get<StoreDetailViewModel>(requireActivity()) } companion object { private const val SELECTED_ID = "selected" fun newInstance(storeId: String): StoreDetailFragment { val fragment = StoreDetailFragment().also { it.arguments = bundleOf(Pair(SELECTED_ID, storeId)) } return fragment } } override fun onCreate(savedInstanceState: Bundle?) { TCApplication.getAppComponent().inject(this) super.onCreate(savedInstanceState) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_store_detail, container, false) viewmodel.storeDetailResult.observe(viewLifecycleOwner) { resultState -> when (resultState) { is ResultState.Loading -> { } is ResultState.Success -> { showDetails(resultState.value) } is ResultState.Failure -> { Snackbar.make( view, "Error getting store details ${resultState.ex.message}", 500 ).show() } } } arguments?.getString(SELECTED_ID)?.let { storeId -> viewmodel.loadStoreDetails(storeId) } return view } private fun showDetails(storeDetails: StoreDetail) { view?.apply { findViewById<TextView>(R.id.name).text = storeDetails.name findViewById<TextView>(R.id.phoneNo).text = storeDetails.phoneNo } } } class StoreDetailViewModel @Inject constructor( private val storeDetailRepository: StoreDetailRepository ): ViewModel(), CoroutineScope by MainScope() { private val _storeDetailResult = MutableLiveData<ResultState<StoreDetail>>() val storeDetailResult: LiveData<ResultState<StoreDetail>> = _storeDetailResult fun loadStoreDetails(storeId: String) { viewModelScope.launch { try { storeDetailRepository.getStoreDetail(storeId) .collect { storeDetail -> _storeDetailResult.postValue(ResultState.Success(storeDetail)) } } catch (e: Exception) { _storeDetailResult.postValue(ResultState.Failure("getStoreDetail($storeId)", e)) } } } }
ViewModelFactory代码
class ViewModelFactory<T: ViewModel> @Inject constructor(private val viewModel: Lazy<T>) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return viewModel.get() as T } inline fun <reified R: T> get(viewModelStoreOwner: ViewModelStoreOwner): T { return ViewModelProvider(viewModelStoreOwner, this)[R::class.java] } }
问题分析
1. StoreDetailViewModel是否应该共享?
绝对不应该。每个StoreDetailFragment对应独立的商店详情页面,属于短期、独立的页面生命周期,共享ViewModel会导致旧页面的数据被新页面复用,引发数据污染和显示异常。
2. 问题根源
StoreDetailFragment获取ViewModel时使用requireActivity()作为ViewModelStoreOwner,导致ViewModel绑定到Activity生命周期,所有详情Fragment实例共享同一个ViewModel;ViewModelFactory通过Lazy<T>注入单例ViewModel,进一步强化了实例共享的问题;- LiveData具有粘性特性,新Fragment创建后观察LiveData时,会立即收到ViewModel中保存的旧数据(商店1的详情),之后才加载新数据,造成先显示旧数据的异常。
修复方案
1. 让ViewModel绑定到Fragment自身生命周期
修改StoreDetailFragment中获取ViewModel的代码,将requireActivity()替换为this,让每个Fragment实例拥有独立的ViewModel:
private val viewmodel: StoreDetailViewModel by lazy { viewModelFactory.get<StoreDetailViewModel>(this) // 替换为Fragment自身作为StoreOwner }
2. 修正ViewModelFactory的单例问题
当前Factory依赖单例注入的ViewModel,需要改为支持多实例的通用实现(结合Dagger的@IntoMap绑定):
修改后的ViewModelFactory
class ViewModelFactory @Inject constructor( private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>> ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { val provider = viewModels[modelClass] ?: throw IllegalArgumentException("Unknown ViewModel class: $modelClass") return provider.get() as T } inline fun <reified R : ViewModel> get(viewModelStoreOwner: ViewModelStoreOwner): R { return ViewModelProvider(viewModelStoreOwner, this)[R::class.java] } }
添加Dagger ViewModel绑定模块
@Module abstract class ViewModelModule { @Binds @IntoMap @ViewModelKey(StoreDetailViewModel::class) abstract fun bindStoreDetailViewModel(viewModel: StoreDetailViewModel): ViewModel // 其他ViewModel绑定同理 } @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) @Retention(AnnotationRetention.RUNTIME) @MapKey annotation class ViewModelKey(val value: KClass<out ViewModel>)
3. 优化ViewModel状态(可选但推荐)
在加载新数据前发送Loading状态,覆盖旧的Success状态,避免旧数据被立即展示:
fun loadStoreDetails(storeId: String) { viewModelScope.launch { _storeDetailResult.postValue(ResultState.Loading) // 先发送加载状态 try { storeDetailRepository.getStoreDetail(storeId) .collect { storeDetail -> _storeDetailResult.postValue(ResultState.Success(storeDetail)) } } catch (e: Exception) { _storeDetailResult.postValue(ResultState.Failure("getStoreDetail($storeId)", e)) } } }
总结
通过让每个StoreDetailFragment拥有独立的ViewModel实例,并修正Factory的实例创建逻辑,彻底解决了旧数据残留的问题。核心思路是让ViewModel的生命周期与所属Fragment绑定,而非复用Activity级别的共享实例。
内容的提问来源于stack exchange,提问作者proj
相关产品推荐
相关产品推荐

