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,就会跳转到聊天详情页。

相关推荐
alexhilton20 小时前
灵活、现代的Android应用架构:完整分步指南
android·kotlin·android jetpack
4z332 天前
Jetpack Compose重组原理(一):快照系统如何精准追踪状态变化
前端·android jetpack
木易 士心3 天前
Android Jetpack Compose 从入门到精通
android·android jetpack
alexhilton3 天前
如何构建Android应用:深入探讨原则而非规则
android·kotlin·android jetpack
雨白3 天前
使用 Jetpack Compose 构建一个整洁架构笔记应用
android·android jetpack·mvvm
Lei活在当下9 天前
【业务场景架构实战】5. 使用 Flow 模式传递状态过程中的思考点
android·架构·android jetpack
天花板之恋10 天前
Compose状态管理
android jetpack
alexhilton11 天前
面向开发者的系统设计:像建筑师一样思考
android·kotlin·android jetpack
Lei活在当下12 天前
【业务场景架构实战】4. 支付状态分层流转的设计和实现
架构·android jetpack·响应式设计