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》

相关推荐
阿巴斯甜1 小时前
ContentObserver的使用:
android
casual_clover1 小时前
【Android 控件】使用 Navigation 实现底部导航功能时左右滑动退出页面功能
android·底部导航滑动退出
urkay-1 小时前
Kotlin Flow分类
android·开发语言·kotlin
阿pin2 小时前
Android随笔-MVC/MVP/MVVM/MVI
android·mvc·mvvm·mvp·mvi
阿pin2 小时前
Android随笔-View绘制流程
android·开发语言·view
2501_916008892 小时前
iOS 证书管理最佳实践 从创建到续期的完整指南
android·ios·小程序·https·uni-app·iphone·webview
Digitally3 小时前
9 款优质无线传输软件,实现安卓设备与电脑互传数据
android·电脑
私人珍藏库11 小时前
[Android] PeakFinder AR v4.8.89 (山峰全景识别+增强现实山峰查看器)
android·人工智能·智能手机·ar·工具·软件
researcher-Jiang12 小时前
高性能计算之OpenMP——超算习堂学习1
android·java·学习
alexhilton13 小时前
Kotlin DSL深度解析:从Gradle脚本到构建你自己的DSL
android·kotlin·android jetpack