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有三个主屏幕作为可组合项:
-
OverviewScreen
– 全部财政买卖和提醒的概览 -
AccountsScreen
– 有关现有账户的数据剖析 -
BillsScreen
– 预定支出
在屏幕的最顶部,Rally运用自界说标签页栏可组合项(RallyTabRow) 在这三个屏幕之间进行导航。点按每个图标应该会翻开当时所选内容,您也会转到其对应的屏幕:
在导航到这些可组合屏幕时,您还能够将它们视为导航意图地,因为咱们期望在特定时刻抵达各个意图地。这些意图地是在RallyDestinations.kt
文件中预界说的。
在该文件中,您会找到全部三个界说为目标的首要意图地(Override
,Accounts
和Bills
),以及一个日后要增加到运用的SingleAccount
。每个目标都从RallyDestination
接口扩展,并且包括有关每个意图地的必要信息,以便进行导航:
- 顶栏的
icon
- 字符串
route
(这对Compose Navigation而言是必须的,可作为指向相应意图地的路径) -
screen
,表明相应意图地的整个可组合项。
运转运用时,您会发现自己实践上能够在当时运用顶栏的意图地之间导航。但是,运用实践上并未运用Compose Navigation,但它当时运用的导航机制依靠手动切换一些可组合项和触发重组来显现新内容。
三、迁移到Compose Navigation
迁移到Jetpack Compose所涉及的根本过程如下:
- 增加最新的
Compose Navigation依靠项
- 设置
NavController
- 增加
NavHost
并创立导航图 - 准备道路以在不同的运用意图地之间导航
- 将当时导航机制替换为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
中界说。咱们现已说到,每个意图地都有界说的icon
、route
和screen
:
接下来,将这些意图地增加到导航图中,其间Overview
作为运用启动时的其实意图地。
运用Compose中的Navigation时,导航图中的每个可组合意图地都与一个道路相关联。道路用字符串表明,用于界说指向可组合项的路径,并指引您的navController
抵达正确的方位。您能够将其视为指向特定意图地的隐式深层链接。每个意图地都必须有一条仅有的道路。
为此,咱们会运用每个RallyDestination
目标的route
特点。例如,Overview.route
是将您转到Overview
屏幕可组合项的录像。
3.4、运用导航图调用NavHost可组合项
下一步是增加NavHost
并创立导航图。
Navigation的3个首要部分是NavController
、NavGraph
和NavHost
。NavController
始终与一个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与导航集成
在此过程中,您需求将RallyTabRow
与navController
和导航图链接起来,以便其导航到正确的意图地。
为此,您需求运用新的navController
,为RallyTabRow
的onTabSelected
回调界说正确的导航操作。此回调界说了挑选特定标签页图标时产生的状况,并经过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
中的各个标签页是否确实会导航到正确的可组合意图地。不过,您现在可能现已注意到以下两个问题:
- 重按同一个标签页会启动同一意图地的多个副本
- 标签页的界面与所显现的正确意图地不符,也就是说,无法正常翻开和收起所选标签页:
下面咱们来处理这两个问题!
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.saveState
或popUpToSaveState
特点之前保存的任何状况。请注意,假如之前未运用要导航到意图地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
和屏幕目标将仅保存导航专属信息(例如icon
和route
),还将与任何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
可组合项能够承受多个函数作为回调,以设置为点击事情,在本示例中,这应该是可让您转到AccountsScreen
或BillsScreen
的导航操作。咱们来将这些导航回调传递给onClickSeeAllAccounts
和onClickSeeAllBills
,以导航到相关意图地。
翻开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
实例。此外,改为传递回调还可让您快速更改点击事情!