一、概念
二、添加依赖
最新版本:Navigation3、ViewModel、自适应布局、序列化。
[versions]
navigation3 = "1.0.0"
navigation3Viewmodel = "2.10.0"
navigation3Adaptive = "1.3.0-alpha05"
kotlinxSerializationCore = "1.9.0"
[libraries]
navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
navigation3-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "navigation3Viewmodel" }
navigation3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive-navigation3", version.ref = "navigation3Adaptive" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
app 的 build.gradle
plugins {
alias(libs.plugins.kotlin.serialization)
}
android {
//需要设为36或更高
compileSdk = 36
//旧写法报错的话改成这样
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget("11"))
}
}
}
dependencies {
implementation(libs.navigation3.runtime)
implementation(libs.navigation3.ui)
implementation(libs.navigation3.viewmodel)
implementation(libs.kotlinx.serialization.core)
}
三、回退栈 NavBackStack
3.1 定义路径 NavKey
路径可以是任何类型但通常是简单的可序列化数据类。
- 使用密封接口方便调用和限制结构。
- 路径无参用 data object,有参用 data class。
- 添加 @Serializable 注解,使其可以序列化。
- 实现 NavKey 接口,作为可以被数据持久化的标记。
sealed interface ScreenRoute {
//无参
@Serializable data object Login : ScreenRoute, NavKey
//有参
@Serializable data class Profile( val id: Long) : ScreenRoute, NavKey
}
3.2 进行导航
回退栈使用可观察集合当做容器,当其中的元素发生变化时触发导航图 NavDisplay 重组来显示栈顶(尾部元素)界面。通过集合的方法 add() 跳转到下一个界面,removeLastOrNull() 回退到上一个界面(如果集合中没有元素了会返回null)。
| rememberNavBackStack() | @Composable 会创建一个在配置更改和进程终止后仍能保留状态的回退栈。 |
//创建回退栈,填入默认显示的界面路径
val backStack = rememberNavBackStack(ScreenRoute.Login)
//跳转到下一个界面
backStack.add(ScreenRoute.Profile(id = 888))
//回退到上一个界面
backStack.removeLastOrNull()
3.3 复刻回退栈模式
/**
* 跳转到下一个界面。
*/
fun NavBackStack<NavKey>.go(key: NavKey) {
add(key)
}
/**
* 栈顶复用:当前已经是要跳转的页面则弹出后再跳转,否则跳转到下一个界面。
*/
fun NavBackStack<NavKey>.goSingleTop(key: NavKey) {
if (key == last()) {
removeAt(lastIndex)
add(key)
} else {
add(key)
}
}
/**
* 栈内复用:要跳转的页面已存在于回退栈中则复用并清除在它之上的页面,否则跳转到下一个页面。
*/
fun NavBackStack<NavKey>.goSingleTask(key: NavKey) {
if (contains(key)) {
val index = indexOfFirst { it == key }
while (lastIndex != index) {
removeAt(lastIndex)
}
} else {
add(key)
}
}
/**
* 弹出当前页面后,再跳转到下一个页面。
*/
fun NavBackStack<NavKey>.popGo(key: NavKey) {
removeAt(lastIndex)
add(key)
}
五、显示容器 NavDisplay
会观察回退栈并显示栈顶的路径 key 对应的目的地 NavEntry。可通过 Lambda 或 DSL 两种方式来构建导航图。
| NavDisplay() | @Composable //进场动画(默认 fadeIn、fadeOut) //出场动画(默认 fadeIn、fadeOut) //预测性返回手势弹出动画(默认 fadeIn、scaleOut) //构建导航图(将返回栈中的路径NavKey转换为目的地NavEntry) 这里设置的默认动画,都可以在 NavEntry 中通过元数据覆盖,从而定制每个界面自己的进出场动画。 |
5.1 构建导航图
将路径 NavKey 转换为目的地 NavEntry。目的地包含了界面可组合项、元数据。
5.1.1 通过 Lambda 构建
| NavEntry | public class NavEntry<T : Any>( 通过构造函数创建目的地。 |
NavDisplay(
backStack = backStack
) { key ->
when (key) {
is ScreenRoute.Login -> NavEntry(key) { LoginScreen() }
is ScreenRoute.Profile -> NavEntry(key) { ProfileScreen(key.id) }
else -> NavEntry(key) {}
}
}
5.1.2 通过 DSL 构建(推荐)
| entryProvider() | public inline fun <T : Any> entryProvider( //没有提供该key对应NavEntry时的目的地,不推荐提供自定义的,遗漏了就是该抛出异常。 |
| entry() | public inline fun <reified K : T> entry( 通过函数创建目的地。 |
NavDisplay(
backStack = backStack,
entryProvider = entryProvider {
entry<ScreenRoute.Login> { LoginScreen() }
entry<ScreenRoute.Profile> { ProfileScreen(it.id) }
}
)
5.1.3 嵌套导航图
【父NavDisplay】的【NavEntry】提供【子NavDisplay】实现。【父NavDisplay】的回退栈只管理自己的界面跳转,【子NavDisplay】有自己的回退栈。
路径
- 节点(Auth、Home)不能定义成接口,因为回退栈添加的元素要是个实例。
sealed interface ScreenRoute {
@Serializable data object Auth : ScreenRoute, NavKey {
@Serializable data object Login : ScreenRoute, NavKey
@Serializable data object Register : ScreenRoute, NavKey
}
@Serializable data object Home : ScreenRoute, NavKey {
@Serializable data object List : ScreenRoute, NavKey
@Serializable data object Detail : ScreenRoute, NavKey
}
}
父 NavDisplay
@Composable
fun MainNavigation() {
val backStack = rememberNavBackStack(ScreenRoute.Auth)
NavDisplay(
backStack = backStack,
entryProvider = entryProvider {
entry<ScreenRoute.Auth> {
AuthNavigation {
backStack.remove(ScreenRoute.Auth) //登陆成功将Auth移除
backStack.add(ScreenRoute.Home)
}
}
entry<ScreenRoute.Home> { HomeNavigation() }
}
)
}
子 NavDisplay
@Composable
fun AuthNavigation(
onLoginSuccess: () -> Unit
) {
val backStack = rememberNavBackStack(ScreenRoute.Auth.Login)
NavDisplay(
backStack = backStack,
entryProvider = entryProvider {
entry<ScreenRoute.Auth.Login> {
LoginScreen(
onLoginSuccess = onLoginSuccess,
goRegister = { backStack.add(ScreenRoute.Auth.Register) }
)
}
entry<ScreenRoute.Auth.Register> { MyScreen("Auth.Register") }
}
)
}
@Composable
fun HomeNavigation() {
val backStack = rememberNavBackStack(ScreenRoute.Home.List)
NavDisplay(
backStack = backStack,
entryProvider = entryProvider {
entry<ScreenRoute.Home.List> {
ListScreen { backStack.add(ScreenRoute.Home.Detail) }
}
entry<ScreenRoute.Home.Detail> { MyScreen("Home.Detail") }
}
)
}
具体界面
@Composable
fun MyScreen(
screenName: String,
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) { Text(screenName) }
}
@Composable
fun LoginScreen(
onLoginSuccess: () -> Unit,
goRegister: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Auth.Login")
Button(onLoginSuccess) { Text("立即登录") }
Button(goRegister) { Text("跳转注册") }
}
}
@Composable
fun ListScreen(
goDetail: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Home.List")
Button(goDetail) { Text("跳转详情") }
}
}
5.2 进出场动画
详见进出场动画分类:EnterTransition、ExitTransition
通过 NavEntry 为界面设置的专用动画会覆盖掉 NavDisplay 设置的统一动画。
5.2.1 通过 NavDisplay 设置统一动画
| ContentTransform | public class ContentTransform( 通过构造函数创建。 |
| togetherWith | public infix fun EnterTransition.togetherWith(exit: ExitTransition): ContentTransform 通过操作符创建。 |
NavDisplay(
transitionSpec = {
ContentTransform(fadeIn(), fadeOut())
},
popTransitionSpec = {
fadeIn() togetherWith fadeOut()
},
)
5.2.2 通过 NavEntry 设置专用动画
NavEntry(
metadata = NavDisplay.transitionSpec {
ContentTransform(fadeIn(), fadeOut())
} + NavDisplay.popTransitionSpec {
fadeIn() togetherWith fadeOut()
}
)
entry<ScreenRoute.Login>(
metadata = NavDisplay.transitionSpec {
ContentTransform(fadeIn(), fadeOut())
} + NavDisplay.popTransitionSpec {
fadeIn() togetherWith fadeOut()
}
)
5.3 ViewModel 生命周期
5.3.1 限定在目的地
默认情况下,ViewModel 的范围为最近的 ViewMOdelStoreOwner(通常是Activity/Fragment)。由于是单 Activity 开发,创建的所有 ViewModel 生命周期都会跟 APP 一样久。
NavEntryDecorator 为每个 NavEntry 提供一个 ViewModelStoreOwner,在 Compose 中通过 viewModel() 方法创建时,会将范围限定在目的地,随着界面的跳转而创建,弹出而销毁,处于回退栈中会保留状态。
NavDisplay(
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
)
)
5.3.2 多个目的地间共享
NavDisplay(
backStack = backStack,
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator() //VM生命周期随所属目的地
),
entryProvider = entryProvider {
val sharedVM = viewModel<SharedVM>() //共享的VM
entry<ScreenRoute.Auth> {
AuthScreen(
authVM = viewModel(), //自己专用的VM
sharedVM = sharedVM //传入共享的VM
)
}
entry<ScreenRoute.Home> {
HomeScreen(
homeVM = viewModel(),
sharedVM = sharedVM
)
}
}
)
5.4 自适应布局 SceneStrategy
详见:自适应布局
默认情况下,一屏只显示一个界面,通过调整策略来支持自适应布局。
| 列表详情策略 | @Composable |
| 辅助窗格策略 | @Composable |
举例列表详情策略显示效果:在【紧凑型】是 ListScreen 点击后跳转到 DetailScreen,DetailSceen 点击后跳转到 ExtraScreen,按返回键会依次返回。在【中等型】会显示两个窗格。首先是左边显示 ListScreen 右边显示占位界面,ListScreen 点击后右边显示 DetailScreen,DetailScreen点击后跑到界面左边,右边显示 ExtraScreen,按返回键变成左边显示 ListScreen 右边显示 DetailScreen,再按返回退到桌面。
@Composable
fun NavScreen() {
val backStack = rememberNavBackStack(ListDetailScreenRoute.List)
val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() //列表详情策略
NavDisplay(
backStack = backStack,
sceneStrategy = listDetailStrategy,
entryProvider = entryProvider {
entry<ListDetailScreenRoute.List>(
metadata = ListDetailSceneStrategy.listPane { //指定为列表界面,可选提供占位界面
Text("未点击列表时,详情页占位界面")
}
) {
ListScreen { backStack.add(ListDetailScreenRoute.Detail(888)) }
}
entry<ListDetailScreenRoute.Detail>(
metadata = ListDetailSceneStrategy.detailPane() //指定为详情界面
) {
DetailScreen(it.id) { backStack.add(ListDetailScreenRoute.Extra) }
}
entry<ListDetailScreenRoute.Extra>(
metadata = ListDetailSceneStrategy.extraPane() //指定为额外界面
) {
ExtraScreen()
}
}
)
}
路径
sealed interface ListDetailScreenRoute {
@Serializable data object List : NavKey, ListDetailScreenRoute
@Serializable data class Detail(val id: Long) : NavKey, ListDetailScreenRoute
@Serializable data object Extra : NavKey, ListDetailScreenRoute
}
三个界面
@Composable
fun ListScreen(
onClick: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
contentAlignment = Alignment.Center
) {
Button(onClick) { Text("点击跳转到 Detail") }
}
}
@Composable
fun DetailScreen(
id: Long,
onClick: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Column {
Text("详情页,id:$id")
Button(onClick) { Text("点击跳转到 Extra") }
}
}
}
@Composable
fun ExtraScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Green),
contentAlignment = Alignment.Center
) { Text("额外的内容") }
}
六、装饰器 NavEntryDecorator
默认装饰器 SaveableStateHolderNavEntryDecorator 可让 NavEntry 的状态在配置更改和进程终止后得以保留。它使用 SaveableStateProvider 封装 NavEntry 内容,使 NavEntry 内容中的 rememberSaveable 调用能够正常运行。
除非你的装饰器提供 SaveableStateProvider,否则应在提供的装饰器列表中包含 SaveableStateHolderNavEntryDecorator 作为第一个装饰器。它是使用 rememberSaveableStateHolderNavEntryDecorator 创建的。
8383

被折叠的 条评论
为什么被折叠?



