Jetpack Compose 入门系列(六):Navigation 3 页面导航

Jetpack Compose 入门系列(六):Navigation 3 页面导航

学完上篇你已经知道标准布局不够用时,如何用自定义 Layout 和 ConstraintLayout 解决复杂排版问题。但真实 App 不可能只有一个页面。本篇我们解决一个更贴近实战的问题:如何在 Compose 中进行页面跳转、传参和返回栈管理


在传统 View/XML 体系里,我们做页面跳转通常有几种方式:

  • ActivityActivity
  • Fragment + NavController
  • navigation.xml 里配置 destination
  • findNavController().navigate(...) 执行跳转

到了 Compose 里,页面本身就是一个个 @Composable 函数。如果还完全沿用过去那套"XML 里声明页面、代码里找控制器"的思路,就会显得有点别扭。

Compose 更习惯的方式是:状态变了,界面跟着变

导航也一样:

flowchart LR A[用户点击按钮] --> B[修改返回栈] B --> C[当前页面 Key 变化] C --> D[显示新的 Composable 页面]

用一句话概括:Compose 导航的核心不是"启动一个页面",而是"描述当前应该显示哪个页面"

先来看一下传统写法和 Navigation 3 写法的区别:

场景 传统 Navigation Compose Navigation 3
页面标识 字符串 route,比如 "detail/{id}" Kotlin 类型,比如 Screen.Detail(id)
参数传递 拼 route、取 arguments data class 构造参数
返回栈 主要由 NavController 内部维护 直接操作 backStack
类型安全 字符串写错运行时才知道 参数错了编译期就能发现

传统写法可能是这样:

kotlin 复制代码
navController.navigate("detail/$productId")

Navigation 3 更像这样:

kotlin 复制代码
backStack.add(AppScreen.Detail(productId = productId))

这就像从 Intent.putExtra("id", id) 升级成了一个明确的 Kotlin 对象:页面是谁、需要什么参数,都写在类型里。

Navigation 3 的核心变化:页面不再主要靠字符串 route 表示,而是用类型安全的 Key 表示


Navigation 3 主要分成两个模块:

模块 作用
navigation3-runtime 提供 NavKey、返回栈等基础能力
navigation3-ui 提供 Compose 里展示页面的 NavDisplay

下面用版本目录写法配置。

2.1 libs.versions.toml

toml 复制代码
[versions]
agp = "9.2.1"
kotlin = "2.2.10"
composeBom = "2026.02.01"
navigation3 = "1.1.3"
activity = "1.8.0"
kotlinxSerialization = "1.11.0"

[libraries]
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }

androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }

kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

2.2 app/build.gradle.kts

kotlin 复制代码
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.serialization)
    alias(libs.plugins.compose.compiler)
}

android {
    namespace = "com.example.navigation3demo"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.navigation3demo"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"
    }

    buildFeatures {
        compose = true
    }
}

dependencies {
    implementation(platform(libs.androidx.compose.bom))

    implementation(libs.androidx.activity.compose)
    implementation(libs.androidx.compose.material3)
    implementation(libs.androidx.compose.ui.tooling.preview)

    implementation(libs.androidx.navigation3.runtime)
    implementation(libs.androidx.navigation3.ui)

    implementation(libs.kotlinx.serialization.json)
}

这段配置里发生了什么:

  1. navigation3-runtime 提供导航返回栈和 NavKey
  2. navigation3-ui 提供 Compose 页面展示容器 NavDisplay
  3. kotlinx.serialization 用来让页面 Key 支持保存和恢复。
  4. Compose BOM 统一管理 Compose 相关依赖版本,避免版本互相打架。
  5. activity-compose 不由 Compose BOM 管理,所以这里单独声明 activity = "1.8.0"
  6. 示例里的 compileSdktargetSdk 写成 36,是为了和当前 Android SDK 环境保持一致;activity-compose 版本本身可以按项目实际情况单独调整。

上面这些版本里,Compose BOM 使用 2026.02.01,Activity Compose 使用 1.8.0。示例不追求最新版本,而是优先选择更稳妥、对入门读者更友好的版本组合。Navigation3 使用 1.1.3,本文重点讲它的核心思路:Key、BackStack、NavDisplay


三、NavKey:把页面定义成 Kotlin 类型

在传统 Navigation Compose 里,我们经常用字符串表示页面:

kotlin 复制代码
composable("home") { }
composable("detail/{productId}") { }

这种写法最大的问题是:字符串写错,编译器不知道

比如:

kotlin 复制代码
navController.navigate("detial/1001")

detail 拼成了 detial,编译器不会提醒你,运行时才可能出问题。

Navigation 3 更推荐把页面定义成 Kotlin 类型:

kotlin 复制代码
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable

sealed interface AppScreen : NavKey {

    @Serializable
    data object Home : AppScreen

    @Serializable
    data class Detail(
        val productId: Int
    ) : AppScreen
}

逐步拆解:

  1. AppScreen 是当前 App 所有页面的统一类型。
  2. Home 没有参数,所以用 data object
  3. Detail 需要商品 ID,所以用 data class
  4. Detail(productId: Int) 直接把页面参数放进构造函数。
  5. @Serializable 让这些页面 Key 可以被保存和恢复。

页面定义对比:

页面 字符串 route 写法 Navigation 3 写法
首页 "home" AppScreen.Home
详情页 "detail/{productId}" AppScreen.Detail(productId)
参数 arguments?.getString("productId") detail.productId
错误发现 运行时 编译期

关键点:NavKey 就是"页面 + 参数"的数据模型。你不是在跳一个字符串,而是在操作一个明确的 Kotlin 对象。


四、最小可运行示例:从首页跳到详情页

我们先做一个最小 Demo:

  • 首页显示两个商品按钮
  • 点击按钮进入详情页
  • 详情页显示商品 ID
  • 点击返回回到首页

页面关系如下:

flowchart TD A[Home 首页] -->|点击商品 1001| B[Detail 详情页] A -->|点击商品 1002| B B -->|返回| A

完整代码如下。为了避免在根页面继续返回时把首页也弹出,示例里会先判断 backStack.size > 1

kotlin 复制代码
package com.example.navigation3demo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import androidx.navigation3.runtime.entry
import androidx.navigation3.runtime.entryProvider
import kotlinx.serialization.Serializable

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Navigation3DemoApp()
            }
        }
    }
}

sealed interface AppScreen : NavKey {

    @Serializable
    data object Home : AppScreen

    @Serializable
    data class Detail(
        val productId: Int
    ) : AppScreen
}

@Composable
private fun Navigation3DemoApp() {
    val backStack = rememberNavBackStack(AppScreen.Home)

    NavDisplay(
        backStack = backStack,
        onBack = {
            if (backStack.size > 1) {
                backStack.removeLastOrNull()
            }
        },
        entryProvider = entryProvider {
            entry<AppScreen.Home> {
                HomeScreen(
                    onProductClick = { productId ->
                        backStack.add(AppScreen.Detail(productId = productId))
                    }
                )
            }

            entry<AppScreen.Detail> { detail ->
                DetailScreen(
                    productId = detail.productId,
                    onBackClick = {
                        if (backStack.size > 1) {
                            backStack.removeLastOrNull()
                        }
                    }
                )
            }
        }
    )
}

@Composable
private fun HomeScreen(
    onProductClick: (Int) -> Unit
) {
    Scaffold { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(
                text = "首页",
                style = MaterialTheme.typography.headlineMedium
            )

            Text(text = "这里是商品列表,点击商品进入详情页。")

            Button(
                onClick = {
                    onProductClick(1001)
                }
            ) {
                Text(text = "查看商品 1001")
            }

            Button(
                onClick = {
                    onProductClick(1002)
                }
            ) {
                Text(text = "查看商品 1002")
            }
        }
    }
}

@Composable
private fun DetailScreen(
    productId: Int,
    onBackClick: () -> Unit
) {
    Scaffold { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(
                text = "详情页",
                style = MaterialTheme.typography.headlineMedium
            )

            Text(text = "当前商品 ID:$productId")

            Button(
                onClick = onBackClick
            ) {
                Text(text = "返回首页")
            }
        }
    }
}

这段代码里发生了什么:

  1. rememberNavBackStack(AppScreen.Home) 创建返回栈,并把首页作为初始页面。
  2. NavDisplay 根据返回栈顶部的页面 Key 显示对应 UI。
  3. entry<AppScreen.Home> 表示当前页面是首页时,显示 HomeScreen
  4. entry<AppScreen.Detail> 表示当前页面是详情页时,显示 DetailScreen
  5. 首页点击商品时,调用 backStack.add(AppScreen.Detail(productId))
  6. 详情页点击返回时,在返回栈不止一个页面的情况下调用 backStack.removeLastOrNull()

返回栈变化可以这样理解:

text 复制代码
刚打开 App:
[Home]

点击商品 1001:
[Home, Detail(1001)]

点击返回:
[Home]

跳转页面就是往返回栈里 add 一个页面 Key;返回页面就是在保留根页面的前提下,从返回栈里移除最后一个页面 Key。


五、页面事件应该上提,不要让页面直接操作 backStack

这一点非常重要。

有些同学刚开始写 Navigation 时,会把 backStack 直接传进页面:

kotlin 复制代码
@Composable
private fun DetailScreen(
    productId: Int,
    backStack: MutableList<AppScreen>
) {
    Button(
        onClick = {
            backStack.removeLastOrNull()
        }
    ) {
        Text(text = "返回")
    }
}

这段代码能跑,但不推荐。

问题在于:DetailScreen 本来只是一个详情页,现在它还知道了外面的导航实现。以后你想在别的地方复用这个详情页,就会很麻烦。

更推荐这样写:

kotlin 复制代码
@Composable
private fun DetailScreen(
    productId: Int,
    onBackClick: () -> Unit
) {
    Button(
        onClick = onBackClick
    ) {
        Text(text = "返回")
    }
}

外层再决定点返回时具体做什么:

kotlin 复制代码
DetailScreen(
    productId = detail.productId,
    onBackClick = {
        if (backStack.size > 1) {
            backStack.removeLastOrNull()
        }
    }
)

来做个对比:

写法 页面是否知道导航实现 是否推荐
页面直接拿 backStack 知道 不推荐
页面只暴露 onBackClick 不知道 推荐
页面直接调用全局导航对象 知道,而且耦合更强 不推荐

这和前面学过的 State 提升是一个思路:

flowchart TB A[页面 Composable] -->|抛出事件 onClick| B[外层导航容器] B -->|修改 backStack| C[NavDisplay] C -->|显示新页面| A

页面只负责展示数据和抛出事件,导航容器负责决定去哪。这样代码更清晰,也更容易复用。


6.1 参数不要再手动拼字符串

❌ 错误:

kotlin 复制代码
val route = "detail/$productId"

这在 Navigation 3 里就没必要了。

✅ 正确:

kotlin 复制代码
backStack.add(AppScreen.Detail(productId = productId))

productIdInt,就是 Int,不用在字符串和数字之间来回转换。

多定义一个页面 Key,少掉一堆 route 拼错、参数取错的问题。

6.2 不要用一堆 if else 模拟页面栈

❌ 错误:

kotlin 复制代码
var currentProductId by rememberSaveable {
    mutableStateOf(0)
}

if (currentProductId == 0) {
    HomeScreen(
        onProductClick = { id ->
            currentProductId = id
        }
    )
} else {
    DetailScreen(
        productId = currentProductId,
        onBackClick = {
            currentProductId = 0
        }
    )
}

两个页面时看起来还能接受,但页面一多,就会变成一大坨 if else

✅ 正确:

kotlin 复制代码
val backStack = rememberNavBackStack(AppScreen.Home)

backStack.add(AppScreen.Detail(productId = 1001))
if (backStack.size > 1) {
    backStack.removeLastOrNull()
}

页面层级交给 backStack,页面内部状态交给各自的 Composable 或 ViewModel。

不要用一个 ID 或几个 Boolean 模拟导航栈,这不是什么玄学,页面多了以后真的会乱。

6.3 返回时不要把根页面弹掉

❌ 容易忽略的写法:

kotlin 复制代码
onBack = {
    backStack.removeLastOrNull()
}

如果当前已经在首页,再继续执行 removeLastOrNull(),就可能把根页面也移除掉。示例少的时候不明显,页面多了以后就容易出现空页面或状态异常。

✅ 更稳妥的写法:

kotlin 复制代码
onBack = {
    if (backStack.size > 1) {
        backStack.removeLastOrNull()
    }
}

也就是说:返回操作通常只应该弹出二级页面,根页面要留在栈底。

实际项目里你也可以封装一个 navigateBack() 方法,把这个判断统一收起来。

6.4 不要把所有页面写成一个 Composable

❌ 错误:

kotlin 复制代码
@Composable
private fun AppContent(
    screen: AppScreen
) {
    if (screen is AppScreen.Home) {
        Text(text = "首页")
    }

    if (screen is AppScreen.Detail) {
        Text(text = "详情页:${screen.productId}")
    }
}

这种写法会让一个 Composable 承担太多职责。

✅ 正确:

kotlin 复制代码
entry<AppScreen.Home> {
    HomeScreen(...)
}

entry<AppScreen.Detail> { detail ->
    DetailScreen(
        productId = detail.productId,
        ...
    )
}

每个页面单独拆成自己的 Composable,导航层只负责把 Key 映射到页面。

页面越多,越要拆清楚。不要把导航表写成一个巨大的 UI 函数。


七、综合实战:课程商城导航 Demo

下面我们把前面的知识串起来,做一个小型课程商城:

  • 首页展示课程列表
  • 点击课程进入详情页
  • 详情页点击购买进入结算页
  • 首页可以进入购物车
  • 购物车可以进入结算页
  • 每个二级页面都可以返回上一页

页面关系如下:

flowchart TD Home[首页] --> Detail[详情页] Home --> Cart[购物车] Detail --> Checkout[结算页] Cart --> Checkout Checkout --> Back[返回上一页]

完整代码:

kotlin 复制代码
package com.example.navigation3demo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import androidx.navigation3.runtime.entry
import androidx.navigation3.runtime.entryProvider
import kotlinx.serialization.Serializable

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                ShopNavigationApp()
            }
        }
    }
}

sealed interface ShopScreen : NavKey {

    @Serializable
    data object Home : ShopScreen

    @Serializable
    data class Detail(
        val productId: Int
    ) : ShopScreen

    @Serializable
    data object Cart : ShopScreen

    @Serializable
    data class Checkout(
        val fromCart: Boolean
    ) : ShopScreen
}

private data class Product(
    val id: Int,
    val name: String,
    val price: String
)

private val sampleProducts = listOf(
    Product(id = 1001, name = "Compose 入门课", price = "¥39"),
    Product(id = 1002, name = "Navigation 3 实战课", price = "¥59"),
    Product(id = 1003, name = "Material 3 UI 组件课", price = "¥49")
)

@Composable
private fun ShopNavigationApp() {
    val backStack = rememberNavBackStack(ShopScreen.Home)

    NavDisplay(
        backStack = backStack,
        onBack = {
            if (backStack.size > 1) {
                backStack.removeLastOrNull()
            }
        },
        entryProvider = entryProvider {
            entry<ShopScreen.Home> {
                ShopHomeScreen(
                    products = sampleProducts,
                    onProductClick = { productId ->
                        backStack.add(ShopScreen.Detail(productId = productId))
                    },
                    onCartClick = {
                        backStack.add(ShopScreen.Cart)
                    }
                )
            }

            entry<ShopScreen.Detail> { detail ->
                ProductDetailScreen(
                    product = sampleProducts.first { it.id == detail.productId },
                    onBackClick = {
                        if (backStack.size > 1) {
                            backStack.removeLastOrNull()
                        }
                    },
                    onBuyClick = {
                        backStack.add(ShopScreen.Checkout(fromCart = false))
                    }
                )
            }

            entry<ShopScreen.Cart> {
                CartScreen(
                    onBackClick = {
                        if (backStack.size > 1) {
                            backStack.removeLastOrNull()
                        }
                    },
                    onCheckoutClick = {
                        backStack.add(ShopScreen.Checkout(fromCart = true))
                    }
                )
            }

            entry<ShopScreen.Checkout> { checkout ->
                CheckoutScreen(
                    fromCart = checkout.fromCart,
                    onBackClick = {
                        if (backStack.size > 1) {
                            backStack.removeLastOrNull()
                        }
                    }
                )
            }
        }
    )
}

@Composable
private fun ShopHomeScreen(
    products: List<Product>,
    onProductClick: (Int) -> Unit,
    onCartClick: () -> Unit
) {
    Scaffold { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text(
                    text = "课程商城",
                    style = MaterialTheme.typography.headlineMedium
                )

                OutlinedButton(
                    onClick = onCartClick
                ) {
                    Text(text = "购物车")
                }
            }

            Text(text = "选择一门课程查看详情:")

            products.forEach { product ->
                ProductItem(
                    product = product,
                    onClick = {
                        onProductClick(product.id)
                    }
                )
            }
        }
    }
}

@Composable
private fun ProductItem(
    product: Product,
    onClick: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = onClick),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Text(
                text = product.name,
                style = MaterialTheme.typography.titleMedium
            )

            Text(text = product.price)
        }
    }
}

@Composable
private fun ProductDetailScreen(
    product: Product,
    onBackClick: () -> Unit,
    onBuyClick: () -> Unit
) {
    Scaffold { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(
                text = "课程详情",
                style = MaterialTheme.typography.headlineMedium
            )

            Text(
                text = product.name,
                style = MaterialTheme.typography.titleLarge
            )

            Text(text = "价格:${product.price}")

            Text(text = "这里是课程介绍,实际项目里可以放课程大纲、讲师信息和购买说明。")

            Button(
                onClick = onBuyClick
            ) {
                Text(text = "立即购买")
            }

            OutlinedButton(
                onClick = onBackClick
            ) {
                Text(text = "返回")
            }
        }
    }
}

@Composable
private fun CartScreen(
    onBackClick: () -> Unit,
    onCheckoutClick: () -> Unit
) {
    Scaffold { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(
                text = "购物车",
                style = MaterialTheme.typography.headlineMedium
            )

            Text(text = "购物车里有 2 门课程。")

            Button(
                onClick = onCheckoutClick
            ) {
                Text(text = "去结算")
            }

            OutlinedButton(
                onClick = onBackClick
            ) {
                Text(text = "返回")
            }
        }
    }
}

@Composable
private fun CheckoutScreen(
    fromCart: Boolean,
    onBackClick: () -> Unit
) {
    val sourceText = if (fromCart) {
        "你是从购物车进入结算页的。"
    } else {
        "你是从详情页直接购买进入结算页的。"
    }

    Scaffold { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(
                text = "结算页",
                style = MaterialTheme.typography.headlineMedium
            )

            Text(text = sourceText)

            Text(text = "这里可以填写订单信息、支付方式和优惠券。")

            OutlinedButton(
                onClick = onBackClick
            ) {
                Text(text = "返回")
            }
        }
    }
}

这份 Demo 的返回栈变化如下:

text 复制代码
打开 App:
[Home]

点击课程 1001:
[Home, Detail(1001)]

点击立即购买:
[Home, Detail(1001), Checkout(false)]

点击返回:
[Home, Detail(1001)]

再次点击返回:
[Home]

如果从购物车进入结算页:

text 复制代码
打开 App:
[Home]

点击购物车:
[Home, Cart]

点击去结算:
[Home, Cart, Checkout(true)]

实战知识点对应表

实战中的效果 使用的 API 对应章节
定义页面类型 sealed interface + NavKey
定义有参数页面 data class Detail(productId)
创建返回栈 rememberNavBackStack
展示当前页面 NavDisplay
注册不同页面 entryProvider + entry<T>
点击跳转详情和结算 backStack.add(...) 四、七
页面返回 if (backStack.size > 1) backStack.removeLastOrNull() 四、六、七
页面事件上提 onProductClickonBackClick

同一个结算页可以通过不同参数区分来源,不需要写两个 route,也不需要手动解析字符串。


八、Navigation 3 使用速查表

你想做什么 推荐写法
定义页面集合 sealed interface AppScreen : NavKey
定义无参数页面 @Serializable data object Home : AppScreen
定义有参数页面 @Serializable data class Detail(val id: Int) : AppScreen
创建返回栈 rememberNavBackStack(AppScreen.Home)
跳转页面 backStack.add(AppScreen.Detail(id))
返回上一页 if (backStack.size > 1) backStack.removeLastOrNull()
显示页面 NavDisplay(...)
注册页面内容 entry<AppScreen.Detail> { detail -> ... }
页面对外暴露事件 onClickonBackClickonBuyClick

九、总结

本篇你学到了 Compose Navigation 3 的核心用法:

  1. 页面建模 :用 NavKeysealed interface 把页面从字符串 route 变成类型安全的 Kotlin 对象
  2. 参数传递 :无参数页面用 data object,有参数页面用 data class
  3. 返回栈管理 :用 rememberNavBackStack() 保存页面栈,跳转就是 backStack.add(...),返回时注意不要弹掉根页面
  4. 页面展示 :用 NavDisplay 根据当前栈顶 Key 显示对应 Composable
  5. 页面注册 :用 entryProviderentry<T> 把不同页面 Key 映射到不同界面
  6. 事件上提 :页面只暴露 onClickonBackClick 这类事件,不直接持有导航对象

核心原则:Navigation 3 的导航不是拼 route 字符串,而是操作一组类型安全的页面 Key。页面越多,这种写法越能减少参数写错、route 拼错的问题。

下一篇我们将学习 ViewModel 与界面状态管理------如何把页面状态从 Composable 中拆出来,让加载中、加载失败、空状态和用户操作更好维护。


如果你在学习过程中有任何疑问,欢迎在评论区留言,我会尽可能回复。

系列文章:

相关推荐
rocpp4 小时前
Android 多语言切换实战:从 Context 到 Android 13 应用语言适配
android·kotlin
释然小师弟5 小时前
Android开发十年:反思与回顾
android·后端·嵌入式
黄林晴7 小时前
用了这么久 Koin Scope,原来一直都用错了?
android·kotlin
爱勇宝20 小时前
我做了一个只用来搜歌词的小 App
android·前端·后端
众少成多积小致巨1 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
Coffeeee1 天前
如何使用Glide和Coil加载WebP动图
android·kotlin·glide
Kapaseker1 天前
5 分钟搞懂 Kotlin DSL
android·kotlin
恋猫de小郭1 天前
AI Agent 开发究竟是啥?如何用 AI 开发 Agent ?深入浅出给你一套概念
android·前端·ai编程
黄林晴1 天前
Android 17 正式发布!target 37 一大批旧代码直接不能用了
android