Navigation Compose:NavHost、NavController 与参数

技术目标

  1. 会用 rememberNavController() + NavHost 注册路由。
  2. 会用 navArgument + NavType 声明路径参数(与可选 query 方案区分)。
  3. 理解 NavBackStackEntry :在 composable { ... } lambda 中读取 argumentsSavedStateHandle,以及与 ViewModelStore 的对应关系。

1. 本仓库最小图

NavController
NavHost
home
samples
sample/profile/{userId}
HomeRoute
SamplesHubScreen
ProfileNavSampleScreen

AppNavHost.ktNavHostRoutes.HOMEstartDestination,各 composable(Routes.*) 注册样例屏。


kotlin 复制代码
package com.kuen.composedemo.navigation

import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.kuen.composedemo.home.AboutRoute
import com.kuen.composedemo.home.HomeRoute
import com.kuen.composedemo.samples.SamplesHubScreen
import com.kuen.composedemo.samples.design.DesignSystemSampleScreen
import com.kuen.composedemo.samples.lazy.LazyListSampleScreen
import com.kuen.composedemo.samples.modifier.ModifierLabScreen
import com.kuen.composedemo.samples.animation.AnimationLabScreen
import com.kuen.composedemo.samples.controls.ControlsLabScreen
import com.kuen.composedemo.samples.focus.FocusInsetsLabScreen
import com.kuen.composedemo.samples.layout.CustomLayoutLabScreen
import com.kuen.composedemo.samples.stability.StabilityLabScreen
import com.kuen.composedemo.samples.profile.ProfileNavSampleScreen
import com.kuen.composedemo.samples.sideeffect.SideEffectSampleScreen
import com.kuen.composedemo.samples.state.StateSampleScreen

@Composable
fun AppNavHost(navController: NavHostController) {
    NavHost(navController = navController, startDestination = Routes.HOME) {
        composable(Routes.HOME) {
            HomeRoute(
                onOpenAbout = { navController.navigate(Routes.ABOUT) },
                onOpenSamples = { navController.navigate(Routes.SAMPLES_HUB) },
            )
        }
        composable(Routes.ABOUT) {
            AboutRoute(onBack = { navController.popBackStack() })
        }
        composable(Routes.SAMPLES_HUB) {
            SamplesHubScreen(
                onBack = { navController.popBackStack() },
                onNavigate = { route -> navController.navigate(route) },
            )
        }
        composable(Routes.DESIGN_SAMPLE) {
            DesignSystemSampleScreen(onBack = { navController.popBackStack() })
        }
        composable(Routes.STATE_SAMPLE) {
            StateSampleScreen(onBack = { navController.popBackStack() })
        }
        composable(
            route = Routes.PROFILE_WITH_ARGS,
            arguments = listOf(
                navArgument(Routes.ARG_USER_ID) { type = NavType.StringType },
            ),
        ) { entry ->
            val userId = entry.arguments?.getString(Routes.ARG_USER_ID).orEmpty()
            ProfileNavSampleScreen(
                userId = userId,
                onBack = { navController.popBackStack() },
            )
        }
        composable(Routes.LAZY_SAMPLE) {
            LazyListSampleScreen(onBack = { navController.popBackStack() })
        }
        composable(Routes.SIDE_EFFECT_SAMPLE) {
            SideEffectSampleScreen(onBack = { navController.popBackStack() })
        }
        composable(Routes.MODIFIER_SAMPLE) {
            ModifierLabScreen(onBack = { navController.popBackStack() })
        }
        composable(Routes.STABILITY_SAMPLE) {
            StabilityLabScreen(onBack = { navController.popBackStack() })
        }
        composable(Routes.CONTROLS_SAMPLE) {
            ControlsLabScreen(onBack = { navController.popBackStack() })
        }
        composable(Routes.CUSTOM_LAYOUT_SAMPLE) {
            CustomLayoutLabScreen(onBack = { navController.popBackStack() })
        }
        composable(Routes.ANIMATION_SAMPLE) {
            AnimationLabScreen(onBack = { navController.popBackStack() })
        }
        composable(Routes.FOCUS_SAMPLE) {
            FocusInsetsLabScreen(onBack = { navController.popBackStack() })
        }
    }
}

2. 路径模板与参数

最小模板(概念):

kotlin 复制代码
NavHost(navController, startDestination = "home") {
    composable("home") { Home() }
    composable("detail/{id}", arguments = listOf(navArgument("id") { type = NavType.IntType })) { entry ->
        val id = entry.arguments?.getInt("id") ?: return@composable
        Detail(id)
    }
}

本仓库 带参路由 定义在 Routes.kt

  • PROFILE_WITH_ARGS = "sample/profile/{$ARG_USER_ID}"ARG_USER_ID = "userId"
  • 工厂方法 profileRoute(userId: String):拼接 "sample/profile/$userId",避免各处手写字符串。

AppNavHost.kt 中对应注册:

kotlin 复制代码
composable(
    route = Routes.PROFILE_WITH_ARGS,
    arguments = listOf(navArgument(Routes.ARG_USER_ID) { type = NavType.StringType }),
) { entry ->
    val userId = entry.arguments?.getString(Routes.ARG_USER_ID).orEmpty()
    ProfileNavSampleScreen(userId = userId, ...)
}

ProfileNavSampleScreen.kt 展示 userIdSamplesHubScreen.kt 通过 onNavigate(Routes.profileRoute("demo_user")) 跳入,便于本地手测。


  • navigate("route") { launchSingleTop = true }:栈顶已存在同 destination 时避免 多实例
  • popUpTo("a") { inclusive = true }:弹出到 a 并决定是否包含 a------极易配错清空栈 ;改完必须手测 系统返回键业务返回
  • popUpTo + saveState / restoreState :底部多 Tab 场景常见;与 rememberSaveable 状态不要混成两套恢复语义。

4. 与 Compose 状态的关系

  • NavController.currentBackStackEntryAsState():把返回栈暴露为 State,可驱动底部导航高亮、标题等。
  • 不要把大对象 Serializable 进路由 :URL 长度与序列化成本都不适合;传 id ,详情页再拉取 / 用 共享 ViewModel scope(按官方推荐模式)传只读缓存。
  • 进程死亡恢复 :路径参数会进 SavedStateHandle内存单例缓存 不可当作唯一数据源,否则恢复后空窗。

5. 与 viewModel() 作用域(衔接 01 篇)

  • 同一 composable(route) { }viewModel() 默认绑定当前 NavBackStackEntry 为 owner(除非另行指定)。
  • 跨 destination 需要共享数据时,显式使用 navigation(start = ...) { ... } 嵌套图ViewModel 的 scoped 获取 API(以当前 Navigation + lifecycle 版本文档为准),避免「在错误 graph 取到错误实例」。

6. 风险清单

  • 路径参数含 /?# → 必须 URL 编码 或改用 query 参数 方案。
  • NavHost + 多 Tab:deeplink返回键 易进错栈;需统一「单一返回栈真相」的产品语义。
  • deeplink 与 navArgument 默认值 :未声明 optional 时,非法 deep link 可能导致 启动即空屏或 crash,要在设计阶段列矩阵测试。

7. 自检清单

  1. 所有 route 字符串是否集中在 Routes(或等价常量模块),避免魔法字符串?
  2. 带参路由是否 同时 具备:navArgument 声明、profileRoute 式拼接、目标屏 空参兜底
  3. navigatepopUpTo 是否在真机上走过 A→B→C→返回 全路径?
  4. 是否需要 launchSingleTop 防止同屏多实例?

参考答案(复习用)

  1. 。本仓库用 Routes.kt 集中常量 + profileRoute();跳转处只传 Routes.* 或工厂返回值,避免 "sample/profile/" + id 散落。
  2. 是才算合格NavHostnavArgumentroute 占位符一致;对外用 Routes.profileRoute(userId) 生成路径;屏内用 entry.arguments?.getString(...).orEmpty() 等兜底,避免 null 或未注册参数导致闪退。
  3. popUpTo / inclusive 后必须手测 。期望:系统返回与 App 内「返回」是否一致、是否会意外清空栈或跳过某屏;多 Tab 时还要测 deeplink 进栈
  4. 若同一 destination 可能被反复 navigate 且不应叠多份 (如单例详情、首页),需要 launchSingleTop = true;若业务就是要多实例栈(少见),则不加。

源码仓库ComposeDemo(分支 main

系列推荐

《副作用 API:LaunchedEffect、DisposableEffect、SideEffect》

《LazyColumn 懒加载、items 与 key》

相关推荐
00后程序员张17 小时前
iOS加固技术路线全面解析:Bitcode模式、源码模式与汇编模式对比及爱加密优势
android·汇编·ios·小程序·uni-app·cocoa·iphone
summerkissyou198717 小时前
android - 性能 - Perfetto - 内存分析教程及例子
android·性能
超开心~17 小时前
Android AAudio介绍及流程分析
android
取个名字太难了~17 小时前
最新手机外接相机实时传输 + 动态美颜完整项目源码
android·数码相机·智能手机·美颜·相机连接·demu
say_fall18 小时前
【Linux系统编程】文件操作基础:C标准库、系统调用、fd是什么和fd与FILE*的关系
android·linux·c语言
ii_best1 天前
更新!移动端开发软件按键安卓版&手机助手v5.1.0上线!本地AI识别全面解锁,脚本开发再升级
android·人工智能·ios·按键精灵
方乐寺村1 天前
s,使用libpng提升png图片的保存速度。接下来本文将阐述在Android中如何集成libpng,以及在使用过程中遇到的问题和最 ...
android
name好难取诶1 天前
PHP 静态分析工具实战 PHPStan 和 Psalm 完全指南
android·开发语言·php
Lydia ,1 天前
安卓基础-线性布局
android
小小勇气大爆发1 天前
Android四大组件总结
android