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

相关推荐
Digitally1 天前
如何用5种实用方法将电脑上的音乐传输到安卓手机
android·智能手机·电脑
HahaGiver6661 天前
Unity与Android原生交互开发入门篇 - 打开Unity游戏的设置
android·unity·交互
2501_915909061 天前
WebView 调试工具全解析,解决“看不见的移动端问题”
android·ios·小程序·https·uni-app·iphone·webview
IT乐手1 天前
android 下载管理工具类
android
2501_915106321 天前
App 怎么上架 iOS?从准备资料到开心上架(Appuploader)免 Mac 上传的完整实战流程指南
android·macos·ios·小程序·uni-app·iphone·webview
科技峰行者1 天前
安卓16提前发布能否改写移动生态格局
android
蒲公英少年带我飞1 天前
Android NDK 编译 protobuf
android
沐怡旸1 天前
【底层机制】ART虚拟机深度解析:Android运行时的架构革命
android·面试
小禾青青1 天前
uniapp安卓打包遇到报错:Uncaught SyntaxError: Invalid regular expression: /[\p{L}\p{N}]/
android·uni-app
studyForMokey1 天前
【Kotlin内联函数】
android·开发语言·kotlin