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》

相关推荐
程序员陆业聪2 小时前
架构哲学与工程化:从开发体验到CI/CD的全维度对比|跨平台框架深度对决(三)
android
程序员陆业聪2 小时前
Android网络全链路拆解:一次HTTP请求背后的性能陷阱
android
程序员陆业聪2 小时前
渲染引擎与性能拆解:自绘vs原生渲染vs Bridge的终极对决|跨平台框架深度对决②
android
程序员陆业聪9 小时前
技术选型决策树:什么团队、什么项目该选什么框架 | 跨平台框架深度对决(4)
android
星辰徐哥10 小时前
Rust异步测试与调试的实践指南
android·java·rust
星河耀银海11 小时前
C++ 运算符重载:自定义类型的运算扩展
android·java·c++
阿巴斯甜11 小时前
Activity 之间大量数据传递有哪些方案?
android
阿巴斯甜11 小时前
必看1
android
帅次13 小时前
副作用 API:LaunchedEffect、DisposableEffect、SideEffect
android·compose·disposable·sideeffect·launched·ondispose