Android Compose Navigation3 详解

0 阅读3分钟

Android Compose Navigation3 详解


一、什么是 Navigation3

Navigation3 是 Google 推出的新一代 Compose 导航库,相比 Navigation2(navigation-compose)更加 Compose-first,支持更灵活的多窗格布局(手机/平板/折叠屏自适应)。

与 Navigation2 对比

特性Navigation2Navigation3
设计理念路由字符串类型安全 Key
多窗格需手动实现原生支持
返回栈NavController 管理普通 List 管理
动画有限支持完全可定制
状态管理ViewModel 绑定更灵活

二、依赖配置

// build.gradle
dependencies {
    implementation "androidx.navigation3:navigation3-ui:1.0.0-alpha01"
    implementation "androidx.lifecycle:lifecycle-viewmodel-navigation3:1.0.0-alpha01"

    // 可选:Material3 适配
    implementation "androidx.compose.material3:material3-adaptive-navigation-suite:1.0.0"
}

三、核心接口与概念

1. NavEntry(导航条目)

// 每个页面对应一个 NavEntry
class NavEntry<T : Any>(
    val key: T,                          // 唯一标识
    val metadata: NavEntryMetadata,      // 元数据(动画等)
    val content: @Composable () -> Unit  // 页面内容
)

2. NavDisplay(导航容器)

@Composable
fun NavDisplay(
    backStack: List<Any>,                // 返回栈
    onBack: () -> Unit,                  // 返回回调
    entryProvider: EntryProvider,        // 页面提供者
    // 可选参数
    transitionSpec: ...,                 // 转场动画
    popTransitionSpec: ...,              // 返回动画
)

3. EntryProvider(页面提供者)

// 通过 entryProvider DSL 注册页面
val provider = entryProvider {
    entry<HomeKey> { key ->
        HomeScreen(key)
    }
    entry<DetailKey> { key ->
        DetailScreen(key)
    }
}

4. NavBackStack(返回栈)

// 返回栈本质是一个普通 List
val backStack = remember { mutableStateListOf<Any>(HomeKey) }

// 导航到新页面
backStack.add(DetailKey(id = 1))

// 返回
backStack.removeLastOrNull()

// 返回到指定页面
backStack.removeAll { it is DetailKey }

四、基础用法

1. 定义路由 Key

// 使用 data class / data object 定义 Key
data object HomeKey
data object SettingsKey
data class DetailKey(val id: Int)
data class ProfileKey(val userId: String)

2. 完整基础示例

@Composable
fun App() {
    // 1. 初始化返回栈
    val backStack = remember { mutableStateListOf<Any>(HomeKey) }

    // 2. 定义页面提供者
    val entryProvider = entryProvider {
        entry<HomeKey> {
            HomeScreen(
                onNavigateToDetail = { id ->
                    backStack.add(DetailKey(id))
                },
                onNavigateToSettings = {
                    backStack.add(SettingsKey)
                }
            )
        }
        entry<DetailKey> { key ->
            DetailScreen(
                id = key.id,
                onBack = { backStack.removeLastOrNull() }
            )
        }
        entry<SettingsKey> {
            SettingsScreen(
                onBack = { backStack.removeLastOrNull() }
            )
        }
    }

    // 3. 渲染导航容器
    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = entryProvider
    )
}

五、多窗格布局(核心特性)

1. 自适应双窗格

@Composable
fun AdaptiveApp() {
    val backStack = remember { mutableStateListOf<Any>(HomeKey) }
    val windowInfo = currentWindowAdaptiveInfo()

    // 判断是否宽屏
    val isWideScreen = windowInfo.windowSizeClass.windowWidthSizeClass ==
        WindowWidthSizeClass.EXPANDED

    if (isWideScreen) {
        // 宽屏:双窗格
        TwoPaneNavDisplay(backStack)
    } else {
        // 窄屏:单窗格
        SinglePaneNavDisplay(backStack)
    }
}

@Composable
fun TwoPaneNavDisplay(backStack: SnapshotStateList<Any>) {
    Row(modifier = Modifier.fillMaxSize()) {
        // 左侧列表
        Box(modifier = Modifier.weight(1f)) {
            NavDisplay(
                backStack = listOf(HomeKey),
                onBack = {},
                entryProvider = entryProvider {
                    entry<HomeKey> {
                        HomeListScreen(
                            onSelect = { id -> backStack.add(DetailKey(id)) }
                        )
                    }
                }
            )
        }

        // 右侧详情
        Box(modifier = Modifier.weight(2f)) {
            val detailKey = backStack.filterIsInstance<DetailKey>().lastOrNull()
            if (detailKey != null) {
                DetailScreen(id = detailKey.id)
            } else {
                EmptyDetailPlaceholder()
            }
        }
    }
}

2. 使用 NavEntryMetadata 控制窗格

entryProvider {
    entry<HomeKey>(
        metadata = NavEntryMetadata(
            pane = NavPane.SINGLE  // 单窗格显示
        )
    ) {
        HomeScreen()
    }

    entry<DetailKey>(
        metadata = NavEntryMetadata(
            pane = NavPane.DETAIL  // 详情窗格
        )
    ) { key ->
        DetailScreen(key.id)
    }
}

六、转场动画

1. 自定义进入/退出动画

NavDisplay(
    backStack = backStack,
    onBack = { backStack.removeLastOrNull() },
    entryProvider = entryProvider,

    // 前进动画
    transitionSpec = {
        slideInHorizontally { it } togetherWith slideOutHorizontally { -it }
    },

    // 返回动画
    popTransitionSpec = {
        slideInHorizontally { -it } togetherWith slideOutHorizontally { it }
    }
)

2. 淡入淡出

transitionSpec = {
    fadeIn(tween(300)) togetherWith fadeOut(tween(300))
}

3. 页面级别动画

// 特定页面使用不同动画
entryProvider {
    entry<DialogKey>(
        metadata = NavEntryMetadata(
            transitionSpec = {
                scaleIn(initialScale = 0.8f) + fadeIn() togetherWith
                scaleOut(targetScale = 0.8f) + fadeOut()
            }
        )
    ) {
        DialogScreen()
    }
}

七、ViewModel 集成

// 1. 定义 ViewModel
class DetailViewModel(val id: Int) : ViewModel() {
    val data = MutableStateFlow<DetailData?>(null)

    init {
        loadData(id)
    }
}

// 2. ViewModel Factory
class DetailViewModelFactory(private val id: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return DetailViewModel(id) as T
    }
}

// 3. 在 NavEntry 中使用
entryProvider {
    entry<DetailKey> { key ->
        // ViewModel 与 NavEntry 生命周期绑定
        val viewModel: DetailViewModel = viewModel(
            factory = DetailViewModelFactory(key.id)
        )
        DetailScreen(viewModel = viewModel)
    }
}

八、深层链接处理

@Composable
fun App() {
    val backStack = remember { mutableStateListOf<Any>(HomeKey) }

    // 处理 Intent 深层链接
    val intent = LocalContext.current as? Activity
    LaunchedEffect(intent) {
        intent?.intent?.data?.let { uri ->
            when {
                uri.path?.startsWith("/detail/") == true -> {
                    val id = uri.lastPathSegment?.toIntOrNull()
                    if (id != null) {
                        backStack.clear()
                        backStack.add(HomeKey)
                        backStack.add(DetailKey(id))
                    }
                }
            }
        }
    }

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = entryProvider { /* ... */ }
    )
}

九、底部导航栏集成

// 定义底部导航 Tab
sealed class BottomTab {
    data object Home : BottomTab()
    data object Search : BottomTab()
    data object Profile : BottomTab()
}

@Composable
fun MainScreen() {
    var selectedTab by remember { mutableStateOf<BottomTab>(BottomTab.Home) }

    // 每个 Tab 独立的返回栈
    val homeStack   = remember { mutableStateListOf<Any>(HomeKey) }
    val searchStack = remember { mutableStateListOf<Any>(SearchKey) }
    val profileStack = remember { mutableStateListOf<Any>(ProfileKey) }

    Scaffold(
        bottomBar = {
            NavigationBar {
                NavigationBarItem(
                    selected = selectedTab is BottomTab.Home,
                    onClick = { selectedTab = BottomTab.Home },
                    icon = { Icon(Icons.Default.Home, null) },
                    label = { Text("首页") }
                )
                NavigationBarItem(
                    selected = selectedTab is BottomTab.Search,
                    onClick = { selectedTab = BottomTab.Search },
                    icon = { Icon(Icons.Default.Search, null) },
                    label = { Text("搜索") }
                )
                NavigationBarItem(
                    selected = selectedTab is BottomTab.Profile,
                    onClick = { selectedTab = BottomTab.Profile },
                    icon = { Icon(Icons.Default.Person, null) },
                    label = { Text("我的") }
                )
            }
        }
    ) { padding ->
        Box(modifier = Modifier.padding(padding)) {
            // 根据 Tab 切换返回栈
            val currentStack = when (selectedTab) {
                BottomTab.Home    -> homeStack
                BottomTab.Search  -> searchStack
                BottomTab.Profile -> profileStack
            }

            NavDisplay(
                backStack = currentStack,
                onBack = { currentStack.removeLastOrNull() },
                entryProvider = entryProvider { /* ... */ }
            )
        }
    }
}

十、使用场景总结

场景方案
普通页面跳转backStack.add(Key)
返回上一页backStack.removeLastOrNull()
返回到根页面backStack.removeAll { it !is HomeKey }
替换当前页面backStack[backStack.lastIndex] = NewKey
手机/平板自适应WindowSizeClass + 条件渲染
底部导航多个独立 backStack
深层链接LaunchedEffect 处理 Intent
对话框/弹窗独立 NavEntry + 缩放动画

十一、注意事项

⚠️ API 可能变动
⚠️ 不建议在生产项目中直接使用
⚠️ 与 Navigation2 不兼容,无法混用
✅ 新项目可以尝试,关注官方更新


总结

Navigation3 最大的优势是返回栈即普通 List,操作直观,天然支持多窗格布局,非常适合折叠屏和平板适配场景。