测试:小伙子,怎么你的App不根据角度,进行横竖屏切换了。上个版本还没事呢?
bug介绍
有个数据源,是单例模式, 使用的接口回调的方式,更新Viewmodel里的数据,ViewModel实现更新数据的接口,viewModel 实例化的时候,将实例通过set方法,注入到数据源里。 我在NavHost通过hiltViewModel外边实例化了一个接收角度的viewModel,用来控制屏幕方向,横屏和竖屏切换 在Navhost里首页里,用hiltViewModel 实例化了一个相同的viewModel,用来显示实时的角度。
我在Compose里是用hiltViewModel()方法实例化,第一个版本,是没用Compose的Navgation,是没有问题的。viewModel只有一个实例,所有数据更新会跟着更新。
用了Navgation之后,数据就不会更新了,疯狂挠头。debug了半天,最后打印了个日志,发现。viewModel的init方法走了两遍。
在 Navigation Compose 中,ViewModelStore
是由 NavBackStackEntry
实例化的 ,而 NavBackStackEntry
是导航库用来管理每个目的地(destination)生命周期的核心组件。下面详细解释它的创建和管理机制:
1. ViewModelStore
的实例化时机
ViewModelStore
是由 NavBackStackEntry
在初始化时创建的:
- 当导航到一个新目的地时,Navigation 库会创建一个新的
NavBackStackEntry
。 NavBackStackEntry
内部持有一个ViewModelStore
,用于管理该目的地的 ViewModel 实例。- 这个
ViewModelStore
会跟随NavBackStackEntry
的生命周期 (即当目的地被移除时,ViewModelStore
也会被清除)。
关键代码逻辑(简化版)
kotlin
// NavBackStackEntry 的简化实现
class NavBackStackEntry(
// ...
) : ViewModelStoreOwner { // 实现了 ViewModelStoreOwner 接口
private val viewModelStore = ViewModelStore() // 在这里实例化
override fun getViewModelStore(): ViewModelStore {
return viewModelStore
}
// 当该 Entry 被销毁时,清理 ViewModelStore
fun destroy() {
viewModelStore.clear()
}
}
2. NavBackStackEntry
如何管理 ViewModelStore
?
(1) 导航时创建新的 NavBackStackEntry
当调用 navController.navigate("route")
时:
- Navigation 库会检查目标目的地是否需要新建
NavBackStackEntry
。 - 如果是新目的地,就会创建一个新的
NavBackStackEntry
,并初始化它的ViewModelStore
。 - 该
NavBackStackEntry
会被压入返回栈(BackStack)。
(2) 返回时销毁 NavBackStackEntry
当用户按返回键或调用 navController.popBackStack()
:
- Navigation 库会从返回栈弹出最顶层的
NavBackStackEntry
。 - 调用
NavBackStackEntry.destroy()
,进而触发viewModelStore.clear()
,释放所有关联的 ViewModel。
3. 为什么不同目的地默认不共享 ViewModelStore
?
- 设计目标 :Navigation 库希望每个目的地有独立的状态管理,避免内存泄漏或状态污染。
- 默认行为 :每个
NavBackStackEntry
持有自己的ViewModelStore
,因此:ScreenA
的hiltViewModel()
从NavBackStackEntryA
获取 ViewModel。ScreenB
的hiltViewModel()
从NavBackStackEntryB
获取 ViewModel。- 两者默认不是同一个实例。
4. 如何验证 ViewModelStore
的作用域?
可以通过以下方式观察 ViewModelStore
的归属:
kotlin
composable("screenA") {
val backstackEntry = LocalViewModelStoreOwner.current as NavBackStackEntry
Log.d("NAV_DEBUG", "ScreenA ViewModelStore: ${backstackEntry.viewModelStore}")
val vm = hiltViewModel<SharedViewModel>()
// ...
}
composable("screenB") {
val backstackEntry = LocalViewModelStoreOwner.current as NavBackStackEntry
Log.d("NAV_DEBUG", "ScreenB ViewModelStore: ${backstackEntry.viewModelStore}")
val vm = hiltViewModel<SharedViewModel>()
// ...
}
运行后会发现:
- 如果
screenA
和screenB
是不同的目的地,它们的ViewModelStore
是不同的对象。 - 如果它们属于同一个
navigation
导航图,则可能共享同一个ViewModelStore
。
5. 共享 ViewModelStore
的几种方式
(1) 使用同一个 navigation
导航图
kotlin
NavHost(navController, "main") {
navigation(startDestination = "screenA", route = "feature") {
composable("screenA") { /* 共享 ViewModelStore */ }
composable("screenB") { /* 共享 ViewModelStore */ }
}
}
(2) 手动指定 ViewModelStoreOwner
kotlin
val parentEntry = remember { navController.getBackStackEntry("parent_route") }
val vm = hiltViewModel<SharedViewModel>(parentEntry)
(3) 使用 Activity 作用域(全局共享)
kotlin
val activity = LocalContext.current as ComponentActivity
val vm = hiltViewModel<SharedViewModel>(activity)
总结
关键点 | 说明 |
---|---|
ViewModelStore 由谁创建? |
NavBackStackEntry 在初始化时创建 |
默认作用域 | 每个导航目的地(NavBackStackEntry )有自己的 ViewModelStore |
如何共享? | 使用同一导航图、手动指定 ViewModelStoreOwner 或使用 Activity 作用域 |
何时销毁? | 当 NavBackStackEntry 被移除时,ViewModelStore 会调用 clear() |
这种设计确保了 Compose Navigation 的灵活性和内存安全性,同时允许开发者按需控制 ViewModel 的作用域。