我的 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 结构的情况下解决这个问题(以及首先处理导致它的其他问题)?它甚至需要修理吗?如果修复了与状态相关的错误,单例视图模型是否可以?
在将近半年没有人回答这个问题后,这个问题在我的项目中变得很关键,所以我开始寻找解决方案。经过几天的谷歌搜索,我几乎准备好通过反射手动添加和删除视图模型到
Activity
的ViewModelStore
中或从中删除视图模型,但幸运的是我找到了一个单线解决方案。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()
) {
// ...
}
更好的是,它不会是内存泄漏,因为它仍然附加到目的地。