多模块项目中带类型安全导航的Shared ViewModel实现方案咨询
问题背景
正如标题所示,我正在探寻多模块项目中实现Shared ViewModel的最优方案。
以下是我当前采用的实现方式:
App模块中的NavHost定义
package the_app_module.navigation @Composable fun NavGraph(navController: NavHostController) { // Shared ViewModel val sharedTemplateQuestViewModel: TemplateQuestViewModel = hiltViewModel() NavHost( navController = navController, startDestination = OnBoardingFlowRoutes.LoginRoute, ) { onboardingFlowScreens(navController = navController) // Teacher Screens teacherClassroomScreens(navController = navController) myQuestsScreen(navController = navController) teacherHomeScreen() teacherProfileScreen() templateQuestScreens( navController = navController, sharedTemplateQuestViewModel = sharedTemplateQuestViewModel, ) // Student Screens studentClassroomScreen() studentHomeScreen() studentProfileScreen() } } fun NavDestination?.isRouteInHierarchy(route: KClass<*>): Boolean = this?.hierarchy?.any { it.hasRoute(route) } ?: false
每个函数(如teacherClassroomScreens、templateQuestScreens等)都定义在各自的模块中,包含该模块专属的页面。
templateQuestScreens模块实现
package my_quests.navigation fun NavController.navigateToCreateTemplateQuest(navOptions: NavOptions? = null) { navigate(TemplateQuestRoutes.CreateTemplateQuestRoute, navOptions) } fun NavController.navigateToTemplateQuestions(navOptions: NavOptions? = null) { navigate(TemplateQuestRoutes.TemplateQuestionsRoute, navOptions) } fun NavGraphBuilder.templateQuestScreens( navController: NavController, sharedTemplateQuestViewModel: TemplateQuestViewModel, ) { createTemplateQuestScreen( onBackAction = navController::popBackStack, onNavigateToQuestions = navController::navigateToTemplateQuestions, sharedTemplateQuestViewModel = sharedTemplateQuestViewModel, ) templateQuestionsScreen( onBackAction = navController::popBackStack, sharedTemplateQuestViewModel = sharedTemplateQuestViewModel, ) } private fun NavGraphBuilder.createTemplateQuestScreen( onBackAction: () -> Unit, onNavigateToQuestions: () -> Unit, sharedTemplateQuestViewModel: TemplateQuestViewModel, ) { composable<TemplateQuestRoutes.CreateTemplateQuestRoute> { CreateTemplateQuestRoute( viewModel = sharedTemplateQuestViewModel, onBackAction = onBackAction, onNavigateToQuestions = onNavigateToQuestions, ) } } private fun NavGraphBuilder.templateQuestionsScreen( onBackAction: () -> Unit, sharedTemplateQuestViewModel: TemplateQuestViewModel, ) { composable<TemplateQuestRoutes.TemplateQuestionsRoute> { TemplateQuestionsRoute( viewModel = sharedTemplateQuestViewModel, onBackAction = onBackAction, ) } }
TemplateQuestRoutes定义
package my_quests.navigation sealed interface TemplateQuestRoutes { @Serializable data object CreateTemplateQuestRoute : TemplateQuestRoutes @Serializable data object TemplateQuestionsRoute : TemplateQuestRoutes }
页面可组合函数实现
@Composable fun CreateTemplateQuestRoute( viewModel: TemplateQuestViewModel, onNavigateToQuestions: () -> Unit, onBackAction: () -> Unit, ) { CreateTemplateQuestContainer( viewModel = viewModel, onNavigateToQuestions = onNavigateToQuestions, onBackAction = onBackAction, ) } @Composable fun CreateTemplateQuestContainer( viewModel: TemplateQuestViewModel, onNavigateToQuestions: () -> Unit, onBackAction: () -> Unit, ) { // Content here }
当前实现的问题
这种实现方式存在核心问题:ViewModel的生命周期永远不会结束。由于在根NavGraph中通过Dagger Hilt实例化ViewModel,它会绑定到整个应用的生命周期,持续存在直至应用进程销毁。若不进行显式作用域限定或及时清理,极易引发内存泄漏或数据过期问题。
解决方案与最佳实践
1. 用嵌套导航图限定ViewModel作用域
Jetpack Compose Navigation支持为嵌套导航图创建专属作用域的ViewModel,其生命周期会绑定到该嵌套图的存在周期——当用户离开整个嵌套图(嵌套图从返回栈中移除)时,ViewModel会自动触发onCleared(),完成生命周期收尾。
修改步骤:
- 将
templateQuestScreens封装为独立的嵌套导航图,而非直接在根NavHost中添加单个页面。 - 在嵌套图的作用域内初始化ViewModel,而非在根NavGraph中。
修改后的templateQuestScreens实现:
fun NavGraphBuilder.templateQuestScreens(navController: NavController) { // 嵌套导航图,绑定专属作用域 navigation( startDestination = TemplateQuestRoutes.CreateTemplateQuestRoute, route = "template_quest_graph" ) { // 在嵌套图内部初始化ViewModel,作用域绑定到当前嵌套图 val viewModel: TemplateQuestViewModel = hiltViewModel() createTemplateQuestScreen( onBackAction = navController::popBackStack, onNavigateToQuestions = navController::navigateToTemplateQuestions, sharedTemplateQuestViewModel = viewModel, ) templateQuestionsScreen( onBackAction = navController::popBackStack, sharedTemplateQuestViewModel = viewModel, ) } }
根NavGraph中只需调用templateQuestScreens(navController),无需传递ViewModel参数。
2. 结合Hilt注解强化作用域
确保TemplateQuestViewModel使用@HiltViewModel注解,Hilt会自动将嵌套图内初始化的ViewModel作用域限定到该导航图:
@HiltViewModel class TemplateQuestViewModel @Inject constructor( // 依赖注入项 ) : ViewModel() { // ViewModel业务逻辑 }
3. 类型安全导航下的嵌套图适配
为保持类型安全,可在TemplateQuestRoutes中添加嵌套图的路由标记:
sealed interface TemplateQuestRoutes { @Serializable data object TemplateQuestGraph : TemplateQuestRoutes // 嵌套图路由 @Serializable data object CreateTemplateQuestRoute : TemplateQuestRoutes @Serializable data object TemplateQuestionsRoute : TemplateQuestRoutes }
随后在嵌套导航图中使用route = TemplateQuestRoutes.TemplateQuestGraph,维持类型安全特性。
4. 降低模块间耦合
当前实现中App模块直接依赖TemplateQuestViewModel所在模块,增加了耦合度。优化方案:
- 在
templateQuest模块内部完成ViewModel的初始化与作用域限定。 - App模块仅需调用
templateQuestScreens(navController),无需感知ViewModel的存在。
5. 手动清理资源(可选)
若需在ViewModel生命周期结束前手动清理资源,可重写onCleared()方法:
@HiltViewModel class TemplateQuestViewModel @Inject constructor() : ViewModel() { override fun onCleared() { super.onCleared() // 清理协程、取消订阅、释放资源等操作 } }
内容的提问来源于stack exchange,提问作者leonel Z

