Compose学习笔记-Navigation2

Jetpack Compose

导航是指允许用户跨越、进入和退出应用中不同内容片段的交互。

Android Jetpack 的 Navigation 组件包含 Navigation 库、Safe Args Gradle 插件,以及可帮助我们实现应用导航的工具。Navigation 组件可处理各种导航用例,从简单的按钮点击到更复杂的模式(例如应用栏和抽屉式导航栏)。

主要概念

构思 目的 类型
宿主 包含当前导航目的地的界面元素。也就是说,当用户浏览应用时,该应用实际上会在导航宿主中切换目的地。 ComposeNavHost
fragmentNavHostFragment
图表 一种数据结构,用于定义应用中的所有导航目的地以及它们如何连接在一起。 NavGraph
控制器 用于管理目的地之间导航的中央协调器。该控制器提供了一些方法,可在目的地之间导航、处理深层链接、管理返回堆栈等。 NavController
目的地 导航图中的节点。当用户导航到此节点时,主机会显示其内容。 NavDestination通常是在构建导航图时创建的。
路线 唯一标识目的地及其所需的任何数据。您可以使用路线导航。路线可带您前往目的地。

优点和功能

Navigation 组件提供了许多其他优势和功能,包括:

  • 动画和过渡:为动画和过渡提供标准化资源。
  • 深层链接:实现并处理可将用户直接转到目的地的深层链接。
  • 界面模式:只需极少的额外工作即可支持抽屉式导航栏和底部导航等模式。
  • 类型安全:支持在目的地之间传递类型安全的数据。
  • ViewModel 支持:允许将 ViewModel 的作用域限定为导航图,以便在导航图的目的地之间共享与界面相关的数据。
  • fragment 事务:全面支持和处理 fragment 事务。
  • 返回和向上:默认情况下,系统会正确处理返回和向上操作。

配置环境

plugins {
  // Kotlin serialization plugin for type safe routes and navigation arguments
  kotlin("plugin.serialization") version "2.0.21"
}

dependencies {
  val nav_version = "2.9.0"

  // Jetpack Compose integration
  implementation("androidx.navigation:navigation-compose:$nav_version")

  // Feature module support for Fragments
  implementation("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")

  // Testing Navigation
  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")

  // JSON serialization library, works with the Kotlin serialization plugin
  implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}

创建导航控制器

导航控制器是导航中的关键概念之一。它用于存放导航图,并会公开可让应用在导航图中的目的地之间移动的方法。

在Compose中创建NavController需要使用到rememberNavController ()

val navController = rememberNavController()

应在可组合项层次结构中的较高层级创建 NavController。它所在的组合层次应该足够高,以便需要引用它的所有可组合项都能成功引用它。这样我们变可将NavController用作单一可信来源(状态提升原则)。

设计导航图

Navigation 组件使用导航图管理应用导航。导航图是一种数据结构,包含应用内的每个目的地以及这些目的地之间的连接。

注意:导航图不同于[返回堆栈],后者是 NavController 内用于保存用户近期所到目的地的堆栈。

目的地类型

有三种常规类型的目的地:Hosted、Dialog和 activity。下表概述了这三种目的地类型及其用途。

类型 说明 用例
Hosted 填充整个导航宿主。也就是说,托管目的地的大小与导航宿主的大小相同,之前的目的地不会显示。 主界面和详情界面。
Dialog 显示叠加界面组件。此界面与导航宿主的位置或大小无关。之前的目的地会显示在该目的地下方。 提醒、选择、表单。
activity 表示应用中的独特界面或功能。 充当导航图的退出点,启动与 Navigation 组件分开管理的新 Android activity。
在 Modern Android Development 中,应用由 1 个 activity 组成。因此,当与第三方 activity 互动或作为迁移过程的一部分时,最适合使用 activity 目的地。

使用

常见NavGraph

在Compose中我们要使用NavHost可组合项,使用Kotlin DSL为NavHost可组合项添加NavGraph;有以下两种方式可以创建:

  • 作为 NavHost 的一部分:在添加 NavHost 的过程中直接构建导航图。
      //定义导航线路
      sealed interface Route {
          @Serializable
          data object Main : Route
    
          @Serializable
          data object Login : Route
      }
    
      @Composable
      fun RootContainer() {
          val navigationController = rememberNavController()
          ComposeLearnTheme {
              NavHost(
                  navController = navigationController,
                  startDestination = Route.Main
              ) {
                  composable<Route.Main> { MainScreen() }
                  composable<Route.Login> { LoginScreen() }
              }
          }
      }
    
  • 以编程方式:使用 NavController.createGraph()方法创建 NavGraph 并将其直接传递给 NavHost
      sealed interface Route {
          @Serializable
          data object Main : Route
    
          @Serializable
          data object Login : Route
      }
    
      @Composable
      fun RootContainer() {
          val navController = rememberNavController()
          val navGraph = remember {
              navController.createGraph(startDestination = Route.Main) {
                  composable<Route.Main> { MainScreen() }
                  composable<Route.Login> { LoginScreen() }
              }
          }
          ComposeLearnTheme {
              NavHost(navController, navGraph)
          }
      }
    

传递参数

如果我们要传递参数给目标页面则需要在定义路线是使用参数:

@Serializable
data class Login(val account: String = "") : Route

上面的定义是说明我们在跳转到登录页面时可以携带一个默认的account参数,如果我们希望account为必传则去掉默认赋值即可(这是Kotlin的语法特性)。

获取路线实例

我们可以使用NavBackStackEntry.toRoute()SavedStateHandle.toRoute()获取路线实例。使用 composable() 创建目的地时,NavBackStackEntry 可用作参数。

sealed interface Route {

    @Serializable
    data class Login(val account: String = "") : Route
}

@Composable
fun RootContainer() {
    val navController = rememberNavController()
    val navGraph = remember {
        navController.createGraph(startDestination = Route.Login(account = "6067429")) {
            composable<Route.Login> {backStackEntry ->
                LoginScreen(backStackEntry.toRoute<Route.Login>().account)
            }
        }
    }
    ComposeLearnTheme {
        NavHost(navController, navGraph)
    }
}

对话框目的地 Dialog destinations

使用Navigation我们可以轻松的开发对话框(Dialog),我们只需要使用dialog()函数向NavGraph添加一个目的地即可:

sealed class Route {
    @Serializable
    data object Main : Route()

    @Serializable
    data object Dialog : Route()
}

@Composable
fun RootContainer() {
    val navController = rememberNavController()
    val navGraph = remember {
        navController.createGraph(startDestination = Route.Main) {
            composable<Route.Main> {
                MainScreen { route ->
                    navController.navigate(route)
                }
            }
            //为导航图添加Dialog目的地。
            dialog<Route.Dialog>{
                CustomDialog{
                    navController.popBackStack()
                }
            }
        }
    }
    ComposeLearnTheme {
        NavHost(navController, navGraph)
    }
}

Activity目的地

在Compose中我们不但可以以可组合项和Dialog为目的地,还可以使用Activity为目的地:

sealed class Route {
    @Serializable
    data object PageA : Route()

    @Serializable
    data object PageB : Route()
}

@Composable
fun RootContainer() {
    val navController = rememberNavController()

    val navGraph = remember {
        navController.createGraph(startDestination = Route.PageA) {
            composable<Route.PageA> {
                PageAScreen {
                    navController.navigate(Route.PageB)
                }

            }
            activity<Route.PageB> {
                //指定跳转到SecondActivity。
                activityClass = SecondActivity::class
            }
        }
    }
    NavHost(navController, navGraph)
}

嵌套图

应用中的登录流程、向导或其他子流程通常是[嵌套导航图]的最佳表示形式。通过以这种方式嵌套独立的子导航流程,您可以更轻松地理解和管理应用界面的主流程。
嵌套图是可以重复使用的。它们还提供了一定程度的封装,即嵌套图之外的目的地无法直接访问嵌套图内的任何目的地。相反,它们只能navigate到嵌套图本身。

顶级导航图

如需在Compose中使用嵌套导航图,需要使用NavGraphBuilder.navigation()函数,然后我们可以像使用NavGraphBuilder.composable()NavGraphBuilder.dialog()函数一样使用navigation()
主要区别在于navigation()会创建嵌套图而非目的地。然后,通过在navigation()的lambda函数中调用composable()dialog(),向嵌套图中添加目的地。

sealed class Route {

    @Serializable
    data object Main : Route()

    @Serializable
    data object Detail : Route()

    @Serializable
    open class Game : Route() {
        @Serializable
        data object Home : Game()

        @Serializable
        data object Gaming : Game()

        @Serializable
        data class End(val gameOver: Boolean = false) : Game()
    }
}

@Composable
fun RootContainer() {
    val navController = rememberNavController()
    val navGraph = remember {
        navController.createGraph(startDestination = Route.Main) {
            composable<Route.Main> {
                MainScreen { route ->
                    navController.navigate(route)
                }
            }
            composable<Route.Detail> { DetailScreen() }
            //创建嵌套图
            navigation<Route.Game>(startDestination = Route.Game.Home) {
                composable<Route.Game.Home> {
                    GameHomeScreen { route ->
                        navController.navigate(route) {
                            popUpTo(Route.Game())
                        }
                    }
                }
                composable<Route.Game.Gaming> {
                    GamingScreen { route ->
                        navController.navigate(route) {
                            popUpTo(Route.Game())
                        }
                    }
                }
                composable<Route.Game.End> {
                    GameEndScreen(
                        it.toRoute<Route.Game.End>().gameOver,
                    ) { route ->
                        navController.navigate(route) {
                            popUpTo(Route.Game())
                        }
                    }
                }
            }
        }
    }
    ComposeLearnTheme {
        NavHost(navController, navGraph)
    }
}

导航选项

在使用navigation()进行导航时可以传递一个NavOptions类型的参数,此类包含传输至目标目的地以及从该处传出的所有特殊配置,包括动画资源配置、弹出行为,以及是否应在单一顶级模式下启动目的地。

sealed class Route {
    @Serializable
    data object Main : Route()

    @Serializable
    data object ProductList : Route()
}

@Composable
fun RootContainer() {
    val navController = rememberNavController()
    val normalScreenAnim: AnimBuilder.() -> Unit = remember {
        {
            enter = R.anim.screen_enter
            exit = R.anim.screen_exit
            popEnter = R.anim.screen_pop_enter
            popExit = R.anim.screen_pop_exit
        }
    }
    val navOption = remember{
        navOptions {
            anim(normalScreenAnim)
        }
    }
    val navGraph = remember {
        navController.createGraph(startDestination = Route.Main) {
            composable<Route.Main> {
                MainScreen { route ->
                    navController.navigate(
                        route,
                        navOption
                    )
                }

            }
            composable<Route.ProductList> {
                ProductListScreen()
            }
        }
    }
    ComposeLearnTheme {
        NavHost(navController, navGraph)
    }
}

导航到目的地

Navigation 组件提供了一种导航到目的地的简单通用方式。此接口支持一系列上下文和界面框架。

使用NavController

我们在目的地之间移动时所使用的的关键类就是NavController。在Compose中我们通过val navController = rememberNavController()的方式获取NavController。

导航

在Compose中如果我们想要导航到某个目的地(目标页面),则可以使用NavController.navigate()方法。navigate()方法有很多重载,在使用时根据自身业务选择对应的重载进行调用。

下面将对常用重载进行介绍:

导航到可组合项

如需导航到和组合项应该使用NavController.navigate<T>,使用该重载时navigate()会接收一个route作为参数,我们可以传递目的地的key。

@Serializable
object FriendsList

navController.navigate(route = FriendsList)

公开可组合项4中的事件

当可组合函数需要导航到新屏幕时,我们不应该将NavController传递到可组合项,而是应该根据[单向数据流]的原则将可组合项中的事件通过回调的方式对调用者暴露,有调用者处理该事件,原则上之允许在使用NavHost可组合项处获得NavController的实例。

使用 NavDeepLinkRequest 导航

使用navigate(NavDeepLinkRequest)重载可以让我们导航到DeepLink目的地:

//创建产品列表的导航图
composable<Route.ProductList>(
    deepLinks = listOf(
        //添加deepLink支持
        navDeepLink {
            uriPattern = "my-app://app.products.list"
        }
    )
) {
    ProductListScreen()
}

//通过deepLink跳转到产品列表
navController.navigate(
    NavDeepLinkRequest.Builder.fromUri(
        uri = "my-app://app.products.list".toUri()
    ).build()
)

注意:使用 NavDeepLinkRequest 进行导航时,返回堆栈不会重置。 此行为与其他形式的[deep link navigation]不同。不过,popUpTo()popUpToInclusive() 仍会从返回堆栈中移除目的地。

除了Uri之外,NavDeepLinkRequest还支持带有Action和MIME类型的deep link。如需想请求添加Action需使用fromAction()setAction()。如需想请求添加MIME则需使用fromMimeType()setMimeType()

其他重载

在日常开发中我们可能还会用到其他的跳转到目的地的方式,而navigate()也基本上都提供了响应的重载。

以程序化方式与 Navigation 组件交互

导航组件提供以程序化的方式与之交互的方法,下面是与Compose相关的介绍:

使用 NavBackStackEntry 引用目的地

我们可以通过NavController.getBackStackEntry()方法获取对导航堆栈任何目的地的NavBackStackEntry的引用。如果返回堆栈包含指定目的地的多个实例,getBackStackEntry()会返回堆栈中最顶层的实例。

navController.getBackStackEntry(Route.Main)

向上一个目的地返回结果

NavBackStackEntry提供了对SavedStateHandle的访问权限。是键值对映射,可用于存储和检索数据。这些值会在进程终止后保留,包括配置更改时。使用给定的SavedStateHandle,我们可以在目的地之间访问和传递数据。作为一种在数据从堆栈中退出后从目的地取回数据的机制,此方法特别有用。
下面就通过一个栗子来看下如果利用SavedStateHandle从PageB中返回一个结果到PageA:

sealed class Route {
    @Serializable
    data object PageA : Route()

    @Serializable
    data object PageB : Route()
}

@Composable
fun RootContainer() {
    val navController = rememberNavController()
    val navGraph = remember {
        navController.createGraph(startDestination = Route.PageA) {
            composable<Route.PageA> {
                PageAScreen {
                    val currentStackEntry = navController.currentBackStackEntry
                    navController.navigate(Route.PageB)
                    val result = suspendCoroutine { continuation ->
                        if (currentStackEntry != null) {
                            currentStackEntry.run {
                                savedStateHandle.getLiveData<String>("result").observe(this) {
                                    //拿到结果后返回给协程
                                    continuation.resume(it)
                                    //移除结果,防止被重复接收。
                                    savedStateHandle.remove<String>("result")                                  }
                            }
                        } else {
                            continuation.resume("")
                        }
                    }
                    return@PageAScreen result
                }

            }
            composable<Route.PageB> {
                PageBScreen { result ->
                    //通过previousBackStackEntry获取上一个页面的NavBackStackEntry实例。
                    val stackEntry = navController.previousBackStackEntry
                    //退出当前页面
                    navController.popBackStack() 
                    //利用savedStateHandle进行传值。
                    stackEntry?.savedStateHandle?.set("result", result)
                }
            }
        }
    }
    NavHost(navController, navGraph)
}


@Composable
fun PageAScreen(onGotoPageB: suspend CoroutineScope.() -> String) {
    val coroutineScope = rememberCoroutineScope()
    var pageBResult by rememberSaveable { mutableStateOf("") }
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterVertically)
    ) {
        Text("我是PageA")
        if (pageBResult.isNotBlank()) {
            Text("我接收到了页面B返回的数据。")
        }
        Button(onClick = {
            coroutineScope.launch {
                pageBResult = onGotoPageB(this)
            }
        }) {
            Text("进入PageB")
        }
    }
}

@Composable
fun PageBScreen(onResult: (result: String) -> Unit) {
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text("我是PageB页面")
        Button(
            modifier = Modifier.padding(top = 150.dp),
            onClick = { onResult("你好,我是页面B!") },
        ) {
            Text("返回数据并关闭")
        }
    }
}

导航和返回堆栈

NavController 会存储“返回堆栈”。当用户在应用中导航到屏幕时,NavController 会在返回堆栈中添加和移除目的地。在堆栈中,返回堆栈采用“后进先出”原则的数据结构。

基本行为

关于返回堆栈的行为,您应考虑以下核心事实:

  • 第一个目的地:当用户打开应用时,NavController 会将第一个目的地推送到返回堆栈的顶部。
  • 推送到堆栈:每次调用 NavController.navigate() 都会将给定目的地推送到堆栈顶部。
  • 弹出顶部目的地:点按向上返回会分别调用 NavController.navigateUp()NavController.popBackStack()方法。它们会将弹出目的地从堆栈中弹出。如需详细了解向上返回之间的区别,请参阅导航原则页面。

返回

NavController.popBackStack() 方法会尝试将当前目的地从返回堆栈中弹出,并导航到上一个目的地。这样可以有效地将用户在其导航历史记录中向后移动一步。该方法会返回一个布尔值,表明它是否已成功返回到目的地。

返回到特定目的地

我们还可以使用NavController.popBackStack()方法返回到指定的目标页面(之前打开过的)。

navController.popBackStack(route = Route.PageB, inclusive = true)

这个重载中有一个inclusive参数,表示是在转到route指定的目的地页面后是否也应该从返回栈中弹出该目的地页面。

处理失败的返回

当 popBackStack() 返回 false 时,对 NavController.getCurrentDestination() 的后续调用会返回 null。这意味着应用已从返回堆栈中弹出最后一个目的地。在这种情况下,用户只会看到 空白屏幕。

这可能发生在以下情况:

  • popBackStack() 没有从堆栈中弹出任何内容。
  • popBackStack() 确实从返回堆栈中弹出了目的地,但堆栈现在为空。

如果要解决这种情况,我们应该在popBackStack()返回false后调用Activity的finish()方法,结束Activity。例如:

if (!navController.popBackStack()) {
    // 调用Activity的finish方法。
    finish()
}

弹出到目的地

当我们需要从一个目的地导航到另一个目的地时从返回堆栈中移除当前目的地,则需要使用popUpTo()方法,并将该方法关联到navigate()中:

navController.navigate(Route.PageC){
    popUpTo(Route.PageA){
        inclusive = true  //指定从返回栈中弹出Route.PageA
    }
}

弹出时保存状态

在使用popUpTo()popBackStack()导航到目的地时,我们可以选择保存当前目的地以及从返回栈中弹出的所有目的地的状态,以便我们在后续再打开该页面时回复状态:

navController.navigate(Route.PageC){
    popUpTo(Route.PageB){
        inclusive = true  //弹出当前目的地
        saveState = true  //保存当前目的地的状态。
    }
    restoreState = true
}

这个我暂时没弄懂应用场景以及如何使用的,有知道的小伙伴麻烦帮忙解答一下,感谢!!!

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容