Jetpack Compose Navigation

939 阅读6分钟

Navigation 是 Android 导航组件,用于页面间跳转

Compose Navigation 依赖

// build.gradle
dependencies {
  implementation "androidx.navigation:navigation-compose:2.5.3"
  // ...
}

NavController

简介

  • 可跟踪返回堆栈可组合条目、使堆栈向前移动、支持对返回堆栈执行操作,以及在不同目的地状态之间导航
  • 是导航的核心,设置 Compose Navigation 时必须先创建它
  • 负责在目标页面(即应用中的屏幕)之间导航

创建

调用 rememberNavController() 函数获取 NavHostController

NavHostController 是 NavController 类的子类,可提供与 NavHost 可组合项搭配使用的额外功能

@Composable
fun Test() {
    val navHostController = rememberNavController()
    // ...
}
  • NavController 放置在可组合项层次结构的顶层,即不向下传递
  • 确保 NavController 是在可组合屏幕之间导航和维护返回堆栈的主要可信来源

Navigation 的 3 个主要部分是 NavControllerNavGraph 和 NavHost

每个 NavController 都必须与一个 NavHost 相关联。

NavHost

NavHost 充当容器,负责显示 NavGraph 的当前目的地

在可组合项之间进行导航时,NavHost 的内容会自动进行重组

// 源码
@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) {
    NavHost(
        navController,
        remember(route, startDestination, builder) {
            navController.createGraph(startDestination, route, builder)
        },
        modifier
    )
}
  • navController NavHostController 类的实例。您可以使用此对象在屏幕之间导航,例如,通过调用 navigate() 方法导航到另一个目标页面。您可以通过从可组合函数调用 rememberNavController() 来获取 NavHostController
  • startDestination :此字符串路线用于定义应用首次显示 NavHost 时默认显示的目标页面

NavHost 需要指定 startDestination: 启动时显示的目的地

builder: NavGraphBuilder.() -> Unit 负责定义和构建导航图

在 NavHost 的内容函数中,调用 composable() 函数。composable() 函数有两个必需参数。

  • route : 与路线名称对应的字符串。这可以是任何唯一的字符串

  • content :您可以在此处调用要为特定路线显示的可组合项

  • arguments :

public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    addDestination(
        ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
            this.route = route
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}

composable() 函数是 NavGraphBuilder 的扩展函数

NavGraph 导航图

用于映射要导航到的可组合项目标页面

builder 形参要求使用 NavGraphBuilder.composable 扩展函数,将各个可组合目的地添加到导航图中,并定义必要的导航信息。

@Composable
fun RallyNavHost() {
    NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = modifier
) {
    composable(route = Overview.route) {
    
    }
  }
}
// NavGraphBuilder.composable 源码
public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    addDestination(
        ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
            this.route = route
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}

导航

通过 navController.navigate(route) 执行导航操作

为了使代码具有可测试性且可重复使用,建议不要将整个 navController 直接传递给可组合项。不过,您应该始终提供回调,定义您希望触发的确切导航操作。

navigateUp

popBackStack

  • 跳转移除返回堆栈中的所有屏幕,并返回起始屏幕
navController.popBackStack(@IdRes destinationId: Int, inclusive: Boolean)
  • destinationId : 表示您希望返回到的目标页面的路线
  • inclusive: 如果为 true,会移除指定路线;为 false,popBackStack() 将移除起始目标页面之上的所有目标页面,但不包含该起始目标页面,并仅留下该起始目标页面作为最顶层的屏幕显示给用户

launchSingleTop

  • 确保返回堆栈顶部最多只有给定目的地的一个副本,Compose Navigation API 提供了一个 launchSingleTop 标志
navController.navigate(route) { launchSingleTop = true }

popUpTo

  • popUpTo(startDestination) { saveState = true } 
  • 弹出到导航图的起始目的地,以免在您选择标签页时在返回堆栈上构建大型目的地堆栈

restoreState

  • restoreState = true 
  • 确定此导航操作是否应恢复 PopUpToBuilder.saveState 或 popUpToSaveState 属性之前保存的任何状态。请注意,如果之前未使用要导航到的目的地 ID 保存任何状态,此项不会产生任何影响
fun NavHostController.navigateSingleTopTo(route: String) {
    this.navigate(route) {
        popUpTo(this@navigateSingleTopTo.graph.findStartDestination().id) {
            saveState = true
        }
        launchSingleTop = true
        restoreState = true
    }
}

currentBackStackEntryAsState

如需以 State 的形式获取返回堆栈中当前目的地的实时更新,您可以使用 navController.currentBackStackEntryAsState(),然后获取其当前 destination

currentBackStack?.destination 会返回 NavDestination

参数传递

具名实参的定义方式是附加到路线并用花括号括起来,如下所示:{argument}

Tips:为了提高代码安全性和处理任何极端情况,可以将默认值设置为实参并明确指定其类型。

步骤:

  1. 如需在导航时随路线一起传递实参,需要按照以下模式将它们附加在一起:"route/{argument}"
  2. 参数列表,NavGraphBuilder.composable 声明接受参数类型,定义其 arguments 形参;可以根据需要定义任意数量的实参,因为 composable 函数默认接受实参列表;(如未明确设置类型,系统将根据此实参的默认值推断出其类型)
  3. 获取参数,通过 navBackStackEntry.arguments?.getString(KEY) 获取

每个 NavHost 可组合函数都可以访问当前的 NavBackStackEntry,该类用于保存当前路线的相关信息,以及返回堆栈中条目的已传递实参

默认情况下,所有参数都会被解析为字符串。composable() 的 arguments 参数接受 NamedNavArgument 列表。您可以使用 navArgument 方法快速创建 NamedNavArgument,然后指定其确切 type 类型:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

可选参数

特点:

  • 可选参数必须使用查询参数语法 ("?argName={argName}") 来添加
  • 可选参数必须具有 defaultValue 集或 nullability = true(将默认值隐式设置为 null
composable(
    route = "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

deepLink 深层链接

调用深层链接,Android 可以打开到相应页面

使用 navDeepLink 方法创建 NavDeepLink

由于向外部应用公开深层链接这一功能默认处于未启用状态,因此您还必须向应用的 manifest.xml 文件添加 <intent-filter> 元素 步骤:

  1. AndroidManifest.xml 添加深层链接,通过 <activity> 内的 <intent-filter> 创建新的过滤器,<action> 类型为 VIEW,类别为 BROWSABLE 和 DEFAULT
  2. 使用 <data> 添加 shemehost,如下
  3. 代码中响应 intent,给 NavGraphBuilder.composable 中参数 deepLinks 赋值;
  4. adb 测试 deepLink,命令如下
adb shell am start -d "rally://single_account/Checking" -a android.intent.action.VIEW
// Manifest.xml
<activity
    android:name=".RallyActivity"
    android:windowSoftInputMode="adjustResize"
    android:label="@string/app_name"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    // deepLink 
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="rally" android:host="single_account" />
    </intent-filter>
</activity>
@Composable
fun Test() {
    ... 
    composable(
        route = SingleAccount.routeWithArgs,
        // ...
        deepLinks = listOf(navDeepLink {
            uriPattern = "rally://${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
        })
    ) { ... }
}

Nested Navigation 嵌套导航

需向 NavHost 添加嵌套图,您可以使用 navigation 扩展函数:

NavHost(navController, startDestination = "home") {
    ...
    // Navigating to the graph via its route ('login') automatically
    // navigates to the graph's start destination - 'username'
    // therefore encapsulating the graph's internal routing logic
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
    ...
}
fun NavGraphBuilder.loginGraph(navController: NavController) {
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
}
NavHost(navController, startDestination = "home") {
    ...
    loginGraph(navController)
    ...
}

注意:需要 loginGraphNavGraphBuilder 的扩展函数

BottomNavigation

底部导航栏

// 依赖
dependencies {
    implementation("androidx.compose.material:material:1.3.1")
}
@Composable
fun BottomNavigation(
    modifier: Modifier = Modifier,
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    elevation: Dp = BottomNavigationDefaults.Elevation,
    content: @Composable RowScope.() -> Unit
): Unit

BottomNavigation 应该包含多个 BottomNavigationItems 项,每个导航项代表一个单一的目的地。

@Composable
fun ScaffoldDemo(){
    var selectedItem by remember { mutableStateOf(0) }
    val items = listOf("主页", "我喜欢的", "设置")
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text("主页")
                },
                navigationIcon = {
                    IconButton(onClick = {

                    }) {
                        Icon(Icons.Filled.ArrowBack, null)
                    }
                }
            )
        },
        bottomBar = {
            BottomNavigation {
                items.forEachIndexed { index, item ->
                    BottomNavigationItem(
                        icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
                        label = { Text(item) },
                        selected = selectedItem == index,
                        onClick = { selectedItem = index }
                    )
                }
            }
        }
    ){

    }
}

上面的代码效果如图: image.png

NavigationBar

出自 Meterial Design 3,功能与 BottomNavigation 类似; NavigationBar 需要与 NavigationBarItem 搭配使用

@Composable
fun NavigationBar(
    modifier: Modifier = Modifier,
    containerColor: Color = NavigationBarDefaults.containerColor,
    contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
    tonalElevation: Dp = NavigationBarDefaults.Elevation,
    windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
    content: @Composable RowScope.() -> Unit
) { ... }
@Composable
fun RowScope.NavigationBarItem(
    selected: Boolean,
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    label: @Composable (() -> Unit)? = null,
    alwaysShowLabel: Boolean = true,
    colors: NavigationBarItemColors = NavigationBarItemDefaults.colors(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) { ... }

NavigationDrawer

这部分的介绍请查看# Compose NavigationDrawer 和 响应式 UI 导航适配;写的很详细,简介易懂!

文章参考:

Jetpack Compose 导航

Android Compose的Window Insets

Accompanist组件库中文指南 - Insets篇

Jetpack Compose Navigation and Deeplinks

nested navigation

Compose NavigationDrawer 和 响应式 UI 导航适配