11、导航与路由:Navigation Compose入门、NavHost与NavController、参数传递、深层链接

导航,说白了就是页面之间的跳转逻辑。在传统的Android开发中,我们用Intent、FragmentTransaction来管理页面切换,代码写多了容易乱成一锅粥。Jetpack Compose带来了全新的导航方案——Navigation Compose,它用声明式的方式解决了路由问题。

我个人习惯把导航理解为「一张地图」。你站在哪个页面,想去哪个页面,路上要不要带点参数过去,这些都由Navigation Compose帮你规划好。今天我们就来把这套东西彻底搞明白。

核心要点:Navigation Compose的核心是NavHost + NavController的组合。NavController负责导航逻辑,NavHost负责页面容器的渲染。

11.1 Navigation Compose入门

先说说怎么引入。在build.gradle.kts里加上依赖:

dependencies {
    implementation("androidx.navigation:navigation-compose:2.7.7")
}

嗯,版本号建议用最新的稳定版。我上个月刚踩过一个坑——用了2.6.x的某个旧版本,结果在参数传递时遇到了类型转换异常,升级到2.7.7就解决了。

Navigation Compose的核心思想很简单:你定义一组「路由」(route),每个路由对应一个可组合函数。NavHost根据当前路由,渲染对应的页面。

举个例子:

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

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

这里rememberNavController()创建了一个NavController实例,它贯穿整个导航生命周期。NavHost则像一个容器,根据路由字符串决定显示哪个页面。

小技巧:startDestination是应用启动时默认显示的页面。我建议把首页的路由定义成一个常量,比如const val ROUTE_HOME = "home",避免拼写错误。

11.2 NavHost与NavController

NavController是导航的「大脑」。它维护了一个返回栈(back stack),记录了用户访问过的页面历史。你可以调用navController.navigate("route")跳转到新页面,调用navController.popBackStack()返回上一页。

NavHost则是「显示器」。它监听NavController的状态变化,当路由改变时,自动切换到对应的可组合函数。

这里有个容易混淆的点:NavController本身不负责UI渲染,它只管理状态。真正渲染页面的是NavHost里的composable块。

我曾经在项目里犯过一个低级错误——在多个Composable里各自创建了不同的NavController实例,结果导航完全失效。记住:整个导航图应该共享同一个NavController实例。

看一个更完整的例子:

@Composable
fun MyApp() {
    val navController = rememberNavController()

    Scaffold(
        bottomBar = {
            BottomNavigationBar(navController)
        }
    ) { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(paddingValues)
        ) {
            composable("home") { HomeScreen(navController) }
            composable("search") { SearchScreen(navController) }
            composable("settings") { SettingsScreen(navController) }
        }
    }
}

底部导航栏的点击事件里,调用navController.navigate()并配合navController.popUpTo()来管理返回栈,避免页面无限叠加。

注意:使用底部导航时,如果用户已经在「首页」又点击了「首页」按钮,不要重复压栈。建议用navController.navigate("home") { popUpTo("home") { inclusive = true } }来处理。

11.3 参数传递

页面之间传参,这是导航里最常用的功能。Navigation Compose支持两种方式:路径参数和查询参数。

路径参数:参数直接嵌入在路由字符串里。

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

// 跳转
navController.navigate("detail/12345")

查询参数:参数跟在问号后面,类似URL的query string。

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

// 跳转
navController.navigate("search?query=Kotlin")

我个人更推荐路径参数,因为它在返回栈里更清晰,调试时一眼就能看出当前页面是哪个item。查询参数适合可选参数或搜索场景。

传递复杂对象怎么办?我建议把对象序列化成JSON字符串,作为路径参数传递。或者用共享ViewModel,在页面间共享数据。直接传Parcelable对象虽然可行,但容易导致导航图臃肿。

避坑指南:我曾经在参数里传了一个很大的列表对象,结果导航动画卡顿了好几秒。后来改成传ID,页面内部再根据ID从本地数据库查询数据,流畅多了。

11.4 深层链接

深层链接(Deep Link)允许外部应用或通知直接打开你App的某个特定页面。比如用户点击一个商品链接,直接跳转到商品详情页,而不是先打开首页再跳转。

Navigation Compose对深层链接的支持非常优雅。你只需要在composable块里添加deepLinks参数:

composable(
    route = "detail/{itemId}",
    deepLinks = listOf(
        navDeepLink { uriPattern = "myapp://detail/{itemId}" },
        navDeepLink { uriPattern = "https://www.example.com/detail/{itemId}" }
    )
) { backStackEntry ->
    val itemId = backStackEntry.arguments?.getString("itemId")
    DetailScreen(itemId = itemId)
}

这里定义了两个深层链接模式:一个是自定义scheme(myapp://),一个是标准的https链接。当系统收到匹配的Intent时,Navigation Compose会自动解析参数并导航到对应页面。

在AndroidManifest.xml里还需要配置一下:

<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="myapp"
            android:host="detail" />
    </intent-filter>
    <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="https"
            android:host="www.example.com"
            android:pathPrefix="/detail" />
    </intent-filter>
</activity>

深层链接在推送通知场景下特别有用。用户收到一条推送,点击后直接跳转到对应的消息详情页,而不是App首页。用户体验会好很多。

注意:深层链接的uriPattern里,路径参数名必须和路由里的参数名一致。比如路由是"detail/{itemId}",深层链接里也要用{itemId},不能写成{id}或{productId}。

11.5 知识体系总览

下面这张图总结了本章的核心知识点:

Navigation Compose 知识体系 Navigation Compose NavHost NavController 参数传递 深层链接 composable() 定义页面 startDestination 起始页 navigate() 跳转 popBackStack() 返回 路径参数 {param} 查询参数 ?key=value 自定义scheme / https

从图中可以看出,Navigation Compose以NavHost和NavController为骨架,参数传递和深层链接为血肉,共同构成了完整的导航体系。理解了这个结构,你就能灵活应对各种页面跳转需求。

最后说一句:导航是App的骨架,骨架稳了,功能才能往上堆。花点时间把导航设计好,后面开发会顺畅很多。


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