Compose Navigation 3 深度解析(一):为什么我们需要它

3 阅读10分钟

Navigation 3 是个啥

Navigation3是Google在2025年5月20号发布的,用于在 Compose 应用中管理导航。直到2025年11月19号才发布第一个正式版本 1.0.0。所以现有网上的教程基本上都是Navigation2的, 即便是问AI Navigation3怎么写,给出的代码也经常混杂Navigation2的代码,所以我准备以几篇文章,大概讲解 Navigation3 怎么使用。

在说明为什么要有 Navigation 3 之前,先来看看 Navigation 3 是个啥,下面是官方的介绍

Navigation 3 是一款旨在与 Compose 搭配使用的新导航库。借助 Navigation 3,您可以完全控制返回堆栈。

  • 用于对返回堆栈进行建模的惯例,其中返回堆栈上的每个条目都表示用户已前往的内容
  • 一种可随返回堆栈变化(包括动画)自动更新的界面
  • 用于返回堆栈中项的范围,允许在项位于返回堆栈中时保留状态
  • 一种自适应布局系统,可同时显示多个目的地,并允许在这些布局之间无缝切换
  • 一种用于让内容与其父布局(元数据)进行通信的机制

翻译成人话呢就是在 Compose 中我们做了一个类似于 Activity 系统的页面队列管理器,不但支持显示新页面,返回老页面,还支持记住页面的历史,页面的数据,而且还支持动画、自适应布局以及和其他页面通信呦。

一句话概括:管理场景跳转以及组合显示页面为场景

传统 View 系统与 Compose

方便做对比,咱们这里使用 前端Html来做对比说明,毕竟Android其实也算是前端了,很多的设计理念都是相通的。

传统 View 系统怎么做导航

Navigation 3 本质上是一个路由系统,用于管理 Compose 应用中的导航。要说明为什么需要它,咱得先看看传统Android是怎么做多页面系统的。

传统Android View系统中,每个页面的单位是啥?Activity啊,每个Activity都是一个页面,通过Intent来跳转页面。

对于Html呢?传统的Html页面,比如使用jQuery编写的网页,每个页面是一个html文件。

新框架下前端架构

Compose 框架和前端的 React 以及 Vue 有异曲同工之妙。都是通过数据驱动页面,来实现多页面系统的。这类的框架都有一个大名鼎鼎的外号:One Page Application(SPA)。

嘛意思?

一个 React SPA 就是一个单页面应用,整个应用都是一个页面,通过路由来切换不同的页面。

一个 Vue SPA 就是一个单页面应用,整个应用都是一个页面,通过路由来切换不同的页面。

简单来说,对于前端,单页面意味着就只有一个 index.html 文件,所有的页面效果都是通过路由来实现的, js 根据不同的路由,在index.html 中渲染出不同的内容。

那对于Android来说,单页面意味着什么?意味着只有一个Activity,所有的页面内容都是通过路由来切换的。

先说明Compose是不是也可以通过多个Activity来实现多页面系统呢?当然可以,但没必要!虽然通过多个Activity也可以实现多页面系统,但是不优雅,不完美,没办法充分利用Compose的特性。

为什么?

SPA 相比于传统 View 系统有什么优势

传统 View 系统以 Activity 为单位,还记得最开始学习 Android 的时候先学的啥?Activity 的声明周期啊,onCreate(),onStart(),onResume(),onPause(),onStop(),onDestroy()。那 Compose 组件有生命周期吗。有啊,Enter, Composition, Leave,就问你简洁不简洁。

转存失败,建议直接上传图片文件

从生命周期来看,就可以看到,组件生命周期要简单的多,相对于的性能消耗也会小得多。

Activity 会比 Compose 占用更多的内存。而且启动 Activity 需要系统完成窗口创建,视图树初始化、声明周期回调,进程间通信等各种操作。

Compose 的页面本质上是 Composeable 函数,页面切换只是 Compose 内部的局部重组,不会创建新容器,性能消耗极小。

这咋这么熟悉呢?线程?协程?不也是这个思想嘛!

还有一个优势就是传统 View 页面中,一个 xml 就是一个 view,要想单独设计一个一个小组件组合 xml 是一件非常费劲的事情,而且页面与数据逻辑分离,维护比 Compose 要复杂,Compose 的声明式状态驱动 UI 方式,组合 UI 那就是是否调用 Compose 函数,而且每个组件的使用方式那就是函数参数,一目了然。

更重要的是基于 Compose 的 KMP 项目可以做到跨平台,传统 View 架构下的 Android 只能局限于 Android。

为什么要有 Navigation

从上面的描述可以看出来,Navigation 和 SPA 是相辅相成的,如果页面还是以 Activity 为单位,页面切换由安卓系统 AMS 管理,也就没 Navigation 什么事情了。

为什么要有 Navigation?这个问题就像 React 为什么要有 react-router、Vue 为什么要有 vue-router 一样,是现代单页面应用架构的必然选择。那么,让我们来具体分析一下如果没有 Navigation 会怎么样。

如果没有 Navigation

先来看一看,如果没有 Navigation 的话,我们切换多个页面怎么办呢?比如有两个页面 Home, About,是不是得这么写?

var homeVisible by remember { mutableStateOf(true) }
var aboutVisible by remember { mutableStateOf(false) }

if (homeVisible) {
    Home(goToAbout = {
        homeVisible = false
        aboutVisible = true
    })
} else if (aboutVisible) {
    About(goToHome = {
        homeVisible = true
        aboutVisible = false
    })
}

但是这里有一个致命的问题,状态管理。Compose 状态是绑定到可组合项上的,上面的逻辑,一旦 Home 不可见,Home 组件其实已经销毁了,里面状态也就销毁了,如果存在一个临时的内部状态,想要恢复就比较麻烦。

状态提升

一个解决方式就是状态提升。比如下面这样

var homeState by remember { mutableStateOf(HomeState()) }
var aboutState by remember { mutableStateOf(AboutState()) }

if (homeState.visible) {
    Home(state = homeState, goToAbout = {
        homeState.visible = false
        aboutState.visible = true
    })
} else if (aboutState.visible) {
    About(state = aboutState, goToHome = {
        homeState.visible = true
        aboutState.visible = false
    })
}

这样就避免了 Home 消失的时候状态一块儿消失。

貌似也不是那么复杂是不是呢?但是如果考虑上从 Home 页面进入 About 页面后,点击返回重新返回 Home 呢?在 About 里面是不是得监听返回事件,About 是不是得这么写

@Composable
fun About(state: AboutState, goToHome: () -> Unit) {
    // 监听返回事件
     BackHandler(enabled = true) {
        goToHome()
    }
}

从 Home 页面可能会进入很多页面,每个页面都要这么写吗?这还只是两个页面,如果十几个页面怎么办呢?如果十几个页面有调用栈,要依次返回怎么办呢?不同页面之间的切换如果要添加动画怎么办呢?是不是想想都头大呢?

跨平台

还有一个很大的原因在于什么呢?Compose 设计出来并不单单简单应用于 Android,一个 Compose 页面理想情况下不应单单只能被应用于 Android 系统,它应该还可以被轻松地整合到 IOS、桌面端这些系统,如果每个页面是一个 Activity 的话,强依赖于 Activity,那么迁移到桌面端,就必须得为每个页面编写 window 和处理返回事件,这大大影响了 Compose 作为跨平台框架的优势。

如果使用单页面系统,每个平台只需要创建一个窗口就可以,而不必要为每个页面单独创建窗口。

OK, Navigation 3 的出现就是为了解决这些问题。

Navigation 3

针对以上描述,我们再来看看 Google 对 Navigation 的描述

  1. 返回堆栈上的每个条目都表示用户已前往的内容(页面返回栈,记录页面跳转)
  2. 一种可随返回堆栈变化(包括动画)自动更新的界面 (支持页面间动画)
  3. 一种用于让内容与其父布局(元数据)进行通信的机制(页面间通信)
  4. 用于返回堆栈中项的范围,允许在项位于返回堆栈中时保留状态(页面切换保留状态)

这描述还是比较长对不对?咱再简化一下:

Navigation3其实就干两件事: 管理场景跳转以及组合显示页面为场景

使用起来也比较方便,使用 NavDisplay 函数来进行定义即可。

比如我们有三个页面,Home, Product, About。我们定义三个 NavKey,通过 NavKey 来标识切换到哪个页面,框架会根据键的类型切换页面。

@Serializable
data object Home : NavKey

@Serializable
data class Product(val id : String) : NavKey

@Serializable
data object About : NavKey

@Composable
fun App() {
    // 默认主页面(Home)
    val backStack = rememberNavBackStack(Home)

    NavDisplay(
        // 导航堆栈
        backStack = backStack,
        // 导航条目装饰器(用于处理viewmodel和状态保存)
        entryDecorators = listOf(
            rememberSaveableStateHolderNavEntryDecorator(),
            rememberViewModelStoreNavEntryDecorator()
        ),
        // 导航返回事件
        onBack = {
            backStack.pop()
        },
        entryProvider = entryProvider {
            entry<Home> {
                HomePageView(goToProduct = {
                    backStack.add(Product(it))
                }, goToAbout = {
                    backStack.add(About)
                })
            }
            entry<Product> {
                ProductPageView()
            }
            entry<About> {
                AboutPageView()
            }
        }
    )
}

以上是使用Navigation 3的简单代码,使用起来也比较方便,我们只需要在每个页面中添加一个键,框架会根据键的类型切换页面,而且自动处理状态保存和恢复,自动添加动画,自动处理返回事件。

示例代码讲解

虽然例子比较简单,但是刚接触的时候估计也得理解一下,对于上面的示例代码,这里做一个简单的讲解。

咱们还是得围绕Navigation3的功能以及 NavDisplay 函数来讲解讲解。

再强调一下,Navigation3其实就干两件事: 管理场景跳转以及组合显示页面为场景

  1. 既然要管理页面,他得存储页面的跳转记录吧, 这就是 backStack 参数的作用,这是一个列表,存储了当前有多少页面,以及页面的顺序。
  2. 要管理页面,那他得知道什么时候该显示那种类型的页面吧,这就是 entryProvider 的作用,规定了不同类型的页面的显示逻辑。
  3. OK,再进一步,既然要组合页面为场景,他得知道要怎么组合吧,sceneStrategy 看名字就能看出来,场景策略,场景是啥,多个页面组合到一起就是场景,场景策略,顾名思义,就是怎么从 backStack 中的页面中组合出场景。
  4. 好了,场景的跳转和组合都有了,NavDisplay 函数就完成了,当然不是,没有动画的页面没有灵魂!!我们得添加动画, transitionSpec, popTransitionSpec, predictivePopTransitionSpec 定义了页面的动画,包括页面入栈动画,页面出栈动画,以及预测出栈动画。
  5. 最后,作为一个通用的导航框架,得给用户自定义预留一些切口允许用户干预吧,entryDecorators 参数的作用,就是用户可以自定义一些装饰器, 装饰器是编程中的老朋友了,在Python里面叫装饰器,在JavaScript里面叫代理,在Springboot里面叫AOP,巴拉巴拉。

好了,基本上Navigation3所有的功能都是围绕这些参数展开的。

至于如何处理页面跳转,如何组织场景,如何添加页面动画,如何实现深度链接,装饰器如何使用,我准备在后续的文章中详细说明。