Compose - 使用 Navigation3 正式版

一、概念

二、添加依赖

最新版本:Navigation3ViewModel自适应布局序列化

[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
public fun rememberNavBackStack(
    vararg elements: NavKey
): NavBackStack<NavKey>

会创建一个在配置更改和进程终止后仍能保留状态的回退栈。

//创建回退栈,填入默认显示的界面路径
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
public fun <T : Any> NavDisplay(
    backStack: List<T>,        //回退栈
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    onBack: () -> Unit = {        //返回键操作(基本上默认界面回退到桌面没有杀死APP,偶尔也会杀死)
        if (backStack is MutableList<T>) { backStack.removeLastOrNull() }
    },
    entryDecorators: List<NavEntryDecorator<T>> = listOf(rememberSaveableStateHolderNavEntryDecorator()),
    sceneStrategy: SceneStrategy<T> = SinglePaneSceneStrategy(),
    sizeTransform: SizeTransform? = null,

    //进场动画(默认 fadeIn、fadeOut)
    transitionSpec: AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform = defaultTransitionSpec(),

    //出场动画(默认 fadeIn、fadeOut)
    popTransitionSpec: AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform = defaultPopTransitionSpec(),

    //预测性返回手势弹出动画(默认 fadeIn、scaleOut)
    predictivePopTransitionSpec: AnimatedContentTransitionScope<Scene<T>>.(@NavigationEvent.SwipeEdge Int) -> ContentTransform = defaultPredictivePopTransitionSpec(),

    //构建导航图(将返回栈中的路径NavKey转换为目的地NavEntry)
    entryProvider: (key: T) -> NavEntry<T>,
)

这里设置的默认动画,都可以在 NavEntry 中通过元数据覆盖,从而定制每个界面自己的进出场动画。

5.1 构建导航图

将路径 NavKey 转换为目的地 NavEntry。目的地包含了界面可组合项、元数据。

5.1.1 通过 Lambda 构建

NavEntry

public class NavEntry<T : Any>(
    private val key: T,        //路径
    public val contentKey: Any = defaultContentKey(key),
    public val metadata: Map<String, 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) -> NavEntry<T> = {
        throw IllegalStateException("Unknown screen $it")
    },
    builder: EntryProviderScope<T>.() -> Unit,
): (T) -> NavEntry<T>

entry()

public inline fun <reified K : T> entry(
    noinline clazzContentKey: (key: @JvmSuppressWildcards K) -> Any = { defaultContentKey(it) },
     metadata: Map<String, Any> = emptyMap(),        //元数据
     noinline content: @Composable (K) -> Unit,        //界面可组合项
)

通过函数创建目的地。

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(
    public val targetContentEnter: EnterTransition,
    public val initialContentExit: ExitTransition,
    targetContentZIndex: Float = 0f,
    sizeTransform: SizeTransform? = SizeTransform(),
)

通过构造函数创建。

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
public fun <T : Any> rememberListDetailSceneStrategy(
    backNavigationBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange,
    directive: PaneScaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
    adaptStrategies: ThreePaneScaffoldAdaptStrategies = ListDetailPaneScaffoldDefaults.adaptStrategies(),
): ListDetailSceneStrategy<T>

辅助窗格策略

@Composable
public fun <T : Any> rememberSupportingPaneSceneStrategy(
    backNavigationBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilCurrentDestinationChange,
    directive: PaneScaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
    adaptStrategies: ThreePaneScaffoldAdaptStrategies = SupportingPaneScaffoldDefaults.adaptStrategies(),
): SupportingPaneSceneStrategy<T> 

举例列表详情策略显示效果:在【紧凑型】是 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 创建的。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值