Compose Navigation总结

1.Navigation

Compose本身缺乏类似Fragment的页面导航功能,而Navigation库为此提供了关键支持。 它既像一个粘合剂,将高内聚的页面以低耦合的方式组织起来;又像一个管家,负责管理导航页面的状态和对应的回退栈。

2.回退栈

  • Navigation组件内部维护一个目的地堆栈,堆栈的底部为起始目的地,导航到一个新的目的地时会将该目的地推送到堆栈的顶部,这样便形成了一个回退栈。
  • 回退栈里的元素类型是NavBackStackEntry,它不仅存储导航中携带的参数,也是代表了这个目的地的生命周期状态。
  • 页面导航的过程中,提供了操作栈内元素的方法,比如哪些元素出栈之后,再压入新的元素到栈顶。这有点类似于Activity的任务栈。
  • 当点击系统自带的NavigationBar的返回按钮时,堆栈里的目的地就会依次弹出,最后会显示初始目的地,再次点击返回按钮时,退出应用。

3.Navigation组件

Navigation主要包含以下几个组件:

包含导航目的地的界面元素,当用户浏览应用时,会在导航宿主中切换目的地。 NavHost在构建的时候,会构建出NavGraph

一种数据和行为的关系图,定义应用中的所有导航目的地以及它们如何连接在一起,是导航的蓝图。

管理目的地之间导航的状态、处理深层链接、管理返回栈等。

导航图中的节点数据,代表应用中一个内容屏幕,当用户导航到此节点时,会显示此目的地的内容。

3.5 路线 route

任何可序列化的数据类型,唯一标识目的地及其所需的任何数据。使用路线导航,并前往目的地。

4. 使用Navigation

在使用 Jetpack Compose 时创建 NavController,请调用 rememberNavController()

kotlin 复制代码
val navController = rememberNavController()

一般是在顶级的Composable函数中创建NavController,这样所有组件才能引用它。

在Compose中,使用NavHost可组合项,在构建NavHost的过程中,就直接构建了导航图,这是推荐的方式。 说白了,就是在NavHost的lambda表达式里,直接定义每个页面的标签,以及对应的跳转逻辑,如下:

kotlin 复制代码
NavHost(navController = navController, "ChatList") {
        composable (route = "ChatList"){
            ChatListPage {
                navController.navigate("ChatDetail")
            }
        }
        composable(route = "ChatDetail") {
            ChatDetailPage()
        }
    }

通过compoasable函数去定义了两个页面,一个是聊天列表页ChatListPage(),一个是具体某个人的聊天详情页*ChatDetailPage()。在ChatListPage后面跟了一个Lambda表达式,在ChatListPage内部的列表item点击的时候,会调用这个lambda表达式,并导航到ChatDetailPage页面。

route参数就是这个页面唯一的标识,也叫做路线,使用可序列化对象或者类定义路线。我这里使用的是字符串(可序列化)作为路线,优点是实现简单,但不太利于传参。

我们做一下修改,使用可序列化的 类和对象 定义路线:

kotlin 复制代码
@Serializable
object ChatListRoute

@Serializable
data class ChatDetailRoute(val name : String)  //这里定义一个参数,在导航跳转的时候传递

这里要注意,实现序列化需要引入Kotlin序列化插件(Kotlin serialization plugin),以及json序列化库依赖(JSON serialization library),如下:

kotlin 复制代码
[versions]
kotlin = "2.2.10"
navigation = "2.9.4"
serialization = "1.9.0"

[libraries]
navigation = {group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation"}
serialization = {group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization"}

[plugins]
kotlin-serialization = {id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}

在Activity中构建NavGraph如下:

kotlin 复制代码
NavHost(navController = navController, ChatListRoute) {
        composable<ChatListRoute> {
            ChatListPage {
                navController.navigate(ChatDetailRoute(it)) // 注释1
            }
        }
        composable<ChatDetailRoute>{
            ChatDetailPage(it.toRoute<ChatDetailRoute>().name) //注释2 
        }
    }

在代码注释1处,通过 lambda表达式传出的数据给到ChatDetailRoute,这里的it是点击的对话列表Item对应的联系人名字。当导航跳转到对应目的地时,这个参数也会带过去。

在代码注释2处,导航跳转到目的地之后,就把ChatDetailRoute携带的数据取出来,给到目的地页面显示。每当用户导航到一个新的目的地 (Destination) 时,Navigation 组件会在后退栈的顶部创建一个新的 NavBackStackEntry 实例来表示这个刚刚访问的目标。

4.3 导航触发

我们通过naviController的navigation方法触发导航,参数是目的地页面的路线。 默认情况下,navigation的行为是在回退栈压入一个新的Composable的Destination,然后作为栈顶节点进行显示。但navigation方法也提供了一个lambda,可以追加NavOptions的操作。

  • 比如,清空栈顶节点到HomeRoute之间的所有节点(不包含HomeRoute),然后 再入栈ChatDetailRoute。当从聊天详情页返回的时候,直接回到主页。可如下实现:
kotlin 复制代码
navController.navigate(ChatDetailRoute(it)){
                    popUpTo(HomeRoute)
                }
  • 再看看,清空栈顶节点到WelcomeRoute之间的所有节点(包含WelcomeRoute),然后 再入栈HomeRoute。通过欢迎页进入到主页之后,从主页再回退时,不需要再进入欢迎页,直接退出应用。下面的代码:
kotlin 复制代码
navController.navigate(HomeRoute){
                    popUpTo(WelcomeRoute){ inclusive = true}
                }
  • 当栈顶节点已经是Home页面时,不会重新入栈新的Home节点,相当于Activity的SingleTop的launchMode,如下:
kotlin 复制代码
navController.navigate(HomeRoute){
                    launchSingleTop = true
                }

4.4 页面内容

我们看看ChatListPage最简单的内容代码:

kotlin 复制代码
@Composable
fun ChatListPage(onclick : (String) -> Unit) {
LazyColumn{
            itemsIndexed(items = chatlists) { index, chat ->
		...
                ChatItem(chat, Modifier.clickable{onclick(chat)})
            }
        }
}

@Composable
fun ChatItem(name : String, modifier: Modifier) {
    Text(name, modifier
        .fillMaxSize()
        .background(Color.White))
}

这里最重要的就是把点击事件处理的逻辑,通过高阶函数的方式抽象出来,并把具体的处理逻辑交给调用者实现。这是一种典型的"状态上提"策略。

大型的项目会以组件化的方式开发,会涉及多个Module模块,每个模块相互独立,而且每个模块中也有自己的导航逻辑,即也存在一个NavGraph。

当App Module依赖各个Lib Module的时候,可以借助Nested Navitation Graph机制,App Moddule中定义root Graph,而各个模块可以作为其子 Graph存在并关联。

以账号登陆逻辑为例: 大多数应用都有一个用户认证流程,可能包含登录、注册、忘记密码、手机验证等多个步骤。这个流程通常是自包含的,用户完成或取消后会返回到应用的主流程。

使用嵌套导航图的优点:

  • 封装性: 登录/注册相关的屏幕(如登录页、注册页、验证码页)可以被组织在一个单独的嵌套图中。主导航图只需要知道如何导航到这个"登录流程图",而不需要关心其内部的具体页面。
  • 可重用性: 如果应用的多个地方都需要触发登录(例如,在未登录时尝试访问受限功能),可以直接导航到这个嵌套的登录流程图。
  • 可读性: 主导航图会变得更简洁,只需指向"登录流程"子导航图的入口,保持了主导航图的整体架构。

如下是实现的部分代码示例:

kotlin 复制代码
object Routes {
    const val HOME = "home"
    const val PROFILE = "profile"

    // Auth Flow related
    const val AUTH_GRAPH_ROUTE = "auth_graph" // 路由名给整个登录流程图
    const val LOGIN = "login"
    const val REGISTER = "register"
}

在登陆模块的内部实现一个NavGraphBuilder的扩展函数,目的是把内部的导航图内聚,并方便地暴露给主导航调用:

kotlin 复制代码
fun NavGraphBuilder.authGraph(navController: NavHostController) {
    navigation(
        route = Routes.AUTH_GRAPH_ROUTE,           // 整个嵌套图的路由
        startDestination = Routes.LOGIN            // 这个嵌套图的起始页面
    ) {
        composable(Routes.LOGIN) {
            LoginScreen(
                onLoginSuccess = {
                    // 登录成功后,返回到之前的屏幕或导航到主页
                    // 通常会清除登录流程的后退栈
                    navController.popBackStack(Routes.AUTH_GRAPH_ROUTE, inclusive = true) // 清除整个登录流程
                },
                onNavigateToRegister = {
                    navController.navigate(Routes.REGISTER)
                }
            )
        }
        composable(Routes.REGISTER) {
            RegisterScreen(
                onRegisterSuccess = {
                    navController.popBackStack(Routes.AUTH_GRAPH_ROUTE, inclusive = true)// 清除整个登录流程
                },
                onNavigateBackToLogin = {
                    navController.popBackStack() // 返回到登录页
                }
            )
        }
        // 可以添加更多页面,如忘记密码等
    }
}

主导航图:

kotlin 复制代码
NavHost(navController = navController, startDestination = Routes.HOME) {
        composable(Routes.HOME) {
            HomeScreen(
                onNavigateToProfile = {
                    if (UserSession.isLoggedIn.value) {
                        navController.navigate(Routes.PROFILE)
                    } else {
                        // 如果未登录,导航到整个登录流程图
                        navController.navigate(Routes.AUTH_GRAPH_ROUTE)
                    }
                }
            )
        }
        composable(Routes.PROFILE) {
		......
			// 如果有判断未登录,导航到整个登录流程图
                        navController.navigate(Routes.AUTH_GRAPH_ROUTE)
                   }

        // 在这里集成登录流程的嵌套图
        authGraph(navController)
    }
  • Navigation: 主要用于管理应用中不同屏幕或目的地 (Destinations) 之间的切换。它构建了一个导航图 (Navigation Graph),定义了各个屏幕以及它们之间的导航路径。

    它主要关注的是应用的整体导航结构,如从列表页跳转到详情页,从设置页返回主页等。也负责处理回退栈、参数传递、深层链接 、嵌套导航图等。

  • HorizontalPager : 用于实现内容的分页展示,允许用户通过水平或垂直滑动来切换不同的页面 ,比如常见的标签页 (Tabs)轮播图 (Banner)引导页等。

    它主要关注在同一个屏幕区域内,通过手势滑动来展示一系列相关联的内容片段。

通常,包含HorizontalPager的Composable函数会作为一个导航图中的目的地(NavDestination)。比如我们熟知的微信,如下:

它的主页HomePage就是一个典型的标签页,内部包含了一个HorizontalPager,它内部又包含TabItem:聊天通讯录发现,这4个pager页面,通过滑动来切换。 HorizontalPager内部的页面内容,通常也是可以点击的,并导航到其他目的地。比如点击聊天里面的某个聊天项Item,就会跳转到聊天详情页。

相关推荐
我命由我123455 小时前
Android 开发问题:getLeft、getRight、getTop、getBottom 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
alexhilton5 天前
Kotlin互斥锁(Mutex):协程的线程安全守护神
android·kotlin·android jetpack
是六一啊i6 天前
Compose 在Row、Column上使用focusRestorer修饰符失效原因
android jetpack
用户060905255228 天前
Compose 主题 MaterialTheme
android jetpack
用户060905255228 天前
Compose 简介和基础使用
android jetpack
用户060905255228 天前
Compose 重组优化
android jetpack
行墨8 天前
Jetpack Compose 深入浅出(一)——预览 @Preview
android jetpack
alexhilton9 天前
突破速度障碍:非阻塞启动画面如何将Android 应用启动时间缩短90%
android·kotlin·android jetpack
Pika10 天前
深入浅出 Compose 测量机制
android·android jetpack·composer