路由设计
- 采用常量或 sealed class 统一管理。
sealed class Destinations(val route: String) {
data object Home : Destinations("home")
data object Detail : Destinations("detail/{id}") {
fun createRoute(id: Long) = "detail/$id"
}
}
基础导航
@Composable
fun AppNavGraph(navController: NavHostController = rememberNavController()) {
NavHost(navController, startDestination = Destinations.Home.route) {
composable(Destinations.Home.route) {
HomeRoute(onOpenDetail = { id ->
navController.navigate(Destinations.Detail.createRoute(id))
})
}
composable(
Destinations.Detail.route,
arguments = listOf(navArgument("id") { type = NavType.LongType })
) { entry ->
val id = entry.arguments?.getLong("id") ?: return@composable
DetailRoute(id)
}
}
}
多返回栈与 BottomBar
val tabs = listOf("feed", "search", "profile")
val navController = rememberNavController()
NavigationBar {
tabs.forEach { tab ->
NavigationBarItem(
selected = navController.currentDestination?.route == tab,
onClick = {
navController.navigate(tab) {
popUpTo(navController.graph.startDestinationId) { saveState = true }
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(Icons.Default.Home, null) },
label = { Text(tab) }
)
}
}
深链与外部跳转
Manifest 配置
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="app" android:host="example" android:pathPrefix="/detail" />
</intent-filter>
</activity>
Compose 处理
@Composable
fun MainApp(navController: NavHostController = rememberNavController()) {
val intent = LocalContext.current.findActivity()?.intent
LaunchedEffect(intent) {
intent?.data?.lastPathSegment?.toLongOrNull()?.let { id ->
navController.navigate(Destinations.Detail.createRoute(id))
}
}
AppNavGraph(navController)
}
多模块导航
- 在
core/navigation定义NavigationHandler接口。 - Feature 模块通过
@Composable fun registerGraph(...)暴露路由。
interface NavigationDestination {
val route: String
fun register(navGraphBuilder: NavGraphBuilder, navController: NavController)
}
class ProfileDestination @Inject constructor(): NavigationDestination {
override val route = "profile"
override fun register(navGraphBuilder: NavGraphBuilder, navController: NavController) {
navGraphBuilder.composable(route) { ProfileRoute() }
}
}
内存与状态保存
- 使用
navController.currentBackStackEntry?.savedStateHandle在页面间传递结果。
navController.currentBackStackEntry?.savedStateHandle?.set("result", result)
navController.popBackStack()
val result = navController.currentBackStackEntry?.savedStateHandle?.getStateFlow("result", null)
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 返回栈错乱 | 未设置 launchSingleTop | 在 navigate 中配置 launchSingleTop |
| 参数解析失败 | 参数类型不匹配 | 使用 navArgument 指定类型 |
| 多模块冲突 | 路由名称重复 | 采用命名空间前缀或封装 Destination 类 |
实战案例:深链是”从外部一脚踹进 App 内页”
Deep Link 的本质一句话:让 https://myapp.com/product/123 这样的链接直接打开 App 的商品详情页,而不是首页。Android 侧分两步:
- Manifest 里声明:
intent-filter配android:scheme="https"+host="myapp.com",系统才知道这个域名归你管。Android 12+ 还要求用 App Links(assetlinks.json放到域名.well-known下做双向验证),否则只能弹选择框,不能直达。 - Compose Navigation 里接:
composable("product/{id}", deepLinks = listOf(navDeepLink { uriPattern = "https://myapp.com/product/{id}" })),参数自动进backStackEntry.arguments。
我在 Flutter 里做过对等的事(app_links 包 + go_router 的 deep link),最大的坑是冷启动:App 没在后台时,deeplink 进来要等引擎初始化,页面会先闪一下首页再跳详情——和 Flutter 里路由重定向的时序问题一模一样。解法也一样:启动页(splash)先 hold 住,等路由解析完再放行。
另外提醒:国内 Android 生态的 deeplink 被各厂商 ROM 魔改得很厉害,测试时至少覆盖小米/华为/OV,别只在 Pixel 模拟器上自嗨。
参考:Android 官方文档《Navigation deep links》;《Verify Android App Links》
总结
- Compose Navigation 提供声明式路由配置,配合 sealed class 管理更清晰。
- 深链需同时配置 Manifest 与导航逻辑,确保参数安全解析。
- 在多模块项目中抽象导航接口,避免直接引用具体实现,使路由体系可扩展、可维护。
