Android Compose Navigation3 详解
一、什么是 Navigation3
Navigation3 是 Google 推出的新一代 Compose 导航库,相比 Navigation2(navigation-compose)更加 Compose-first,支持更灵活的多窗格布局(手机/平板/折叠屏自适应)。
与 Navigation2 对比
| 特性 | Navigation2 | Navigation3 |
|---|---|---|
| 设计理念 | 路由字符串 | 类型安全 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,操作直观,天然支持多窗格布局,非常适合折叠屏和平板适配场景。