Navigation3 是 Google 官方全新迭代的导航组件,专为 Jetpack Compose 而生,彻底重构了 Navigation2 的设计逻辑。不同于旧版本依赖 XML 导航图、NavController 的繁琐写法,Navigation3 采用纯声明式、返回栈可控的设计,更贴合 Compose 声明式 UI 的编程思想,是目前 Compose 项目官方推荐的最优导航方案。
本文定位为学习记录+实战手册,通俗易懂、干货齐全,包含核心概念、新旧版本对比、基础导航、参数传递、返回栈管理、底部导航、常见避坑,所有代码可直接复制运行。
一、先搞懂:Navigation3 是什么?解决了什么问题?
1. 版本区别:Navigation2 vs Navigation3
很多新手分不清两个版本,这里一句话区分:
- Navigation2:兼容 Fragment 和 Compose,基于 XML 导航图、NavController 控制器,黑盒封装严重,返回栈不可见、难以自定义,参数传递繁琐,是「兼容旧项目」的方案
- Navigation3:仅适配 Compose,无 XML、无隐藏栈,返回栈完全透明可控、纯代码声明式,架构更简洁,是「Compose 新项目首选」
2. Navigation3 核心优势
- 栈完全可控:返回栈是普通集合,开发者可以自由增删、清空、替换,告别旧版本栈黑盒问题
- 纯声明式编程:贴合 Compose 设计理念,无需操作控制器,状态驱动页面切换
- 天然类型安全:支持数据类路由,传参无需 Bundle,杜绝 Key 写错、类型转换异常
- 适配性更强:完美支持单页面、多页面、底部导航、弹窗、大屏双页布局
- 代码极简:移除冗余配置,大幅减少模板代码
二、核心四大组件(必记,全文核心)
Navigation3 抛弃了 NavController、NavGraph、XML 导航图,全新四大核心 API,结构极度清晰:
| 组件 | 作用 |
|---|---|
| 返回栈 BackStack | 存储所有路由页面的栈集合,本质是状态列表,页面跳转、返回全部依赖栈数据变化 |
| NavDisplay | 导航容器,核心入口,监听返回栈变化,自动渲染栈顶页面 |
| NavEntry | 路由条目,绑定路由标识和对应的 Compose 页面 |
| 路由 Key | 唯一页面标识,支持 object(无参页面)、data class(带参页面) |
极简工作流程:定义路由 Key → 初始化返回栈 → NavEntry 绑定页面 → NavDisplay 渲染页面 → 操作栈实现跳转/返回
三、环境配置:添加依赖
Navigation3 是全新独立库,需要单独添加依赖,截止目前最新稳定版,在 Module 级别的 build.gradle 添加依赖:
// Navigation3 核心依赖
implementation "androidx.navigation3:navigation3-runtime:1.1.0"
implementation "androidx.navigation3:navigation3-ui:1.1.0"
同步项目即可,无需额外配置,不依赖任何 XML 文件。
四、零基础实战:实现基础页面跳转
我们从零开始,实现 首页 → 详情页 跳转、页面返回、退出页面等基础功能。
步骤1:定义全局路由(最佳实践)
新建路由管理文件 AppRoutes.kt,统一管理所有页面路由,方便后期维护,区分无参/带参页面:
// 全局路由标识,统一管理所有页面
// 无参页面使用 data object
data object HomeRoute : NavKey
// 带参页面使用 data class,参数直接定义在构造方法
data class DetailRoute(val articleId: Int, val title: String) : NavKey
这就是 Navigation3 类型安全路由 的核心,传参不需要字符串 Key、不需要 Bundle,直接通过实体类携带参数。
步骤2:初始化导航栈 & 绑定页面
在全局入口页面,初始化返回栈,通过 NavDisplay + entryProvider 绑定路由与页面:
@Composable
fun AppNavHost() {
// 初始化导航返回栈,默认首页为 HomeRoute
val backStack = remember { mutableStateListOf<NavKey>(HomeRoute) }
// 导航核心容器
NavDisplay(
backStack = backStack,
// 绑定路由对应的页面
entryProvider = entryProvider {
entry<HomeRoute> {
HomeScreen(
// 跳转详情页:入栈新路由
onToDetail = { id, title ->
backStack.add(DetailRoute(id, title))
}
)
}
entry<DetailRoute> { route ->
DetailScreen(
route = route,
// 返回上一页:弹出栈顶路由
onBack = {
if (backStack.size > 1) {
backStack.removeAt(backStack.size - 1)
}
}
)
}
}
)
}
步骤3:编写页面UI & 跳转逻辑
编写首页、详情页UI,实现跳转和返回功能:
// 首页页面
@Composable
fun HomeScreen(onToDetail: (Int, String) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Navigation3 首页", fontSize = 24.sp)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
// 携带参数跳转到详情页
onToDetail(1001, "Navigation3 学习记录")
}) {
Text("跳转详情页")
}
}
}
// 详情页页面
@Composable
fun DetailScreen(route: DetailRoute, onBack: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "详情页", fontSize = 24.sp)
Spacer(modifier = Modifier.height(16.dp))
// 直接获取路由携带的参数,类型绝对安全
Text(text = "文章ID:${route.articleId}")
Text(text = "文章标题:${route.title}")
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = onBack) {
Text("返回首页")
}
}
}
步骤4:全局挂载导航宿主
在 Activity 中挂载导航容器,作为全局页面入口:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Nav3StudyTheme {
AppNavHost()
}
}
}
}
五、Navigation3 核心操作大全
Navigation3 没有复杂的 API,所有导航操作本质都是操作返回栈集合,通俗易懂,完全透明。
1. 普通跳转(入栈)
打开新页面,保留上一页,支持返回,最常用:
backStack.add(DetailRoute(1002, "新页面"))
2. 页面返回(出栈)
关闭当前页面,回到上一页,需要判断栈大小,避免栈空崩溃:
// 兼容所有 Compose 版本!替代 removeLast(),解决方法不存在崩溃
if (backStack.size > 1) {
backStack.removeAt(backStack.lastIndex)
}
3. 单任务跳转(清空上层栈)
场景:首页跳转详情,再跳转设置,设置页返回直接回到首页,清空中间页面:
// 移除当前所有非首页的页面,再入栈新页面
backStack.removeAll { it !is HomeRoute }
backStack.add(SettingRoute)
4. 重启首页(清空全栈)
场景:退出登录、回到首页,清空所有页面栈:
backStack.clear()
backStack.add(HomeRoute)
5. 替换当前页面
场景:启动页跳转首页,禁止返回启动页:
backStack.removeLast()
backStack.add(HomeRoute)
六、进阶实战:底部导航框架
日常项目最常用的底部 Tab 导航,Navigation3 实现极其简单,且完美适配 Tab 切换不重复入栈的需求。
1. 定义底部Tab路由
// 底部导航Tab路由
data object HomeTabRoute
data object MineTabRoute
data object MessageTabRoute
2. 实现底部导航页面
@Composable
fun BottomNavHost() {
val backStack = remember { mutableStateListOf<Any>(HomeTabRoute) }
// 获取当前栈顶路由
val currentRoute = backStack.lastOrNull()
Scaffold(
bottomBar = {
NavigationBar {
// 首页Tab
NavigationBarItem(
selected = currentRoute is HomeTabRoute,
onClick = {
// 避免重复入栈
if (currentRoute !is HomeTabRoute) {
backStack.clear()
backStack.add(HomeTabRoute)
}
},
icon = { Icon(Icons.Default.Home, contentDescription = null) },
label = { Text("首页") }
)
// 消息Tab
NavigationBarItem(
selected = currentRoute is MessageTabRoute,
onClick = {
if (currentRoute !is MessageTabRoute) {
backStack.clear()
backStack.add(MessageTabRoute)
}
},
icon = { Icon(Icons.Default.Message, contentDescription = null) },
label = { Text("消息") }
)
// 我的Tab
NavigationBarItem(
selected = currentRoute is MineTabRoute,
onClick = {
if (currentRoute !is MineTabRoute) {
backStack.clear()
backStack.add(MineTabRoute)
}
},
icon = { Icon(Icons.Default.Person, contentDescription = null) },
label = { Text("我的") }
)
}
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
NavDisplay(
backStack = backStack,
entryProvider = entryProvider {
entry<HomeTabRoute> { HomeScreen(onToDetail = {id,title ->}) }
entry<MessageTabRoute> { MessageScreen() }
entry<MineTabRoute> { MineScreen() }
}
)
}
}
}
核心亮点:Tab 切换不会重复创建页面、栈干净、无冗余页面,完美解决 Navigation2 底部 Tab 栈混乱的问题。
七、Navigation3 优势总结(对比 Navigation2)
- 栈完全透明:旧版本栈隐藏,无法精准控制;Navigation3 栈是普通集合,自由增删改查
- 类型绝对安全:摒弃字符串路由、Bundle 传参,通过数据类传参,编译期校验错误
- 无XML配置:纯代码实现,告别导航图文件,项目结构更简洁
- 适配Compose:完全声明式、状态驱动,契合 Compose 设计思想
- 学习成本更低:API 极简,无需记忆 NavController 各种方法
八、新手常见坑 & 最佳实践
1. 禁止重复入栈
页面快速点击会重复跳转,需要在跳转前判断当前路由,避免同一页面多次入栈。
2. 返回必须判断栈长度
栈内至少保留一个首页路由,否则栈为空会导致页面空白、程序异常。
3. 路由统一管理
所有路由集中在一个 Kotlin 文件,不要散落各处,方便后期迭代、统一维护。
4. 区分 Tab 导航与普通页面
底部 Tab 切换建议清空栈,普通页面跳转使用 add 入栈,保证栈逻辑清晰。
5. 不要滥用 clear()
仅在退出登录、Tab 切换、返回首页场景使用 clear,避免误清空栈导致页面异常。
九、全文总结
Navigation3 是 Google 为 Compose 量身打造的新一代导航框架,彻底颠覆了 Navigation2 的设计思路,去掉黑盒、回归简单。
核心记忆口诀:路由定义页面,栈控制跳转,NavDisplay 负责渲染。
对于所有全新 Compose 项目,直接使用 Navigation3,无需再使用老旧的 Navigation2;旧项目迭代也推荐逐步迁移,能极大减少导航相关的 Bug,提升代码可维护性。
十、高频疑问解答:能否混用 NavHost?为什么要用 entryProvider?
1、Navigation3 能和传统 NavHost 一起用吗?
结论:不能混用,二者互斥,是两套完全独立的导航体系。
我们先区分两个组件的归属:
- NavHost:属于 Navigation2(传统 Compose 导航),配套
NavController、导航图、字符串路由,是旧版方案 - NavDisplay + entryProvider:属于 Navigation3,是新版专属导航容器,彻底废弃 NavHost
很多新手疑惑:能不能部分页面用 NavHost、部分页面用 NavDisplay?
官方不推荐、且毫无意义:
- 两套导航拥有独立的返回栈,混用会导致页面返回逻辑混乱、手势返回失效、页面层级错乱
- NavHost 是黑盒栈,NavDisplay 是透明栈,底层机制完全不同,无法同步状态
- Navigation3 的诞生目的就是替代 NavHost,解决旧版本所有栈不可控、传参繁琐的痛点
项目适配原则:
- 新项目:全程使用 NavDisplay,彻底不用 NavHost
- 旧项目:维持 NavHost,不要混入 Navigation3,迭代重构时整体迁移
2、为什么 Navigation3 必须用 entryProvider?
很多人初学疑惑:为什么旧版用 NavGraph,新版要用 entryProvider?它的作用到底是什么?
一句话本质:entryProvider 就是 Navigation3 的「导航注册表」,替代了 Navigation2 的 NavGraph 导航图。
entryProvider 核心作用
- 绑定路由与页面映射关系
遍历你所有的路由 Key,告诉 NavDisplay:遇到这个路由,渲染哪个 Compose 页面。完全替代旧版本 XML 导航图、NavGraph 代码导航图。
- 实现编译期类型安全
旧版 NavHost 依赖字符串路由地址,写错不报错,运行崩溃。
而 entry<路由类型> 是泛型约束,路由和页面强绑定,编译阶段就能校验错误,从根源杜绝路由匹配错误。
- 自动注入路由参数
所有带参路由(data class),可以直接在 entry 内部获取路由实例,无需手动解析、无需 Bundle 解包,框架自动分发参数,极大简化传参逻辑。
- 纯代码、无配置文件、轻量解耦
相较于 NavGraph,entryProvider 不需要 XML 文件、不需要手动配置 startDestination,代码内聚性更强,完全贴合 Compose 声明式编程思想。
3、通俗对比:新旧导航核心对应关系
| Navigation2(NavHost) | Navigation3(NavDisplay) | 作用 |
|---|---|---|
| NavHost | NavDisplay | 页面渲染容器 |
| NavGraph | entryProvider | 路由注册表 |
| NavController | mutableStateListOf 栈 | 导航控制器 |
| 字符串路由 + Bundle | data object/data class | 路由标识与传参 |
4、补充:entryProvider 为什么不能省略?
NavDisplay 只负责监听栈变化、渲染页面,它本身不知道「哪个路由对应哪个页面」。
而 entryProvider 的唯一职责,就是维护路由-页面的映射表,是 Navigation3 导航匹配的核心依据,属于必填参数,无法省略。
十一、核心疑问总结
- NavHost 和 Navigation3 互斥,不可混用,两套独立导航栈,混用会造成导航逻辑混乱
- entryProvider 替代 NavGraph,是路由注册表,实现类型安全、参数自动注入
- Navigation3 核心思想:开发者掌控栈,框架只负责渲染,彻底告别黑盒控制器