You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多模块项目中带类型安全导航的Shared ViewModel实现方案咨询

多模块项目中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:33:09