Compose Navigation

要点

导航状态决定当前页面与返回路径。参数传 ID,页面根据 ID 读取数据,可避免序列化整个对象与同步过期副本。

目录


添加依赖

在 gradle/libs.versions.toml 中添加以下固定版本示例;它不是“当前最新”的声明。已有项目应保留自己的版本目录,并核对 Navigation、Kotlin 与 Compose 工具链兼容性:

[versions]
navigationCompose = "2.9.5"

[libraries]
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }

在 app/build.gradle.kts 中引用:

dependencies {
    implementation(libs.androidx.navigation.compose)
}

💡 Navigation 2.8+ 支持类型安全路由(配合 kotlinx.serialization 定义 @Serializable data object),本课先掌握字符串路由这一基础形态,其概念完全通用。


核心三件套

Navigation Compose 由三个角色组成:

角色API职责
NavControllerrememberNavController()导航状态大脑,管理返回栈
NavHostNavHost(navController, startDestination)容器,根据当前目的地切换内容
目的地composable("route") { }一条路由对应一个 Composable 页面

导航接线骨架(TaskListScreen 等页面需由项目提供,不能整段当作独立应用运行):

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

    NavHost(navController = navController, startDestination = "list") {
        composable("list") {
            TaskListScreen(
                onTaskClick = { id -> navController.navigate("detail/$id") }
            )
        }
        composable("detail/{taskId}") { backStackEntry ->
            val taskId = backStackEntry.arguments?.getString("taskId")
            TaskDetailScreen(taskId = taskId)
        }
    }
}

路由传参

位置参数(必填)

路由中用 {占位符} 声明,配合 navArgument 指定类型:

composable(
    route = "detail/{taskId}",
    arguments = listOf(
        navArgument("taskId") { type = NavType.LongType }
    )
) { backStackEntry ->
    val taskId = backStackEntry.arguments?.getLong("taskId") ?: 0L
    TaskDetailScreen(taskId)
}

// 导航时直接填值
navController.navigate("detail/42")

查询参数(可选)

用 ?key={key} 声明并提供默认值:

composable(
    route = "list?filter={filter}",
    arguments = listOf(
        navArgument("filter") {
            type = NavType.StringType
            defaultValue = "all"          // 缺省值
        }
    )
) { backStackEntry ->
    val filter = backStackEntry.arguments?.getString("filter") ?: "all"
}

navController.navigate("list?filter=done")

经验法则:参数只传简单标识(id、query),复杂对象不要塞进路由——它们应存于数据库或共享 ViewModel,页面只拿 id 现查。


↩ 返回栈控制

// 基本前进
navController.navigate("detail/1")

// 返回上一页(系统返回键等价)
navController.popBackStack()

// 带导航选项:避免重复入栈 + 弹到指定目的地
navController.navigate("list") {
    launchSingleTop = true                         // 目的地已在栈顶则不重复创建
    popUpTo("list") { inclusive = false }          // 弹出到 list 为止
    // inclusive 也会移除目标条目;嵌套图下必须选对目标,不能视作通用“清空栈”。
}

典型场景——登录成功后进入主页并清空返回栈,防止按返回键回到登录页。


底部导航

结合 Scaffold 与 NavigationBar,用当前返回栈条目驱动选中态:

@Composable
fun MainScaffold(navController: NavHostController) {
    val backStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = backStackEntry?.destination?.route

    Scaffold(
        bottomBar = {
            NavigationBar {
                listOf(
                    "list" to Icons.Default.List,
                    "profile" to Icons.Default.Person
                ).forEach { (route, icon) ->
                    NavigationBarItem(
                        selected = currentRoute == route,
                        onClick = {
                            navController.navigate(route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true      // 切回标签恢复滚动位置等状态
                            }
                        },
                        icon = { Icon(icon, contentDescription = null) },
                        label = { Text(if (route == "list") "任务" else "我的") }
                    )
                }
            }
        }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = "list",
            modifier = Modifier.padding(innerPadding)     // ⭐ 避免内容被底栏遮挡
        ) {
            composable("list") { TaskListScreen() }
            composable("profile") { ProfileScreen() }
        }
    }
}

popUpTo + saveState/restoreState + launchSingleTop 是官方推荐的标签切换”标准四件套”。


嵌套导航图

用 navigation 把相关页面编组,形成”图内路由”:

NavHost(navController, startDestination = "auth") {
    // 认证流程组
    navigation(startDestination = "login", route = "auth") {
        composable("login") {
            LoginScreen(
                onLoginSuccess = {
                    navController.navigate("home") {
                        popUpTo("auth") { inclusive = true }   // 登录后整组出栈
                    }
                }
            )
        }
        composable("register") { RegisterScreen() }
    }

    // 主流程组
    navigation(startDestination = "home", route = "home_graph") {
        composable("home") { HomeScreen() }
        composable("detail/{taskId}") { TaskDetailScreen() }
    }
}

嵌套图把认证流程编组,popUpTo("auth") { inclusive = true } 用于移除该流程。仅声明 route = "auth" 不会自动注册外部深链;还需声明 deepLinks,外部 URL 入口还涉及 Manifest 的 intent filter。参见 Android 深链说明。


练习与实践

先交付可回查的两页练习

前置:Android Studio 中已有 Empty Activity Compose 项目,Gradle 同步成功,设备能显示静态 Text;下文沿用 Navigation 2.x 字符串路由,不能与 Navigation 3 的 API 混写。把 NavHost 放进 Activity 的 setContent,补齐 runtime、Material3 与 navigation-compose 的 import。文中的页面组件是接线占位,需自行实现。

产物:列表页固定展示 id=1, title=第一条笔记,详情只接收 Long ID,再从固定 Map 查询标题;提供返回按钮及“打开不存在的 999”按钮。详情查不到时显示“笔记不存在”,仍允许返回。导航层传 onOpen(id) 和 onBack() 回调给页面,页面不必直接持有 NavController,便于单独检查 UI。

输入预期输出失败回查
列表点 1详情显示“第一条笔记”route 占位名、NavType.LongType 与 getLong 的键是否一致
点返回回到列表popBackStack 是否返回 false,是否已经位于根目的地
打开 999明确缺失状态,可返回是否把 ID 存在误当作业务记录存在
连续导航同一栈顶详情配置 launchSingleTop 时不重复入栈它只作用于栈顶,不负责全栈去重
详情按系统返回回到列表自定义 BackHandler 是否拦截事件

本轮未执行 Gradle 构建、Android 模拟器或真机,以上为验收预期,原生导航仍未验证。接入后先执行项目的 gradlew.bat :app:assembleDebug,再在设备走完整操作表,记录 Android API、Navigation 版本与截图;编译成功不等于回栈行为正确。

下一步先接入 协程与 Flow,把固定 Map 换成按 ID 查询的数据源,再检查删除记录后返回详情的缺失状态。最后才扩展标签页保存恢复和外部深链。参考 官方导航操作。

基础练习

  • [ ] 为笔记应用加上两个页面:列表页 + 详情页,点击条目携带 id 跳转
  • [ ] 给详情页添加可空的 ?highlight={highlight} 参数,实现”定位到指定关键词”
  • [ ] 在详情页放一个”返回”按钮,用 popBackStack() 实现
  • [ ] 用 launchSingleTop 阻止连续点击两次按钮产生的双重入栈

进阶挑战

  • [ ] 实现三标签底部导航,验证切换标签后滚动位置被恢复(restoreState)
  • [ ] 把登录流程封装为 navigation(startDestination = "login", route = "auth") 嵌套图,登录成功后整组出栈
  • [ ] 进阶阅读:用 kotlinx.serialization 把字符串路由改造成类型安全路由(Navigation 2.8+)

相关文档

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇