Navigation
主要学习内容
- 将 Jetpack Navigation 与 Jetpack Compose 结合使用的基础知识
- 在可组合项之间导航
- 使用必需和可选参数导航
- 使用深层链接导航
- 将 TabBar 集成到导航层次结构中
- 测试导航
准备工作
因为之后的代码都是基于其中的项目进行的,而且
Navigation
的学习是基于一个较完善的项目中进行,存在多个界面之间的切换所以建议下载示例,并通过
Import Project
方式导入其中的NavigationCodelab
项目
在解压文件中的NavigationCodelab
目录中存放本次学习的案例代码
使用Navigation
在Rally
项目中使用Navigation
,遵循以下几个步骤:
- 添加
Navigation
依赖项 - 设置
NavController
和NavHost
- 准备路线
- 用
Navigation
替换原来的跳转方式
添加依赖项
dependencies {
...
//目前最新稳定版本
implementation "androidx.navigation:navigation-compose:2.4.2"
}
设置NavController和NavHost
NavController
是在 Compose 中使用 Navigation 时的核心组件:可以跟踪组成应用屏幕的可组合项的返回堆栈以及每个屏幕的状态
NavController
执行具体的页面切换工作,所以必须先创建它才能进行导航
在 Compose 中,我们通过使用 rememberNavController()
获取到NavHostController
实例
NavHostController
是``NavController`的子类
@Composable public fun rememberNavController( vararg navigators: Navigator<out NavDestination> ): NavHostController { val context = LocalContext.current //可以看到 NavHostController 还是具有保存功能的 return rememberSaveable(inputs = navigators, saver = NavControllerSaver(context)) { createNavController(context) }.apply { for (navigator in navigators) { navigatorProvider.addNavigator(navigator) } } }
每个NavController
都必须与一个 NavHost
可组合项相关联。NavHost
将 NavController
与导航图相关联,导航图用于指定您应能够在其间进行导航的可组合项目的地
可组合项之间进行导航时,NavHost
的内容会自动进行重组。导航图中的每个可组合项目的地都与一个路线相关联
//新建的方法,代码是从RallyApp中copy
//在该方法中修改完成跳转逻辑
@Composable
fun RallyAppWithNavigation() {
val allScreens = RallyScreen.values().toList()
var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
val navController = rememberNavController()
Scaffold(
topBar = {
RallyTabRow(
allScreens = allScreens,
onTabSelected = { screen -> currentScreen = screen },
currentScreen = currentScreen
)
}
) { innerPadding ->
NavHost(navController = navController, startDestination = ""){}
Box(Modifier.padding(innerPadding)) {
...
}
}
}
准备路线
Rally
应用程序具有三个界面:
- 概览 - 所有账户和账单的概览
- 账户 - 查看所有账户信息
- 账单 - 查看所有账单信息
三个界面都是通过可组合项构建的,我们需要将这些界面映射至Navigaiton
目的地中,并且将Overview
作为起始目的地
在 Compose 中使用 Navigation 时,路线是一个 String
,用于定义指向可组合项的路径。我们可以将其视为指向特定目的地的隐式深层链接。每个目的地都应该有一条唯一的路线
本案例中,我们将使用RallyScreen
的name
属性作为路线
在RallyAppWithNavigation
中创建的NavHost
取代之前的Box
,并传入navController
。此处以外NavHost
还需要一个String类型的startDestination
,我们传入RallyScreen.Overview.name
。此外,创建一个Modifier
将填充传递到NavHost
@Composable
fun RallyAppWithNavigation() {
val allScreens = RallyScreen.values().toList()
var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
val navController = rememberNavController()
Scaffold( ... ) { innerPadding ->
NavHost(
navController = navController,
startDestination = RallyScreen.Overview.name,
modifier = Modifier.padding(innerPadding)
) {
}
}
}
NavHost
方法@Composable public fun NavHost( navController: NavHostController, startDestination: String, modifier: Modifier = Modifier, route: String? = null, builder: NavGraphBuilder.() -> Unit ) { ... }
navController
:NavHostController
实例对象startDestination
:起始目的地builder
:在NavGraphBuilder
中构建导航图
NavHost
创建使用 lambda 来构建导航图。我们可以使用 composable()
方法向导航结构添加内容。此方法需要提供一个路线以及应关联到相应目的地的可组合项:
@Composable
fun RallyAppWithNavigation() {
val allScreens = RallyScreen.values().toList()
var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
val navController = rememberNavController()
Scaffold( ... ) { innerPadding ->
NavHost(
navController = navController,
startDestination = RallyScreen.Overview.name,
modifier = Modifier.padding(innerPadding)
) {
composable(RallyScreen.Overview.name){
OverviewBody()
}
composable(RallyScreen.Accounts.name){
AccountsBody(accounts = UserData.accounts)
}
composable(RallyScreen.Bills.name){
BillsBody(bills = UserData.bills)
}
}
}
}
然而此时运行项目,点击导航栏元素并不会发生界面跳转
用Navigation替换原来的跳转方式
在Navigation
中实现具体的页面切换工作是NavCotroller
,NavCotroller
通过navigation()
方法进行切换显示的可组合项,使用navigate()
接受代表目的地路线的单个 String
参数
我们需要在RallyTabRow
的onTabSelected
事件中添加跳转逻辑
@Composable
fun RallyAppWithNavigation() {
val allScreens = RallyScreen.values().toList()
var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
val navController = rememberNavController()
Scaffold(
topBar = {
RallyTabRow(
allScreens = allScreens,
onTabSelected = { screen ->
//currentScreen = screen
navController.navigate(currentScreen.name)
},
currentScreen = currentScreen
)
}
) { innerPadding ->
...
}
}
修改onTabSelected
事件后,currentScreen
状态不再更新,也就是RallyTabRow
内容的选中展开和收合功能不会运转。如果要使得RallyTabRow
启用这项功能就需要更新currentScreen
状态
不过在Navigation
中保留了返回堆栈,并可以将返回堆栈元素作为状态返回,通过这个状态,我们可以对返回堆栈的变更做出反应,比如获取当前的路径
@Composable
fun RallyAppWithNavigation() {
val allScreens = RallyScreen.values().toList()
val navController = rememberNavController()
val backstackEntry by navController.currentBackStackEntryAsState()
var currentScreen =
RallyScreen.fromRoute(backstackEntry?.destination?.route ?: RallyScreen.Overview.name)
Scaffold(
topBar = {
RallyTabRow(
allScreens = allScreens,
onTabSelected = { screen ->
navController.navigate(screen.name)
},
currentScreen = currentScreen
)
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = RallyScreen.Overview.name,
modifier = Modifier.padding(innerPadding)
) {
...
}
}
}
启用OverviewScreen的点击
在此项目中,OverviewBody
忽略了点击事件,其中的"SEE ALL"按钮是可点击的,但是不会进行跳转操作
OverviewBody
可以接受几个函数作为点击事件的回调。我们实现onClickSeeAllAccounts
和onClickSeeAllBills
实现导航到相关目的地
OverviewBody(
onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
onClickSeeAllBills = { navController.navigate(Bills.name) },
)
示例中大量使用单向数据流,通过事件上传、状态下流的做法,提供了可组合项的复用性
参数导航
Navigation Compose 还支持在可组合项目的地之间传递参数。为此,您需要向路线中添加参数占位符
参数导航使得路线动态化,通过将一个或多个参数传递到路由并调整参数类型或默认值来使路由行为动态化
我们通过为Rally
增加点击账户,跳转至显示单个帐户的详细信息界面来学习该内容
在NavHost
中添加新的路线 ("$accountsName/{name}"
) ,同时我们还要指定传递参数的类型
@Composable
fun RallyAppWithNavigation() {
...
Scaffold(
...
) { innerPadding ->
NavHost(
navController = navController,
startDestination = RallyScreen.Overview.name,
modifier = Modifier.padding(innerPadding)
) {
...
val accountsName = RallyScreen.Accounts.name
composable("$accountsName/{name}",
//传递的参数可能不止一个,所以使用List集合
arguments = listOf(
//名字为name的参数类型为String
navArgument("name") {
type = NavType.StringType
}
)
) { ... }
}
}
}
通过向路线中添加参数占位符的方式传递参数,如上所示
$accountsName/{name}
使用美元符号
$
来转义变量名,{argument}
表示一个变量
composable
会接收到NavBackStackEntry
对象,NavBackStackEntry
会根据指定的路径和参数对进行分析
我们可以使用NavBackStackEntry
获取参数值,即name
,然后根据name
查找到UserData
并传递给SingleAccountBody
可组合项
val accountsName = RallyScreen.Accounts.name
composable("$accountsName/{name}",
arguments = listOf(
navArgument("name") {
type = NavType.StringType
}
)
) { backStackEntry ->
val accountName = backStackEntry.arguments?.getString("name")
val account = UserData.getAccount(accountName)
SingleAccountBody(account = account)
}
composable
方法代码public fun NavGraphBuilder.composable( route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), content: @Composable (NavBackStackEntry) -> Unit )
Navigation Compose 还支持可选的导航参数。可选参数与必需参数有以下两点不同:
- 可选参数必须使用查询参数语法 (
"?argName={argName}"
) 来添加- 可选参数必须具有
defaultValue
集或nullable = true
(将默认值隐式设置为null
)多个可选参数之间用
&
连接,中间不能存在空格composable("$accountsName/{name}?arg1={arg1}&arg2={arg2}", arguments = listOf( navArgument("name") { type = NavType.StringType }, navArgument("arg1") { defaultValue = 100 type = NavType.IntType }, navArgument("arg2") { defaultValue = 200 type = NavType.IntType } ) )
navController.navigate("$accountsName/$name?arg2=20") //可选类型可以调换顺序 navController.navigate("$accountsName/$name?arg2=20&arg1=10")
默认情况下,所有参数都会被解析为字符串
导航至SingleAccountBody
若要将参数传递到目的地,我们需要在 navigate
中根据参数位置填写具体的值
我们需要在OverviewBody
中的onAccountClick
和AccountsBody
中的onAccountClick
事件中添加跳转逻辑
@Composable
fun RallyAppWithNavigation() {
...
Scaffold(
...
) { innerPadding ->
NavHost(
navController = navController,
startDestination = RallyScreen.Overview.name,
modifier = Modifier.padding(innerPadding)
) {
val accountsName = RallyScreen.Accounts.name
composable(RallyScreen.Overview.name) {
OverviewBody(onAccountClick = { name ->
navController.navigate("$accountsName/$name")
})
}
composable(RallyScreen.Accounts.name) {
AccountsBody(accounts = UserData.accounts,
onAccountClick = { name ->
navController.navigate("$accountsName/$name")
})
}
...
}
}
}
此时运行应用程序时,单击每个帐户并将进入一个屏幕,显示给定帐户的数据
深层链接
除了参数导航之外,您还可以使用 深层链接 将应用中的目标公开给第三方应用
添加 intent-filter
首先,将深层链接添加至 AndroidManifest.xml
,我们需要使用VIEW
建立RallyActivity
的意图选择器,并指定类别为BROWSABLE
和DEFAULT
然后使用data
标签中指定scheme
和host
这个intent-filter
会使用rally://accounts/{name}
的格式作为深层链接地址
<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>
<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="accounts" />
</intent-filter>
</activity>
不需要在
AndroidManifest.xml
中申明{name}参数
回应深层链接
现在我们可以在RallyActivity
中回应传入的意图
在composable
中使用 navDeepLink
函数增加deepLinks
参数,在navDeepLink
中将uriPattern
赋值为符合intent-filter
的格式
composable(route = RallyScreen.Accounts.name,
//深层链接格式可以存在多个
deepLinks = listOf(navDeepLink {
uriPattern = "rally://accounts/{name}"
})
) {
AccountsBody(accounts = UserData.accounts,
onAccountClick = { name ->
navController.navigate("$accountsName/$name")
})
}
我们可以在当前应用或别的应用中使用深层链接方式跳转至该页面
当前应用使用深层链接
navController.navigate(Uri.parse("rally://accounts/$name"))
其他应用使用深层链接
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface {
DeepLinkNavigationButton{
val deepLinkIntent = Intent()
deepLinkIntent.data="rally://accounts/Checking".toUri()
deepLinkIntent.flags= Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(deepLinkIntent)
}
}
}
}
@Composable
fun DeepLinkNavigationButton(onClick:()->Unit) {
Button(onClick = {
try{
onClick()
}catch (e:Exception){
Log.e("navigation", "exception: ${e.message}" )
e.printStackTrace()
}
}){
Text(text = "深度链接")
}
}
也可以在模拟器上使用 ADB 来测试深层链接
adb shell am start -d "rally://accounts/Checking" -a android.intent.action.VIEW