最近从 Dagger 迁移到 Hilt 后,我开始观察到 ViewModel 的非常奇怪的行为。下面是代码片段:
@HiltAndroidApp
class AndroidApplication : Application() {}
@Singleton
class HomeViewModel @ViewModelInject constructor() :
ViewModel() {}
@AndroidEntryPoint
class HomeFragment : Fragment(R.layout.fragment_home) {
private val homeViewModel by viewModels<HomeViewModel>()
override fun onResume() {
super.onResume()
Timber.i("hashCode: ${homeViewModel.hashCode()}")
}
}
@AndroidEntryPoint
class SomeOtherFragment : Fragment(R.layout.fragment_home) {
private val homeViewModel by viewModels<HomeViewModel>()
override fun onResume() {
super.onResume()
Timber.i("hashCode: ${homeViewModel.hashCode()}")
}
}
所有分片的hashCode值不一致。我无法弄清楚我还缺少什么来在活动中生成视图模型的单例实例。
我正在使用单一活动设计并添加了所有必需的依赖项。
当您使用
by viewModels
时,您正在创建一个作用于该单独 Fragment 的 ViewModel - 这意味着每个 Fragment 将拥有该 ViewModel 类的自己的单独实例。如果您希望单个 ViewModel 实例作用于整个 Activity,您需要使用 by activityViewModels
private val homeViewModel by activityViewModels<HomeViewModel>()
Ian说的是对的,
by viewModels
是Fragment的扩展函数,它会使用Fragment作为ViewModelStoreOwner。
如果您需要将其范围限定在 Activity 内,则可以使用
by activityViewModels
。
但是,您通常不需要 Activity 范围内的 ViewModel。它们在单活动应用程序中实际上是全局的。
要创建 Activity 全局无状态组件,可以使用 Hilt 中的
@ActivityRetainedScope
。这些将可供您在 Activity 或 Fragment 中创建的 ViewModel 使用。
要创建有状态保留组件,您应该依赖 ~~
@ViewModelInject
和 @Assisted
~~ @HiltViewModel
和 @Inject constructor
来获取 SavedStateHandle。
很有可能,此时您真正想要的不是 Activity 范围的 ViewModel,而是 NavGraph 范围的 ViewModel。
要将 SavedStateHandle 获取到 Fragment 内的 NavGraph 范围的 ViewModel 中,请使用
val vm = androidx.hilt.navigation.fragment.hiltNavGraphViewModels(R.navigation.nav_graph_id)
。
如果您不使用 Hilt,则可以使用
= navGraphViewModels
,但您可以使用默认的 ViewModelProvider.Factory 或 CreationExtras 获取 SavedStateHandle。
这是 ianhanniballake 提到的替代解决方案。它允许您在片段之间共享视图模型,而不将其分配给活动,因此您可以避免在单个活动中创建本质上的全局视图模型,如 EpicPandaForce 所说。如果您使用导航组件,则可以为要共享视图模型的片段创建嵌套导航图(遵循本指南:嵌套导航图)
在每个片段中:
private val homeViewModel: HomeViewModel
by navGraphViewModels(R.id.nested_graph_id){defaultViewModelProviderFactory}
当您导航出嵌套图时,视图模型将被删除。当您导航回到嵌套图表时,它将重新创建。
正如此处其他帖子所提到的,使用
by activityViewModels<yourClass>()
会将 VM 范围限定到整个 Activity 的生命周期,使其有效地成为整个应用程序的全局范围(如果它是每个人都使用且 Google 推荐的一个 Activity 架构)。
干净、最小的解决方案: 如果您使用导航图范围视图模型:
替换这个:
val vm: SomeViewModel by hiltNavGraphViewModels(R.id.nav_vm_id)
如下:
val vm by activityViewModels<SomeViewModel>()
这允许我使用这个虚拟机作为活动和这些片段之间的共享视图模型。
否则,即使实时数据观察者也无法工作,因为它会创建彼此独立的新实例和生命周期。
您可以将以下扩展添加到您的代码中,您将能够检索返回堆栈中先前片段的任何 ViewModel 的相同实例。
@MainThread
public inline fun <reified VM : ViewModel> Fragment.backStackViewModels(
@IdRes destinationId: Int
): Lazy<VM?> = lazy {
findNavController().currentBackStack.value.lastOrNull { it.destination.id == destinationId }
?.let { backStackEntry ->
ViewModelLazy(
VM::class,
{ backStackEntry.viewModelStore },
{ HiltViewModelFactory(requireActivity(), backStackEntry) },
{ defaultViewModelCreationExtras }
).value
} ?: run { null }
首次使用 hiltNavGraphViewModels 必须在所有者片段中创建 viewModel,如下所示:
private val mViewModel by hiltNavGraphViewModels<SomeViewModel>(R.id.someDestination)
然后可以使用 backStackViewModels 在任何其他后续片段中检索 viewModel 的相同实例,如下所示:
private val mViewModel by backStackViewModels<SomeViewModel>(R.id.someDestination)
注:
Github要点链接