Android Academy · 课时

跨模块导航

连接各项功能而不产生耦合

第 4 / 4 课13 个步骤

跨模块导航 是 CoddyKit 上的免费 Android Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 4 节课。

导航挑战

当功能位于不同模块中时,一个新问题出现了::feature:home 如何在不依赖 :feature:profile 的情况下打开其中的屏幕?如果功能模块直接相互导入,就会产生耦合,并可能形成循环依赖。

在本课中,您将通过导航连接各个功能,同时保持它们彼此独立。

NavHost 位于何处

唯一的 NavHost 位于:app 模块中,这是唯一允许了解所有功能的地方。每个功能提供自己的目标位置,:app 再将它们组装到同一个依赖图中。

// app/AppNavHost.kt
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
    NavHost(navController, startDestination = HomeRoute) {
        homeScreen(onProfileClick = { navController.navigate(ProfileRoute) })
        profileScreen(onBack = { navController.popBackStack() })
    }
}

类型安全的路由

现代 Navigation Compose 支持类型安全的路由:路由是 @Serializable 对象或数据类,而不是特殊字符串。每个功能都在自己的模块中定义路由类型,因此由该功能负责自己的导航契约。

// feature/profile/ProfileRoute.kt
import kotlinx.serialization.Serializable

@Serializable
data class ProfileRoute(val userId: String)

功能模块公开 NavGraphBuilder 扩展

关键技巧是:每个功能公开一个NavGraphBuilder 扩展函数,用于注册其目标位置。功能模块负责自己的屏幕;:app 只需调用此函数。功能模块永远不会引用其他功能。

// feature/profile/ProfileNavigation.kt
fun NavGraphBuilder.profileScreen(onBack: () -> Unit) {
    composable<ProfileRoute> { backStackEntry ->
        val route: ProfileRoute = backStackEntry.toRoute()
        ProfileScreen(userId = route.userId, onBack = onBack)
    }
}

使用导航回调实现解耦

功能模块绝不能直接调用 navigate(SomeOtherFeatureRoute),因为这样就必须依赖另一个功能。相反,功能模块应公开类似 onProfileClick 的lambda 回调。由 :app 模块决定它们实际要前往哪里。

// feature/home/HomeNavigation.kt
fun NavGraphBuilder.homeScreen(onProfileClick: (String) -> Unit) {
    composable<HomeRoute> {
        HomeScreen(onUserClick = { userId -> onProfileClick(userId) })
    }
}
// :home does NOT know ProfileRoute exists.

:app 模块负责连接一切

只有 :app 模块了解所有路由,并将回调连接到实际导航。这是唯一可以接受耦合的地方——它的全部职责就是组装。

// app/AppNavHost.kt
NavHost(navController, startDestination = HomeRoute) {
    homeScreen(
        onProfileClick = { userId ->
            navController.navigate(ProfileRoute(userId)) // app knows both
        }
    )
    profileScreen(onBack = { navController.popBackStack() })
}

安全地传递参数

由于路由是 @Serializable 数据类,参数会在编译时进行类型检查。您可以在目标位置内部使用 toRoute() 读取参数。不再需要解析字符串,也不会再因运行时调用 arguments?.getString(...) 而崩溃。

// Reading arguments inside the destination
composable<ProfileRoute> { entry ->
    val args: ProfileRoute = entry.toRoute()
    ProfileScreen(userId = args.userId)
}

// Or directly in a ViewModel via SavedStateHandle
val route: ProfileRoute = savedStateHandle.toRoute()

通过 api 模块共享路由类型

有时,一个功能确实需要导航到另一个功能,并且需要使用其路由类型。与其依赖整个功能,不如在一个很小的:feature:profile:api模块中仅公开路由,其中只包含 @Serializable 路由。体积较大的 :impl 模块仍保持私有。

// feature/profile/api  -> only the route type
@Serializable
data class ProfileRoute(val userId: String)

// feature/home/build.gradle.kts
// home may depend on the lightweight api to build the route,
// but never on :feature:profile:impl
implementation(project(":feature:profile:api"))

每个功能使用嵌套导航

包含多个屏幕的功能可以使用 navigation<T> 公开整个嵌套图。功能模块负责自己的内部流程;:app 只需在一个入口点挂载该依赖图。

// feature/onboarding/OnboardingNavigation.kt
fun NavGraphBuilder.onboardingGraph(onFinished: () -> Unit) {
    navigation<OnboardingGraph>(startDestination = WelcomeRoute) {
        composable<WelcomeRoute> { WelcomeScreen() }
        composable<PermissionsRoute> { PermissionsScreen(onDone = onFinished) }
    }
}

跨模块深层链接

类型安全的路由同样支持深层链接。功能模块为自己的目标位置声明深层链接 URI 模式;:app NavHost 会将传入的链接解析到正确的功能屏幕,无需跨功能导入。

composable<ProfileRoute>(
    deepLinks = listOf(
        navDeepLink<ProfileRoute>(basePath = "https://myapp.com/profile")
    )
) { entry ->
    ProfileScreen(userId = entry.toRoute<ProfileRoute>().userId)
}

解耦模式总结

综合起来,规则很简单:

  • 每个功能负责自己的路由类型和 NavGraphBuilder 扩展。
  • 功能之间通过lambda 回调传达意图,而不是直接进行导航。
  • 只有 :app 了解所有功能,并将回调连接到实际路由。
  • 如果必须共享路由,就通过一个很小的 :api 模块公开它。

这样可以让功能彼此独立、能够利用构建缓存,并且不会产生循环依赖。

快速检查

在一个多模块应用中,:feature:home 需要将用户带到 :feature:profile 中的某个屏幕。怎样才能以最整洁的方式保持功能模块彼此解耦?

回顾:跨模块导航

您已经学会在不产生耦合的情况下连接各个功能:

  • 唯一的 NavHost 位于精简的:app模块中。
  • 每个功能负责一个 @Serializable 路由和一个 NavGraphBuilder 扩展。
  • 功能模块公开回调,而不是直接导航到其他功能。
  • 只有在确实需要时,才通过较小的 :api 模块共享路由;深层链接和嵌套图也遵循相同模式。

至此,多模块应用架构的学习就完成了:现在您可以拆分、连接并导航一个可扩展的 Android 代码库。

免费开始

用 AI 导师学习 Kotlin — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
36
课程
152

常见问题解答

「跨模块导航」课时是免费的吗?

是的 — 「跨模块导航」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 4 节课。

「跨模块导航」这节课中我会学到什么?

连接各项功能而不产生耦合 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Android Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「跨模块导航」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Android Academy 课中编写并运行代码吗?

能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 为什么要模块化
  2. 功能模块与核心模块
  3. 管理模块依赖
  4. 跨模块导航
← 返回 Android Academy