Navigation Compose:NavHost、NavController 与参数

技术目标

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

1. 本仓库最小图

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

AppNavHost.kt 中 NavHost 以 Routes.HOME 为 startDestination,各 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 展示 userId;SamplesHubScreen.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. navigate 的 popUpTo 是否在真机上走过 A→B→C→返回 全路径?
  4. 是否需要 launchSingleTop 防止同屏多实例?

参考答案(复习用)

  1. 是 。本仓库用 Routes.kt 集中常量 + profileRoute();跳转处只传 Routes.* 或工厂返回值,避免 "sample/profile/" + id 散落。
  2. 是才算合格 。NavHost 里 navArgument 与 route 占位符一致;对外用 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》

相关推荐
半摆烂日常10 小时前
低代码平台API对接实践:接口鉴权与数据同步的完整实现
android·低代码·rxjava
事圆则缓11 小时前
volatile 使用场景全解析:它能保证什么,又解决不了什么
android
蒲公英内测分发12 小时前
智能投影仪参加海外展会,未上架的 Android 配套 App 怎么让客户限时试用?
android
ii_best12 小时前
按键精灵手机端开发安卓版实战:手写一个可最小化、可拖动的「运行日志悬浮窗」(附完整 源码 + 踩坑记录)
android·ios·智能手机·自动化·ai编程·按键精灵
终端安全笔记12 小时前
安卓做 MDM 管控,先分清三种注册入口
android·安全·智能手机
乌萨达13 小时前
手机模拟器安卓怎么用?电脑安装应用、玩手游与账号同步指南
android·智能手机·电脑·雷电模拟器
美狐美颜SDK开放平台13 小时前
Android与iOS直播APP美颜有什么区别?视频美颜SDK开发详解
android·ios·音视频·视频美颜sdk
春猿火15 小时前
某市-2026【网安·论道】misc-afterimage_note-wp
android
恋猫de小郭16 小时前
Shopify 回应为什么从 RN 回到原生,为什么不用 KMP ?
android·前端·flutter
fundroid18 小时前
Android AI 开发,真正拉开差距的是工程闭环
android·ai·大模型·agent