Compose - 在 Navigation Graph 内部和外部获取相同的 ViewModel 实例

问题描述 投票:0回答:1

我的 Compose 应用程序中的顶级可组合项的结构如下:

ModalBottomSheetLayout(/*...*/) {
    Scaffold(
        topBar = {
            when (currentScreen) {
                /*...*/
            }
        },
        content = {
            AppNavigation(navController)
        },
        bottomBar = {
            // Bottom navigation
        }
    )
}

因为我将

BottomSheet
BottomNavigation
一起使用,所以我不能将前者的处理委托给
AppNavigation
的屏幕,因为它会违反Material Design指南(
BottomSheet
将显示在
BottomNavigation
上方)。

根据我的其他帖子

,我屏幕的顶部栏也与屏幕本身分开

当然,由于绑定到屏幕的底部表单必须反映此屏幕的某些状态,我们想在它们之间共享一个

ViewModel
。顶部栏也是如此。

上述限制必然意味着所需

ViewModel
的实例必须在所有提到的可组合项的范围之外创建,因此NavHost
 及其范围内的 
ViewModelStore
 之外
,这是一个巨大的问题,因为只有其他
ViewModelStore
是活动拥有的,在单一活动模式中永远不会被清除!因此,每个共享的
ViewModel
实际上都变成了单例和内存泄漏,除了它导致的奇怪错误(需要通过创建新实例来更新但不是)。我真正想要的是在用户离开它所绑定的屏幕时销毁共享视图模型,但这似乎不可能。

那么,是否有可能在不改变 UI 结构的情况下解决这个问题(以及首先处理导致它的其他问题)?它甚至需要修理吗?如果修复了与状态相关的错误,单例视图模型是否可以?

kotlin android-jetpack-compose android-viewmodel jetpack-compose-navigation
1个回答
0
投票

在将近半年没有人回答这个问题后,这个问题在我的项目中变得很关键,所以我开始寻找解决方案。经过几天的谷歌搜索,我几乎准备好通过反射手动添加和删除视图模型到

Activity
ViewModelStore
中或从中删除视图模型,但幸运的是我找到了一个单线解决方案。
我在我的项目中使用 Hilt,所以为了获得目标范围的
ViewModel
s 我使用以下扩展函数:

@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.hiltViewModel() =
    ViewModelProvider(
        this.viewModelStore,
        HiltViewModelFactory(LocalContext.current, this)
    )[T::class.java]
private fun NavGraphBuilder.projectsGraph() {
    navigation(
        startDestination = AppTab.Projects.startDestination,
        route = AppTab.Projects.route
    ) {
        composable(AppScreen.Projects.route) {
            Projects()
        }

        composable(AppScreen.ProjectDetails.route) {
            ProjectDetails(projectViewModel = it.hiltViewModel()) // <--
        }
    }
}

Hilt根据自己作用域

ViewModel
内的
NavBackStackEntry
对象里面的信息创建一个
ViewModelStore
实例,我们可以通过
NavHostContoller.currentBackStackEntry
!

获取这个对象

为了方便起见,我已经有了

CompositionLocal
NavHostContoller

val LocalNavController = compositionLocalOf<NavHostController> {
    throw IllegalStateException("NavController does not yet exist")
}
CompositionLocalProvider(
    LocalNavController provides rememberNavController()
) {
    RootComposable()
}

所以剩下要做的唯一一件事就是创建另一个函数,该函数获取为给定

ViewModel
创建的
NavBackStackEntry

/**
 * Returns an existing [ViewModel] for the current navigation destination
 * regardless of where in the composition you want it :)
 */
@Composable
internal inline fun <reified VM: ViewModel> screenViewModel() =
    LocalNavController.current.currentBackStackEntry!!.hiltViewModel<VM>()

因此,如果您的

LocalNavController
提供给您的应用栏和底部工作表,您只需执行以下操作并在导航图内部和外部获得相同的
ViewModel
实例。

@Composable
fun ProjectTopAppBar(
    viewModel: ProjectViewModel = screenViewModel()
) {
    // ...
}

更好的是,它不会是内存泄漏,因为它仍然附加到目的地。

© www.soinside.com 2019 - 2024. All rights reserved.