导航与路由:Navigation Compose 入门

导航,说白了就是页面之间的跳转逻辑。在传统 Android 开发中,我们用 FragmentTransaction 或者 Intent 来切页面。但在 Jetpack Compose 里,一切都变了——没有 Fragment,没有 Activity 栈,取而代之的是 Navigation Compose

我第一次接触 Navigation Compose 时,心里其实有点抵触。毕竟 Fragment 那套导航我用了好多年,突然换成声明式的方式,总觉得不踏实。但用了一个项目之后,我彻底改观了。嗯,真香。

核心三要素:NavHost、NavController、NavBackStackEntry。记住这三个,导航就通了。

NavController:导航的总指挥

NavController 是导航的核心对象。它管理着一个「返回栈」,记录了用户访问过的所有页面。你可以把它想象成一个遥控器——按一下,跳转;按返回键,回退。

获取 NavController 的方式很简单:

val navController = rememberNavController()

注意这个 rememberNavController()。它是个 Composable 函数,会记住 NavController 实例。如果你的 Activity 重建了,它也不会丢失状态。我在项目中遇到过一个问题:忘记用 remember,结果每次重组都新建一个 NavController,导航直接崩了。后来排查了半天,才发现是这行代码的问题。

NavHost:导航的容器

NavHost 是页面的容器。它接收 NavController 和一个起始路由,然后根据路由来展示对应的 Composable 页面。

NavHost(
    navController = navController,
    startDestination = "home"
) {
    composable("home") { HomeScreen(navController) }
    composable("profile") { ProfileScreen(navController) }
    composable("settings") { SettingsScreen(navController) }
}

每个 composable("路由") 定义了一个页面。路由就是字符串,你可以随意命名。我个人习惯用下划线命名法,比如 user_detailorder_list,这样看起来清晰。

小技巧:路由字符串最好定义成常量,避免拼写错误。我一般会写一个 Routes 对象来统一管理。

页面跳转:navController.navigate()

跳转页面只需要调用 navController.navigate("路由")。比如从首页跳到个人中心:

Button(onClick = {
    navController.navigate("profile")
}) {
    Text("去个人中心")
}

这里有个细节:默认情况下,跳转后会把新页面压入返回栈。如果你不想让用户返回到上一个页面(比如登录成功后),可以加上 launchSingleTop = true 或者清除返回栈:

navController.navigate("home") {
    popUpTo("login") { inclusive = true }
}

这段代码的意思是:跳转到 home,同时把 login 页面从栈里移除。用户按返回键就不会回到登录页了。我曾经在项目里没加这个,结果用户登录后按返回键又回到登录页,体验极差。后来加上了 popUpTo,问题解决。

参数传递:导航时带数据

页面之间传参,是导航的刚需。Navigation Compose 支持两种方式:路径参数和查询参数。

路径参数

路径参数就是把参数写在路由里,像 RESTful API 那样:

// 定义路由
composable(
    route = "user/{userId}",
    arguments = listOf(
        navArgument("userId") { type = NavType.StringType }
    )
) { backStackEntry ->
    val userId = backStackEntry.arguments?.getString("userId")
    UserDetailScreen(userId = userId)
}

// 跳转时传参
navController.navigate("user/12345")

路径参数适合必传的参数,比如用户 ID、订单号。如果参数是可选的,可以用查询参数。

查询参数

// 定义路由
composable(
    route = "search?query={query}",
    arguments = listOf(
        navArgument("query") { 
            type = NavType.StringType
            defaultValue = ""
        }
    )
) { backStackEntry ->
    val query = backStackEntry.arguments?.getString("query")
    SearchScreen(query = query)
}

// 跳转时传参
navController.navigate("search?query=Android")

查询参数的好处是可选,不传就用默认值。我个人更推荐路径参数,因为路由更清晰,不容易出错。

注意:参数类型要匹配。如果你声明了 NavType.IntType,传了字符串会直接崩溃。我建议所有参数都用 String 类型,然后在 Composable 里自己转换,这样更安全。

深层链接:从外部打开指定页面

深层链接(Deep Link)允许你从通知、浏览器或其他 App 直接打开 App 的某个页面。Navigation Compose 对深层链接的支持非常优雅。

composable(
    route = "article/{articleId}",
    deepLinks = listOf(
        navDeepLink { uriPattern = "myapp://article/{articleId}" }
    )
) { backStackEntry ->
    val articleId = backStackEntry.arguments?.getString("articleId")
    ArticleDetailScreen(articleId = articleId)
}

配置好之后,用户在浏览器点击 myapp://article/888,就会直接打开你的文章详情页。注意,你还需要在 AndroidManifest.xml 中声明 intent-filter:

<activity android:name=".MainActivity">
    <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="myapp" />
    </intent-filter>
</activity>

深层链接在项目里用得不多,但一旦需要,它就是救命稻草。我记得有一次产品经理要求从推送通知直接跳转到订单详情页,用深层链接几分钟就搞定了。

知识体系图

下面这张图总结了 Navigation Compose 的核心逻辑:

Navigation Compose 核心架构 NavController 管理返回栈 NavHost 页面容器 路由表 composable("路由") 控制 匹配 参数传递 路径参数: user/{id} 查询参数: search?q=xxx 深层链接 外部跳转: myapp://article/{id} 需配置 intent-filter 返回栈 popUpTo launchSingleTop 一句话总结 NavController 控制导航,NavHost 渲染页面,路由表定义页面与参数的映射关系。

常见问题与避坑指南

  • 页面重复创建:多次调用 navigate("home") 会创建多个 HomeScreen 实例。解决方案是使用 launchSingleTop = true
  • 参数丢失:Activity 被系统回收后,NavController 的状态会恢复,但参数不会。建议用 SavedStateHandle 来保存关键参数。
  • 深层链接不生效:检查 AndroidManifest.xml 中的 intent-filter 是否配置正确。我曾经漏写了 BROWSABLE 类别,折腾了一下午。
  • 返回栈混乱:跳转时忘记 popUpTo,导致用户按返回键回到不该回的页面。建议在登录、支付等关键流程中,显式管理返回栈。

我的个人习惯:每个页面的路由都定义成常量,放在一个 NavRoutes.kt 文件里。参数 key 也定义成常量。这样团队协作时,大家不会因为拼写错误而吵架。

Navigation Compose 其实不复杂。你只要记住:NavController 是遥控器,NavHost 是电视屏幕,路由表是频道列表。剩下的就是多写多练,踩过几个坑之后,自然就熟练了。

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