Navigation是一个Jetpack库,用于在运用中从一个意图地导航到另一个意图地。Navigation库还供给了一个专用工件,用于运用Jetpack Compose完结一致而惯用的导航方式。

你将会学到

  • 将Jetpack Navigation与Jetpack Compose合作运用的基础知识
  • 在可组合项之间导航
  • 将自界说标签页栏可组合项集成到导航层级结构中
  • 运用实参进行导航
  • 运用深层链接进行导航
  • 测验导航

一、设置

如需自行学习,请克隆此 Codelab 的开端代码(main分支)。

$ git clone https://github.com/googlecodelabs/android-compose-codelabs.git

或许,您也能够下载两个 ZIP 文件:

  • 开端代码
  • 处理方案

现在,您已下载相应代码,请在 Android Studio 中翻开NavigationCodelab项目文件夹。现已准备就绪,能够开端开发项目了。

二、Rally运用概览

首先,您应当熟悉Rally运用及其代码库。运转运用并稍加探究。

Rally有三个主屏幕作为可组合项:

  1. OverviewScreen – 全部财政买卖和提醒的概览
  2. AccountsScreen – 有关现有账户的数据剖析
  3. BillsScreen – 预定支出

Jetpack Compose(第九趴)——Jetpack Compose Navigation(上)

在屏幕的最顶部,Rally运用自界说标签页栏可组合项(RallyTabRow) 在这三个屏幕之间进行导航。点按每个图标应该会翻开当时所选内容,您也会转到其对应的屏幕:

Jetpack Compose(第九趴)——Jetpack Compose Navigation(上)

在导航到这些可组合屏幕时,您还能够将它们视为导航意图地,因为咱们期望在特定时刻抵达各个意图地。这些意图地是在RallyDestinations.kt文件中预界说的。

在该文件中,您会找到全部三个界说为目标的首要意图地(Override,AccountsBills),以及一个日后要增加到运用的SingleAccount。每个目标都从RallyDestination接口扩展,并且包括有关每个意图地的必要信息,以便进行导航:

  1. 顶栏的icon
  2. 字符串route(这对Compose Navigation而言是必须的,可作为指向相应意图地的路径)
  3. screen,表明相应意图地的整个可组合项。

运转运用时,您会发现自己实践上能够在当时运用顶栏的意图地之间导航。但是,运用实践上并未运用Compose Navigation,但它当时运用的导航机制依靠手动切换一些可组合项和触发重组来显现新内容。

三、迁移到Compose Navigation

迁移到Jetpack Compose所涉及的根本过程如下:

  1. 增加最新的Compose Navigation依靠项
  2. 设置NavController
  3. 增加NavHost并创立导航图
  4. 准备道路以在不同的运用意图地之间导航
  5. 将当时导航机制替换为Compose Navigation

3.1、增加Navigation依靠项

翻开运用的build文件(坐落app/build.gradle)。在“dependencies”区段中,增加navigation-compose依靠项。

dependencies {
    implementation "androidx.navigation:navigation-compose:{latest_version}"
}

您能够点击此处找到最新版 Navigation Compose。

现在,同步项目,然后您就能够开端运用 Compose 中的 Navigation 了

3.2、设置NavController

运用Compose中的Navigation时,NavController是中心组件。它可盯梢回来仓库可组合条目、使仓库向前移动、支持对回来仓库履行操作,以及在不同意图地状况之间导航。因为NavController是导航的中心,因而在设置Compose Navigation时必须先创立它。

NavController是经过调用rememberNavController()函数获取的。这将创立并记住NavController,它能够在装备更改后继续存在(运用rememberSaveable)。

一定要创立NavController并将其放置在可组合项层次结构的顶层(通常坐落App可组合项中)。之后,全部需求引证NavController的可组合项都能够访问它。这么做符合状况提高的准则,并可保证NavController是在可组合屏幕之间导航和维护回来仓库的首要可信来源。

翻开RallyActivity.kt。运用RallyApp内的rememberNavController()获取NavController,因为它是整个运用的根可组合项和入口点:

import androidx.navigation.compose.rememberNavController
// ...
@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            // ...
        ) {
            // ...
        }
    }
}

3.3、Compose Navigation中的道路

如前所述,Rally运用有三个首要意图地,以及一个日后要增加的额外意图地(SingleAccount)。这些意图地在RallyDestinations.kt中界说。咱们现已说到,每个意图地都有界说的iconroutescreen:

Jetpack Compose(第九趴)——Jetpack Compose Navigation(上)

接下来,将这些意图地增加到导航图中,其间Overview作为运用启动时的其实意图地。

运用Compose中的Navigation时,导航图中的每个可组合意图地都与一个道路相关联。道路用字符串表明,用于界说指向可组合项的路径,并指引您的navController抵达正确的方位。您能够将其视为指向特定意图地的隐式深层链接。每个意图地都必须有一条仅有的道路

为此,咱们会运用每个RallyDestination目标的route特点。例如,Overview.route是将您转到Overview屏幕可组合项的录像。

3.4、运用导航图调用NavHost可组合项

下一步是增加NavHost并创立导航图。

Navigation的3个首要部分是NavControllerNavGraphNavHostNavController始终与一个NavHost可组合项相关联。NavHost充任容器,担任显现导航图的当时意图地。当您在可组合项之间进行导航时,NavHost的内容自动进行重组。此外,它还会将NavController与导航图(NavGraph)相关联,后者用于标出能够在其间进行导航的可组意图地。它实践上是一系列可提取的意图地。

回来到RallyActivity.kt中的RallyApp可组合项。将Scaffold内的Box可组合项(其间包括当时屏幕的内容,用于手动切换屏幕)替换为新的NavHost(可依照以下代码示例进行创立)。

传入咱们在上一步中创立navController,以将其挂接到这个NavHost。如前所述,每个NavController都必须与一个NavHost相关联。

NavHost还需求一个startDestination道路才干知道在运用启动时显现哪个意图地,因而请将其设置为Overview.route。此外,传递Modifier以承受外部Scaffold内边距,然后将其运用于NavHost

最后一个形参builder: NavGraphBuilder.() -> Unit担任界说和构建导航图。该形参运用的是Navigation Kotlin DSL中的lambda语法,因而可作为函数正文内部的跟随lambda传递并从括号中取出:

import androidx.navigation.compose.NavHost
...
Scaffold(...) { innerPadding ->
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = Modifier.padding(innerPadding)
    ) {
        // builder parameter will be defined here as the graph
    }
}

3.5、向NacGraph增加意图地

现在,您能够界说导航图以及NavController可导航到的意图地。如上所述,builder形参要求运用函数,因而Navigation Compose供给了NavGraphBuilder.composable扩展函数,以便轻松将各个可组合意图地增加到导航图中,并界说必要的导航信息。

第一个意图地是Overview,因而您需求经过composable扩展函数增加它,并为其设置仅有字符串route。此操作只会将意图地增加到导航图中,因而您还需求界说导航到此意图地时要显现的实践界面。此外,还可经过composable函数正文的跟随lambda完结此操作,这是Compose中常用的模式:

import androidx.navigation.compose.composable
// ...
NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
}

咱们会依照这种模式将全部三个主屏幕可组合项增加为三个意图地:

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
    composable(route = Accounts.route) {
        Accounts.screen()
    }
    composable(route = Bills.route) {
        Bills.screen()
    }
}

现在运转运用,您会看到Overview作为其实意图地,以及其对应的界面。

咱们之前说到过自界说顶部标签页栏可组合项RallyTabRow,该可组合项之前处理了屏幕之间的手动导航。此时,它尚未与新导航相关联,因而你能够验证一下点击标签页是否会更改所显现屏幕可组合项的意图地。接下来,咱们来处理这个问题!

四、将RallyTabRow与导航集成

在此过程中,您需求将RallyTabRownavController和导航图链接起来,以便其导航到正确的意图地。

为此,您需求运用新的navController,为RallyTabRowonTabSelected回调界说正确的导航操作。此回调界说了挑选特定标签页图标时产生的状况,并经过navController.navigate(route)履行导航操作。

遵从此指南,在RallyActivity中找到RallyTabRow金额组合项及其回调形参onTabSelected

因为咱们期望点按标签页后导航到特定意图地,因而您还需求知道所挑选的切当标签页图标。走运的是,onTabSelected:(RallyDestination) -> Unit形参已供给这些信息。您将运用这些信息和RallyDestination道路来引导navController并在用户挑选标签页时调用navController.navgate(newScreen.route):

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreen = rallyTabRowScreens,
                    // Pass the callack like this,
                    // defining the navigation action when a tab is selected:
                    onTabSelected = { newScreen ->
                        navController.navigate(newScreen.route)
                    },
                    currentScreen = currentScreen,
                )
            }
        )
    }
}

假如现在运转运用,能够验证点按RallyTabRow中的各个标签页是否确实会导航到正确的可组合意图地。不过,您现在可能现已注意到以下两个问题:

  1. 重按同一个标签页会启动同一意图地的多个副本
  2. 标签页的界面与所显现的正确意图地不符,也就是说,无法正常翻开和收起所选标签页:

Jetpack Compose(第九趴)——Jetpack Compose Navigation(上)

下面咱们来处理这两个问题!

4.1、启动意图地的单个副本

为了处理第一个问题,并保证回来仓库顶部最多只有给定意图地的一个副本, Compose Navigation API供给了一个launchSingleTop标志,您能够将其传递给navController.navigate()操作,如下所示:

navController.navigate(route) { launchSingleTop = true}

因为您期望整个运用中完结这种行为,因而关于每个意图地,不要将此标签复制粘贴到全部的navigate(...)调用中,而是将其提取到RallyActivity底部的辅佐扩展程序中。

import androidx.navigation.NavHostController
// ...
fun NavHostController.navigateSingleTopTo(route: String) = this.navigate(route) { launchSingleTop = true }

现在,您能够将navController.navigate(newScteen.route)调用替换为navigateSingleTopTo(...)。从头运转运用,然后验证一下:在顶栏中屡次点击器图标时,现在是否只会取得单一意图地的一个副本:

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreen = rallyTabRowScreens,
                    onTabSelected = { newScreen ->
                        navController.navigationTopTo(newScreen.route)
                    },
                    currentScreen = currentScreen,
                )
            }
        )
    }
}

4.2、操控导航选项和回来仓库状况

除了launchSingleTop之外,您还能够运用NavOptionsBuilder中的其他标志进一步操控和自界说导航行为。因为RallyTabRow的效果类似于BottomNavigation,因而您还应想一想,在导航和脱离意图地时是否要保存并康复意图地状况。例如,假如要翻滚到“Overview”屏幕底部,然后导航到“Accounts”屏幕,接着回来,那么您是否要保持翻滚方位?是否要重按RallyTabRow中的同一意图地,以从头加载屏幕状况?这些都是有效问题,因根据您自己的运用规划要求来确认。

咱们将介绍同一navigateSingleTopTo扩展函数中可供您运用的一些其他选项:

  • launchSingleTop = true-如上所述,这可保证回来仓库顶部最多只有给定意图地的一个副本
  • 在Rally运用中,这意味着,屡次重按赞同标签页不会启动同一意图地的多个副本
  • popUpTo(startDestination) { saveState = true }-弹出到导航图的开端意图地,以免在您挑选标签页时在回来仓库上堆集很多意图地
  • 在Rally中,这意味着,在任何意图地按下回来箭头都会将整个回来仓库弹出到“Overview”屏幕
  • restoreState = true-确认此导航操作是否应康复PopUpToBuilder.saveStatepopUpToSaveState特点之前保存的任何状况。请注意,假如之前未运用要导航到意图地ID保存任何状况,此项不会产生任何影响
  • 在Rally中,这意味着,重按同一标签页会保存屏幕上之间的数据和用户状况,而无需从头加载。

您能够将全部这些选项逐个增加到代码中,然后再增加每个选项后运转运用,并在增加每个标志后验证切当行为。这样一来,您就能够在实践中了解每个标志是怎么更改导航和回来仓库的状况。

import androidx.navigation.NavHostController
import androidx.navigation.NavGraph.Companion.findStartDestination
// ...
fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) {
        popUpTo(
            this@navigateSingleTopTo.graph.findStartDestination().id
        ) {
            saveState = true
        }
        launchSingleTop = true
        restoreState = true
    }

4.3、处理标签页界面问题

RallyTabRow虽然仍在运用手动导航机制,但会运用currentScreen变量来确认是翻开仍是收起各个标签页。

不过,在您更改完结后,currentScreen将不再更新。因而,RallyTabRow内翻开和收起所选标签页的操作不再起效果。

如需运用Compose Navigation从头启用此行为,您需求知道在每个时刻点显现的当时意图地是什么(或许用导航术语来说,当时回来仓库条意图顶部是什么),然后再次行为每次更改时更新RallyTabRow

如需以State的形式获取回来仓库中当时意图地的实时更新,您能够运用navController.currentBackStackEntryAsState(),然后获取器当时的destination:

import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()
        val currentBackStack by navController.currentBackStackEntryAsState()
        val currentDestination = currentBackStack?.destination
    }
}

currentBackStack?.destination会回来NavDestination,如需从头正确更新currentScreen,您需求主意设法将回来值NavDestination与Rally的三个首要屏幕可组合项之一进行匹配。您必须确认当时显现的意图地,以便随后将这些信息传递给RallyTabRow。如前所述,每个意图地都有一条仅有的道路,因而咱们能够运用此String道路作为类似ID,以进行严厉的对比并找到仅有匹配。

如需更新currentScreen,您需求迭代rallyTabRowScreen列表,以找到匹配道路,然后回来对应的RallyDestination。kotlin为此供给了一个快捷.find()函数:

import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...
@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()
        val currentBackStack by navController.currentBackStackEntryAsState()
        val currentDestination = currentBackStack?.destination
        // Change the variable to this and use Overview as a backup screen if this returns null
        val currentScreen = rallyTabRowScreens.find { it.route == currentDestination?.route } ?: Overview
    }
}

因为已将currentScreen传递给RallyTabRow,因而您能够运转运用,并验证标签页栏界面现在是否已相应更新。

五、将RallyDestination中提取屏幕可组合项

到现在为止,为简单起见,咱们一向运用RallyDestination接口中的screen特点以及经过该特点扩展的屏幕目标,将可组合界面增加到NavHost(RallyActivity.kt)中:

import com.example.compose.rally.ui.overview.OverviewScreen
NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
}

若要完结这一目标,正确且更简洁的方式是将可组合项直接增加到NavHost导航图中,然后从RallyDestination中提取。之后,RallyDestination和屏幕目标将仅保存导航专属信息(例如iconroute),还将与任何Compose界面相关的信息别离。

翻开RallyDestinations.kt。将每个屏幕的可组合项从RallyDestination目标的screen形参提取到NavHost中对应的composable函数中,以替换之前的.screen()调用,如下所示:

import com.example.compose.rally.ui.accounts.AccountsScreen
import com.example.compose.rally.ui.bills.BillsScreen
import com.example.compose.rally.ui.overview.OverviewScreen
NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        OverviewScreen()
    }
    composable(route = Accounts.route) {
        AccountsScreen()
    }
    composable(route = Bills.route) {
        BillsScreen()
    }
}

此时,您能够从RallyDestination以及目标中安全移出screen形参:

interface RallyDestination {
    val icon: ImageVector
    val route: String
}
object Overview: RallyDestination {
    override val icon = Icons.Filled.PieChart
    override val route = "overview"
}

再次运转运用,并验证全部是否像先前一样正常运转。现在,您现已完结此过程,接下来能够在可组合屏幕中设置点击事情了。

5.1、对OverviewScreen启用点击

现在,OverviewScreen中的全部点击事情都已被忽略。也就是说,“Accounts”和“Bills”的子部分“SEE ALL”按钮可供点击,但实践上不会跳转到任何方位。此过程的意图是为了这些点击事情启用导航。

OverviewScreen可组合项能够承受多个函数作为回调,以设置为点击事情,在本示例中,这应该是可让您转到AccountsScreenBillsScreen的导航操作。咱们来将这些导航回调传递给onClickSeeAllAccountsonClickSeeAllBills,以导航到相关意图地。

翻开RallyActivity.kt,在NavHost中找到OverviewScreen,然后将navController.navigateSingleTopTo(...)传递给具有相同道路的两个导航回调:

OverviewScreen(
    onClickSeeAllAccounts = {
        navController.navigateSingleTopTo(Accounts.route)
    },
    onClickSeeAllBills = {
        navController.navigateSingleTopTo(Bills.route)
    }
)

navController现在将取得满足的信息(例如切当的意图),可在用户点击按钮时导航到正确的意图地。假如您查看OverviewScreen的完结,便会发现这些回调已设置为相应的onClick形参:

@Composable
fun OverviewScreen(...) {
    // ...
    AccountsCard(
        onClickSeeAll = onClickSeeAllAccounts,
        onAccountClick = onAccountClick
    )
    // ...
    BillsCard(
        onClickSeeAll = onClickSeeAllBills
    )
}

如前所述,若能将navController保持在导航层次结构的顶层并将其供给到App可组合项等级(例如,不将其直接传递给OverviewScreen),就能够轻松地单独预览、重复运用和测验OverviewScreen可组合项,而不必依靠于实践或模仿的navController实例。此外,改为传递回调还可让您快速更改点击事情!