技术目标
- 会用
rememberNavController()+NavHost注册路由。 - 会用
navArgument+NavType声明路径参数(与可选 query 方案区分)。 - 理解
NavBackStackEntry:在composable { ... }lambda 中读取arguments、SavedStateHandle,以及与ViewModelStore的对应关系。
1. 本仓库最小图
NavController
NavHost
home
samples
sample/profile/{userId}
HomeRoute
SamplesHubScreen
ProfileNavSampleScreen
AppNavHost.kt 中 NavHost 以 Routes.HOME 为 startDestination,各 composable(Routes.*) 注册样例屏。
kotlin
package com.kuen.composedemo.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.kuen.composedemo.home.AboutRoute
import com.kuen.composedemo.home.HomeRoute
import com.kuen.composedemo.samples.SamplesHubScreen
import com.kuen.composedemo.samples.design.DesignSystemSampleScreen
import com.kuen.composedemo.samples.lazy.LazyListSampleScreen
import com.kuen.composedemo.samples.modifier.ModifierLabScreen
import com.kuen.composedemo.samples.animation.AnimationLabScreen
import com.kuen.composedemo.samples.controls.ControlsLabScreen
import com.kuen.composedemo.samples.focus.FocusInsetsLabScreen
import com.kuen.composedemo.samples.layout.CustomLayoutLabScreen
import com.kuen.composedemo.samples.stability.StabilityLabScreen
import com.kuen.composedemo.samples.profile.ProfileNavSampleScreen
import com.kuen.composedemo.samples.sideeffect.SideEffectSampleScreen
import com.kuen.composedemo.samples.state.StateSampleScreen
@Composable
fun AppNavHost(navController: NavHostController) {
NavHost(navController = navController, startDestination = Routes.HOME) {
composable(Routes.HOME) {
HomeRoute(
onOpenAbout = { navController.navigate(Routes.ABOUT) },
onOpenSamples = { navController.navigate(Routes.SAMPLES_HUB) },
)
}
composable(Routes.ABOUT) {
AboutRoute(onBack = { navController.popBackStack() })
}
composable(Routes.SAMPLES_HUB) {
SamplesHubScreen(
onBack = { navController.popBackStack() },
onNavigate = { route -> navController.navigate(route) },
)
}
composable(Routes.DESIGN_SAMPLE) {
DesignSystemSampleScreen(onBack = { navController.popBackStack() })
}
composable(Routes.STATE_SAMPLE) {
StateSampleScreen(onBack = { navController.popBackStack() })
}
composable(
route = Routes.PROFILE_WITH_ARGS,
arguments = listOf(
navArgument(Routes.ARG_USER_ID) { type = NavType.StringType },
),
) { entry ->
val userId = entry.arguments?.getString(Routes.ARG_USER_ID).orEmpty()
ProfileNavSampleScreen(
userId = userId,
onBack = { navController.popBackStack() },
)
}
composable(Routes.LAZY_SAMPLE) {
LazyListSampleScreen(onBack = { navController.popBackStack() })
}
composable(Routes.SIDE_EFFECT_SAMPLE) {
SideEffectSampleScreen(onBack = { navController.popBackStack() })
}
composable(Routes.MODIFIER_SAMPLE) {
ModifierLabScreen(onBack = { navController.popBackStack() })
}
composable(Routes.STABILITY_SAMPLE) {
StabilityLabScreen(onBack = { navController.popBackStack() })
}
composable(Routes.CONTROLS_SAMPLE) {
ControlsLabScreen(onBack = { navController.popBackStack() })
}
composable(Routes.CUSTOM_LAYOUT_SAMPLE) {
CustomLayoutLabScreen(onBack = { navController.popBackStack() })
}
composable(Routes.ANIMATION_SAMPLE) {
AnimationLabScreen(onBack = { navController.popBackStack() })
}
composable(Routes.FOCUS_SAMPLE) {
FocusInsetsLabScreen(onBack = { navController.popBackStack() })
}
}
}
2. 路径模板与参数
最小模板(概念):
kotlin
NavHost(navController, startDestination = "home") {
composable("home") { Home() }
composable("detail/{id}", arguments = listOf(navArgument("id") { type = NavType.IntType })) { entry ->
val id = entry.arguments?.getInt("id") ?: return@composable
Detail(id)
}
}
本仓库 带参路由 定义在 Routes.kt:
PROFILE_WITH_ARGS = "sample/profile/{$ARG_USER_ID}",ARG_USER_ID = "userId"。- 工厂方法
profileRoute(userId: String):拼接"sample/profile/$userId",避免各处手写字符串。
AppNavHost.kt 中对应注册:
kotlin
composable(
route = Routes.PROFILE_WITH_ARGS,
arguments = listOf(navArgument(Routes.ARG_USER_ID) { type = NavType.StringType }),
) { entry ->
val userId = entry.arguments?.getString(Routes.ARG_USER_ID).orEmpty()
ProfileNavSampleScreen(userId = userId, ...)
}
ProfileNavSampleScreen.kt 展示 userId;SamplesHubScreen.kt 通过 onNavigate(Routes.profileRoute("demo_user")) 跳入,便于本地手测。

3. navigate 常用选项(技术)
navigate("route") { launchSingleTop = true }:栈顶已存在同 destination 时避免 多实例。popUpTo("a") { inclusive = true }:弹出到a并决定是否包含a------极易配错清空栈 ;改完必须手测 系统返回键 与 业务返回。popUpTo+saveState/restoreState:底部多 Tab 场景常见;与rememberSaveable状态不要混成两套恢复语义。
4. 与 Compose 状态的关系
NavController.currentBackStackEntryAsState():把返回栈暴露为State,可驱动底部导航高亮、标题等。- 不要把大对象 Serializable 进路由 :URL 长度与序列化成本都不适合;传 id ,详情页再拉取 / 用 共享 ViewModel scope(按官方推荐模式)传只读缓存。
- 进程死亡恢复 :路径参数会进
SavedStateHandle;内存单例缓存 不可当作唯一数据源,否则恢复后空窗。
5. 与 viewModel() 作用域(衔接 01 篇)
- 同一
composable(route) { }内viewModel()默认绑定当前NavBackStackEntry为 owner(除非另行指定)。 - 跨 destination 需要共享数据时,显式使用
navigation(start = ...) { ... }嵌套图 或ViewModel的 scoped 获取 API(以当前 Navigation + lifecycle 版本文档为准),避免「在错误 graph 取到错误实例」。
6. 风险清单
- 路径参数含
/、?、#→ 必须 URL 编码 或改用 query 参数 方案。 - 多
NavHost+ 多 Tab:deeplink 与 返回键 易进错栈;需统一「单一返回栈真相」的产品语义。 - deeplink 与
navArgument默认值 :未声明 optional 时,非法 deep link 可能导致 启动即空屏或 crash,要在设计阶段列矩阵测试。
7. 自检清单
- 所有 route 字符串是否集中在
Routes(或等价常量模块),避免魔法字符串? - 带参路由是否 同时 具备:
navArgument声明、profileRoute式拼接、目标屏 空参兜底? navigate的popUpTo是否在真机上走过 A→B→C→返回 全路径?- 是否需要
launchSingleTop防止同屏多实例?
参考答案(复习用)
- 是 。本仓库用
Routes.kt集中常量 +profileRoute();跳转处只传Routes.*或工厂返回值,避免"sample/profile/" + id散落。 - 是才算合格 。
NavHost里navArgument与route占位符一致;对外用Routes.profileRoute(userId)生成路径;屏内用entry.arguments?.getString(...).orEmpty()等兜底,避免null或未注册参数导致闪退。 - 改
popUpTo/inclusive后必须手测 。期望:系统返回与 App 内「返回」是否一致、是否会意外清空栈或跳过某屏;多 Tab 时还要测 deeplink 进栈。 - 若同一 destination 可能被反复
navigate且不应叠多份 (如单例详情、首页),需要launchSingleTop = true;若业务就是要多实例栈(少见),则不加。
源码仓库 :ComposeDemo(分支
main)