21、Compose与Navigation:Compose Navigation、NavHost、composable、参数传递、实战:Compose页面导航。

各位同学,今天我们来聊聊 Compose 里的导航。

说实话,Jetpack Navigation 在 View 体系里我就一直在用。到了 Compose 时代,它变得更简洁了。你想想看,以前写一个 Fragment 跳转,要配 XML、写 NavGraph、还要处理 ViewModel 作用域。现在?一个 NavHost 加几个 composable 函数就搞定了。

我个人习惯把导航看作「页面的路由表」。你定义好每个页面的路径,然后通过 navController.navigate() 跳转。就这么简单。

核心组件:NavHost 与 composable

先看最基础的结构。你需要一个 NavHost 作为容器,里面用 composable 注册每个页面。

@Composable
fun AppNavigation() {
    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.IntType }
            )
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
            DetailScreen(itemId = itemId)
        }
    }
}

这里有个细节我提一下。startDestination 是应用启动时显示的页面。我在项目中遇到过有人把 startDestination 写成了不存在的路由,结果应用直接白屏。嗯,这个坑踩过一次就记住了。

关键点:每个 composable 路由必须唯一。参数用花括号包裹,比如 detail/{itemId}

参数传递:两种方式

参数传递是导航的核心能力。Compose Navigation 支持两种方式:路径参数和查询参数。

类型 写法 适用场景
路径参数 detail/{itemId} 必须传递的参数,如 ID
查询参数 search?query={keyword} 可选参数,如搜索关键词

路径参数是强制的。如果你定义了一个 {itemId},跳转时必须传值。查询参数是可选的,你可以给默认值。

// 路径参数跳转
navController.navigate("detail/42")

// 查询参数跳转
navController.navigate("search?query=android")

我曾经在项目里犯过一个错:路径参数用了 String 类型,但实际传的是数字。结果页面一直显示异常。后来才发现 NavType 要匹配。你想想看,类型不匹配,解析出来的值就是 null,能不崩吗?

小技巧:参数类型尽量用 Int 或 String。复杂对象建议用 ViewModel 共享,不要序列化到路由里。

实战:完整的页面导航

我们做一个简单的例子:首页列表点击跳转到详情页,详情页可以返回。

// 首页
@Composable
fun HomeScreen(onNavigateToDetail: (Int) -> Unit) {
    Column {
        Text("商品列表", style = MaterialTheme.typography.headlineMedium)
        LazyColumn {
            items(10) { index ->
                ListItem(
                    headlineContent = { Text("商品 $index") },
                    modifier = Modifier.clickable {
                        onNavigateToDetail(index)
                    }
                )
            }
        }
    }
}

// 详情页
@Composable
fun DetailScreen(itemId: Int, onBack: () -> Unit) {
    Column {
        IconButton(onClick = onBack) {
            Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
        }
        Text("商品详情 ID: $itemId", style = MaterialTheme.typography.headlineMedium)
    }
}

导航的返回操作,我习惯用 navController.popBackStack()。但要注意:如果当前页面是起始页,调用 popBackStack() 会直接退出应用。所以最好加个判断。

if (navController.previousBackStackEntry != null) {
    navController.popBackStack()
} else {
    // 退出应用或显示确认弹窗
}
避坑指南:我曾经在深层嵌套导航时,连续调用了两次 popBackStack(),结果跳过了目标页面。后来我改用 navController.navigate(route) { popUpTo("home") } 来清理回退栈。

知识体系总览

下面这张图概括了 Compose Navigation 的核心结构。你看,从 NavController 出发,经过 NavHost,路由到各个 composable 页面。参数传递贯穿其中。

Compose Navigation 核心结构 NavController NavHost HomeScreen route: "home" DetailScreen route: "detail/{id}" SearchScreen route: "search?q={q}" 参数传递:路径参数 / 查询参数 / NavArgument 类型定义

总结

Compose Navigation 的核心就三件事:定义路由、传递参数、处理返回。你只要把 NavHost 搭好,剩下的就是拼积木一样组合页面。

我个人建议:路由字符串统一放在一个常量文件里管理。别到处写魔法字符串,后期维护起来真的很痛苦。

好了,这一节的内容就到这里。代码不多,但都是实战中会反复用到的模式。你可以在自己的项目里试试看,有问题随时交流。


公众号:蓝海资料掘金营,微信deep3321