从 NavBackStackEntry 看 Compose 导航的 ViewModel 生命周期管理

测试:小伙子,怎么你的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()
    }
}

当调用 navController.navigate("route") 时:

  1. Navigation 库会检查目标目的地是否需要新建 NavBackStackEntry
  2. 如果是新目的地,就会创建一个新的 NavBackStackEntry,并初始化它的 ViewModelStore
  3. NavBackStackEntry 会被压入返回栈(BackStack)。

当用户按返回键或调用 navController.popBackStack()

  1. Navigation 库会从返回栈弹出最顶层的 NavBackStackEntry
  2. 调用 NavBackStackEntry.destroy(),进而触发 viewModelStore.clear(),释放所有关联的 ViewModel。

3. 为什么不同目的地默认不共享 ViewModelStore

  • 设计目标 :Navigation 库希望每个目的地有独立的状态管理,避免内存泄漏或状态污染。
  • 默认行为 :每个 NavBackStackEntry 持有自己的 ViewModelStore,因此:
    • ScreenAhiltViewModel()NavBackStackEntryA 获取 ViewModel。
    • ScreenBhiltViewModel()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>()
    // ...
}

运行后会发现:

  • 如果 screenAscreenB 是不同的目的地,它们的 ViewModelStore 是不同的对象。
  • 如果它们属于同一个 navigation 导航图,则可能共享同一个 ViewModelStore

5. 共享 ViewModelStore 的几种方式

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 的作用域。

相关推荐
佛系打工仔35 分钟前
绘制K线第二章:背景网格绘制
android·前端·架构
my_power5204 小时前
车载安卓面试题汇总
android
csj504 小时前
安卓基础之《(15)—内容提供者(1)在应用之间共享数据》
android
yeziyfx4 小时前
kotlin中 ?:的用法
android·开发语言·kotlin
2501_915918416 小时前
只有 Flutter IPA 文件,通过多工具组合完成有效混淆与保护
android·flutter·ios·小程序·uni-app·iphone·webview
robotx6 小时前
AOSP 设置-提示音和振动 添加一个带有开关(Switch)的设置项
android
青莲8436 小时前
RecyclerView 完全指南
android·前端·面试
青莲8436 小时前
Android WebView 混合开发完整指南
android·前端·面试
龙之叶6 小时前
【Android Monkey源码解析三】- 运行解析
android
KevinWang_8 小时前
Android 的 assets 资源和 raw 资源有什么区别?
android