You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DetailFragment的ViewModel是否应在实例间共享?附修复咨询

商店详情Fragment切换时旧数据残留问题排查与修复

问题背景

我实现了一个商店列表页面StoreFeedFragment,点击列表项会通过replace()切换到StoreDetailFragment展示对应商店详情,所有Fragment切换逻辑在MainActivity中处理。近期遇到一个bug:

  1. 点击商店1,详情页正常显示商店1信息;
  2. 返回列表页后点击商店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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 04:00:45