官方页面一、概念二、添加依赖最新版本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.gradleplugins { 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.navigation3.adaptive) implementation(libs.kotlinx.serialization.core) }三、回退栈 NavBackStack3.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()Composablepublic fun rememberNavBackStack(vararg elements: NavKey): NavBackStackNavKey会创建一个在配置更改和进程终止后仍能保留状态的回退栈。//创建回退栈填入默认显示的界面路径 val backStack rememberNavBackStack(ScreenRoute.Login) //跳转到下一个界面 backStack.add(ScreenRoute.Profile(id 888)) //回退到上一个界面 backStack.removeLastOrNull()3.3 复刻回退栈模式/** * 跳转跳转到下一个界面。 */ fun NavBackStackNavKey.go(key: NavKey) { add(key) } /** * 栈顶复用当前已经是要跳转的页面则无视否则跳转到下一个界面。 */ fun NavBackStackNavKey.goSingleTop(key: NavKey) { if (key ! last()) { add(key) } } /** * 刷新栈顶复用当前已经是要跳转的页面则移除后再添加否则跳转到下一个界面。 */ fun NavBackStackNavKey.goSingleTopButRefresh(key: NavKey) { if (key last()) { removeAt(lastIndex) add(key) } else { add(key) } } /** * 栈内复用要跳转的页面已存在于回退栈中则复用并清除在它之上的页面否则跳转到下一个页面。 */ fun NavBackStackNavKey.goSingleTask(key: NavKey) { if (contains(key)) { val index indexOfFirst { it key } while (lastIndex ! index) { removeAt(lastIndex) } } else { add(key) } } /** * 先栈内复用再跳转先栈内复用到指定页面再跳转到下一个页面。 * 场景举例1ListDetail手机横屏为 Expanded点击不同List条目旋转回Compact此时是detail界面按返回键发现回退栈有很多detail界面期待的是直接返回list界面。 * 场景举例2播放器页面点击专辑/歌手跳转后返回直接退到分类首页。 * param goSingleTaskKey 先跳转的位置 * param goKey 后跳转的界面 */ fun NavBackStackNavKey.goSingleTopAfterGoSingleTask(goKey: NavKey, goSingleTaskKey:NavKey) { if (contains(goSingleTaskKey)) { val index indexOfFirst { it goSingleTaskKey } while (lastIndex ! index) { removeAt(lastIndex) } } else { add(goSingleTaskKey) } add(goKey) } /** * 弹出当前再跳转先弹出当前页面再跳转到下一个页面。 */ fun NavBackStackNavKey.goAfterPop(key: NavKey) { removeAt(lastIndex) add(key) } /** * 先跳转弹出再跳转先跳转到指定界面第一次出现的位置并弹出再跳转到下一个界面。 * param popKey 先弹出并跳转的位置 * param goKey 后跳转的界面 */ fun NavBackStackNavKey.goAfterPop(goKey: NavKey, popKey: NavKey) { if (contains(popKey)) { val index indexOfFirst { it popKey } while (lastIndex index) { removeAt(lastIndex) } add(goKey) } else { add(goKey) } } /** * 先清空所有再跳转先清空回退栈中所有界面再跳转到下一个页面。 */ fun NavBackStackNavKey.goAfterClear(key: NavKey) { clear() add(key) } /** * 返回返回上一个界面。 */ fun NavBackStackNavKey.back() { removeLastOrNull() }五、显示容器 NavDisplay会观察回退栈并显示栈顶的路径 key 对应的目的地 NavEntry。可通过 Lambda 或 DSL 两种方式来构建导航图。NavDisplay()Composablepublic fun T : Any NavDisplay(backStack: ListT, //回退栈modifier: Modifier Modifier,contentAlignment: Alignment Alignment.TopStart,onBack: () - Unit { //返回键操作基本上默认界面回退到桌面没有杀死APP偶尔也会杀死if (backStack is MutableListT) { backStack.removeLastOrNull() }},entryDecorators: ListNavEntryDecoratorT listOf(rememberSaveableStateHolderNavEntryDecorator()),sceneStrategy: SceneStrategyT SinglePaneSceneStrategy(),sizeTransform: SizeTransform? null,//跳转动画默认 fadeIn、fadeOuttransitionSpec: AnimatedContentTransitionScopeSceneT.() - ContentTransform defaultTransitionSpec(),//返回动画默认 fadeIn、fadeOutpopTransitionSpec: AnimatedContentTransitionScopeSceneT.() - ContentTransform defaultPopTransitionSpec(),//预测性返回手势弹出动画默认 fadeIn、scaleOutpredictivePopTransitionSpec: AnimatedContentTransitionScopeSceneT.(NavigationEvent.SwipeEdge Int) - ContentTransform defaultPredictivePopTransitionSpec(),//构建导航图将返回栈中的路径NavKey转换为目的地NavEntryentryProvider: (key: T) - NavEntryT,)这里设置的默认动画都可以在 NavEntry 中通过元数据覆盖从而定制每个界面自己的进出场动画。5.1 构建导航图将路径 NavKey 转换为目的地 NavEntry。目的地包含了界面可组合项、元数据。5.1.1 通过 Lambda 构建NavEntrypublic class NavEntryT : Any(private val key: T, //路径public val contentKey: Any defaultContentKey(key),public val metadata: MapString, Any emptyMap(), //元数据private val content: Composable (T) - Unit, //界面可组合项)通过构造函数创建目的地。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时的目的地不推荐提供自定义的遗漏了就是该抛出异常。noinline fallback: (unknownScreen: T) - NavEntryT {throw IllegalStateException(Unknown screen $it)},builder: EntryProviderScopeT.() - Unit,): (T) - NavEntryTentry()public inline fun reified K : T entry(noinline clazzContentKey: (key: JvmSuppressWildcards K) - Any { defaultContentKey(it) },metadata: MapString, Any emptyMap(), //元数据noinline content: Composable (K) - Unit, //界面可组合项)通过函数创建目的地。NavDisplay( backStack backStack, entryProvider entryProvider { entryScreenRoute.Login { LoginScreen() } entryScreenRoute.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 } }父 NavDisplayComposable fun MainNavigation() { val backStack rememberNavBackStack(ScreenRoute.Auth) NavDisplay( backStack backStack, entryProvider entryProvider { entryScreenRoute.Auth { AuthScreen { backStack.remove(ScreenRoute.Auth) //登陆成功将Auth移除 backStack.add(ScreenRoute.Home) } } entryScreenRoute.Home { HomeScreen() } } ) }子 NavDisplayComposable fun AuthScreen( onLoginSuccess: () - Unit ) { val backStack rememberNavBackStack(ScreenRoute.Auth.Login) NavDisplay( backStack backStack, entryProvider entryProvider { entryScreenRoute.Auth.Login { LoginScreen( onLoginSuccess onLoginSuccess, goRegister { backStack.add(ScreenRoute.Auth.Register) } ) } entryScreenRoute.Auth.Register { MyScreen(Auth.Register) } } ) } Composable fun HomeScreen() { val backStack rememberNavBackStack(ScreenRoute.Home.List) NavDisplay( backStack backStack, entryProvider entryProvider { entryScreenRoute.Home.List { ListScreen { backStack.add(ScreenRoute.Home.Detail) } } entryScreenRoute.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 进出场动画 ContentTransform详见进出场动画分类EnterTransition、ExitTransition通过 NavEntry 为界面设置的专用动画会覆盖掉 NavDisplay 设置的统一动画。ContentTransformpublic class ContentTransform(public val targetContentEnter: EnterTransition,public val initialContentExit: ExitTransition,targetContentZIndex: Float 0f,sizeTransform: SizeTransform? SizeTransform(),)通过构造函数创建。togetherWithpublic infix fun EnterTransition.togetherWith(exit: ExitTransition): ContentTransform通过操作符创建。5.2.1 通过 NavDisplay 设置统一动画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() } )entryScreenRoute.Login( metadata NavDisplay.transitionSpec { ContentTransform(fadeIn(), fadeOut()) } NavDisplay.popTransitionSpec { fadeIn() togetherWith fadeOut() } )5.2.3 采用滑入滑出动画的问题建议问题一Slide动画默认效果是反习惯的如进入动画默认从左向右推入而习惯上从右向左推入。默认效果是从界面的一半开始推入推出而推出一半再消失很违和完整推入推出视觉更流畅。偏移直接返回 it 解决。问题二跳转transitionSpec返回popTransitionSpec两个都包含了进出场动画ContentTransform。不再是之前那样只对当前界面设置动画多了强制对另一个页面的设置。只设置 ContentTransform 中对应导航动作的动画另一个用 NONE。预测返回 predictivePopTransitionSpec 需要指定动画的话同普通返回 popTransitionSpec 设置一样的val slideInAnimation ContentTransform( slideInHorizontally(animationSpec tween(500), initialOffsetX {it}), ExitTransition.None ) val slideOutAnimation ContentTransform( EnterTransition.None, slideOutHorizontally(animationSpec tween(500), targetOffsetX {it}) ) NavDisplay( transitionSpec { slideInAnimation }, popTransitionSpec { slideOutAnimation } )封装参数 isIn 用来区分是跳转transitionSpec还是返回popTransitionSpec 和 predictivePopTransitionSpec。object NavUtils { fun slideAnimation(isIn: Boolean, durationMs: Int 500) ContentTransform( targetContentEnter if (isIn) slideInHorizontally(animationSpec tween(durationMs), initialOffsetX {it}) else EnterTransition.None, initialContentExit if (isIn) ExitTransition.None else slideOutHorizontally(animationSpec tween(durationMs), targetOffsetX {it}) ) fun fadeAnimation(isIn: Boolean, durationMs: Int) ContentTransform( targetContentEnter if (isIn) fadeIn(animationSpec tween(durationMs)) else EnterTransition.None, initialContentExit if (isIn) ExitTransition.None else fadeOut(animationSpec tween(durationMs)) ) fun noAnimation() ContentTransform( targetContentEnter EnterTransition.None, initialContentExit ExitTransition.None ) fun entryFadeAnimation(durationMs: Int 500): MapString, Any { return NavDisplay.transitionSpec { fadeAnimation(true, durationMs) } NavDisplay.popTransitionSpec { fadeAnimation(false, durationMs) } NavDisplay.predictivePopTransitionSpec { fadeAnimation(false, durationMs) } } fun entrySlideAnimation(durationMs: Int 500): MapString, Any { return NavDisplay.transitionSpec { slideAnimation(true, durationMs) } NavDisplay.popTransitionSpec { slideAnimation(false, durationMs) } NavDisplay.predictivePopTransitionSpec { slideAnimation(false, durationMs) } } fun entryNoAnimation(): MapString, Any { return NavDisplay.transitionSpec { noAnimation() } NavDisplay.popTransitionSpec { noAnimation() } NavDisplay.predictivePopTransitionSpec { noAnimation() } } }使用NavDisplay( backStack backStack, transitionSpec { NavUtils.slideAnimation(isIn true) }, popTransitionSpec { NavUtils.slideAnimation(isIn false) }, predictivePopTransitionSpec { NavUtils.slideAnimation(isIn false) }, entryProvider entryProvider { entryScreenRoute.List( metadata NavUtils.entryNoAnimation() ) { HomeScreen() } entryScreenRoute.Detail( metadata NavUtils.entrySlideAnimation(1000) ) { HomeScreen() } } }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 viewModelSharedVM() //共享的VM entryScreenRoute.Auth { AuthScreen( authVM viewModel(), //自己专用的VM sharedVM sharedVM //传入共享的VM ) } entryScreenRoute.Home { HomeScreen( homeVM viewModel(), sharedVM sharedVM ) } } )5.4 自适应布局 SceneStrategy详见自适应布局默认情况下一屏只显示一个界面通过调整策略来支持自适应布局。列表详情策略Composablepublic fun T : Any rememberListDetailSceneStrategy(backNavigationBehavior: BackNavigationBehavior BackNavigationBehavior.PopUntilScaffoldValueChange,directive: PaneScaffoldDirective calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),adaptStrategies: ThreePaneScaffoldAdaptStrategies ListDetailPaneScaffoldDefaults.adaptStrategies(),): ListDetailSceneStrategyT辅助窗格策略Composablepublic fun T : Any rememberSupportingPaneSceneStrategy(backNavigationBehavior: BackNavigationBehavior BackNavigationBehavior.PopUntilCurrentDestinationChange,directive: PaneScaffoldDirective calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),adaptStrategies: ThreePaneScaffoldAdaptStrategies SupportingPaneScaffoldDefaults.adaptStrategies(),): SupportingPaneSceneStrategyT举例列表详情策略显示效果在【紧凑型】是 ListScreen 点击后跳转到 DetailScreenDetailSceen 点击后跳转到 ExtraScreen按返回键会依次返回。在【中等型】会显示两个窗格。首先是左边显示 ListScreen 右边显示占位界面ListScreen 点击后右边显示 DetailScreenDetailScreen点击后跑到界面左边右边显示 ExtraScreen按返回键变成左边显示 ListScreen 右边显示 DetailScreen再按返回退到桌面。Composable fun NavScreen() { val backStack rememberNavBackStack(ListDetailScreenRoute.List) val listDetailStrategy rememberListDetailSceneStrategyNavKey() //列表详情策略 NavDisplay( backStack backStack, sceneStrategy listDetailStrategy, entryProvider entryProvider { entryListDetailScreenRoute.List( metadata ListDetailSceneStrategy.listPane { //指定为列表界面可选提供占位界面 Text(未点击列表时详情页占位界面) } ) { ListScreen { backStack.add(ListDetailScreenRoute.Detail(888)) } } entryListDetailScreenRoute.Detail( metadata ListDetailSceneStrategy.detailPane() //指定为详情界面 ) { DetailScreen(it.id) { backStack.add(ListDetailScreenRoute.Extra) } } entryListDetailScreenRoute.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 创建的。