导航基础:NavHost、composable 路由、参数传递
导航,说白了就是页面之间的跳转。在传统的 Android 开发中,我们用 Intent、FragmentTransaction 那一套,写起来又臭又长。到了 Compose 时代,导航变得声明式了——你只需要告诉系统「我要去哪里」,剩下的交给框架。
我个人习惯把导航比作「地图 + 路标」。NavHost 就是地图,composable 路由就是路标,参数传递就是你要带上的行李。今天我们就来把这套东西讲透。
1. 导航三件套:NavHost、NavController、composable
先看一个最简单的例子。假设我们有两个页面:首页和详情页。
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToDetail = { id ->
navController.navigate("detail/$id")
}
)
}
composable(
route = "detail/{itemId}",
arguments = listOf(
navArgument("itemId") { type = NavType.StringType }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId")
DetailScreen(itemId = itemId)
}
}
}
嗯,这里要注意几个点:
- rememberNavController():创建导航控制器,它管理着整个导航栈。记得放在顶层 Composable 里,别放在某个子组件里,否则跳转时会丢失状态。
- NavHost:相当于一个容器,里面放着你所有的路由定义。startDestination 是默认首页。
- composable("路由路径"):定义每个页面的路由。路径可以带参数,用花括号包裹。
2. 参数传递:路径参数 vs 查询参数
参数传递有两种方式,我在项目中都用过,各有适用场景。
2.1 路径参数
路径参数是写在 URL 路径里的,比如 detail/abc123。适合传递「必须要有」的参数,比如 ID。
// 定义
composable(
route = "detail/{itemId}",
arguments = listOf(
navArgument("itemId") { type = NavType.StringType }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId") ?: ""
DetailScreen(itemId)
}
// 跳转
navController.navigate("detail/abc123")
这里有个坑:路径参数默认是 String 类型,如果你要传 Int,记得显式指定 NavType.IntType。我曾经因为忘记指定类型,导致参数解析出来是 null,排查了半天。
2.2 查询参数
查询参数是跟在问号后面的,比如 detail?itemId=abc123&source=list。适合传递「可选」或「额外」的信息。
// 定义
composable(
route = "detail?itemId={itemId}&source={source}",
arguments = listOf(
navArgument("itemId") { type = NavType.StringType; defaultValue = "" },
navArgument("source") { type = NavType.StringType; defaultValue = "unknown" }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId") ?: ""
val source = backStackEntry.arguments?.getString("source") ?: "unknown"
DetailScreen(itemId, source)
}
// 跳转
navController.navigate("detail?itemId=abc123&source=list")
3. 导航栈管理:返回、清除、单例
导航不只是跳转,还得管怎么回来。我见过不少新手把返回逻辑写崩了,页面栈乱成一锅粥。
3.1 标准返回
用 navController.popBackStack() 即可。默认返回上一页,如果栈里没有上一页了,就退出应用。
3.2 清除栈顶
有时候跳转到登录页后,不希望用户按返回键回到首页。这时候用 launchSingleTop = true 配合 popUpTo。
navController.navigate("login") {
popUpTo("home") { inclusive = true }
}
这段代码的意思是:跳转到 login 页面,同时把 home 页面从栈里移除。这样用户登录后按返回键,直接退出应用,不会回到首页。
3.3 单例模式
如果你不想重复创建同一个页面(比如首页),用 launchSingleTop = true。
navController.navigate("home") {
launchSingleTop = true
}
这样如果首页已经在栈顶,就不会再创建一个新的。
4. 实战:一个完整的导航示例
我们做一个简单的商品列表 → 商品详情 → 商品编辑的流程。看看参数传递和栈管理怎么配合。
// 路由定义
object Routes {
const val PRODUCT_LIST = "product_list"
const val PRODUCT_DETAIL = "product_detail/{productId}"
const val PRODUCT_EDIT = "product_edit?productId={productId}"
}
// NavHost 配置
NavHost(
navController = navController,
startDestination = Routes.PRODUCT_LIST
) {
composable(Routes.PRODUCT_LIST) {
ProductListScreen(
onProductClick = { productId ->
navController.navigate("product_detail/$productId")
}
)
}
composable(
route = Routes.PRODUCT_DETAIL,
arguments = listOf(
navArgument("productId") { type = NavType.StringType }
)
) { backStackEntry ->
val productId = backStackEntry.arguments?.getString("productId") ?: ""
ProductDetailScreen(
productId = productId,
onEditClick = {
navController.navigate("product_edit?productId=$productId")
},
onBackClick = {
navController.popBackStack()
}
)
}
composable(
route = Routes.PRODUCT_EDIT,
arguments = listOf(
navArgument("productId") { type = NavType.StringType; defaultValue = "" }
)
) { backStackEntry ->
val productId = backStackEntry.arguments?.getString("productId") ?: ""
ProductEditScreen(
productId = productId,
onSaveSuccess = {
// 保存成功后,返回详情页并刷新
navController.popBackStack()
}
)
}
}
你想想看,这个流程里参数传递是不是很清晰?路径参数传必填的 productId,查询参数传可选的编辑模式。返回时用 popBackStack,不会搞乱栈。
5. 避坑指南
最后分享几个我踩过的坑:
- 参数类型不匹配:路径参数默认 String,传 Int 时要显式指定 NavType.IntType。否则解析出来是 null。
- 忘记设置 defaultValue:查询参数必须设置 defaultValue,否则编译不报错,运行时崩溃。
- 导航栈溢出:如果用户频繁跳转,栈会越来越大。建议在不需要返回的页面用 popUpTo 清除。
- rememberNavController 位置不对:一定要放在最顶层的 Composable 里,否则子组件重新组合时,导航状态会丢失。
- NavHost 是地图,composable 是路标,NavController 是方向盘。
- 路径参数传必填数据,查询参数传可选数据。
- 用 popUpTo 和 launchSingleTop 管理栈,别让栈无限膨胀。
- 参数类型和 defaultValue 一定要写对,否则运行时等着崩溃吧。
导航这块其实不难,但细节很多。你只要把「地图、路标、行李」这三个概念理清楚,写起来就很顺手。我在项目中用这套模式写了十几个页面,从来没出过问题。
这张图把导航的核心结构画出来了。NavHost 是容器,里面放多个 composable 路由。参数传递分路径参数和查询参数两种,NavController 负责跳转和返回。你只要记住这个结构,写导航代码就不会乱。