Navigation
主要學(xué)習(xí)內(nèi)容
- 將 Jetpack Navigation 與 Jetpack Compose 結(jié)合使用的基礎(chǔ)知識
- 在可組合項之間導(dǎo)航
- 使用必需和可選參數(shù)導(dǎo)航
- 使用深層鏈接導(dǎo)航
- 將 TabBar 集成到導(dǎo)航層次結(jié)構(gòu)中
- 測試導(dǎo)航
準(zhǔn)備工作
因為之后的代碼都是基于其中的項目進(jìn)行的,而且
Navigation
的學(xué)習(xí)是基于一個較完善的項目中進(jìn)行铸敏,存在多個界面之間的切換所以建議下載示例属桦,并通過
Import Project
方式導(dǎo)入其中的NavigationCodelab
項目
在解壓文件中的NavigationCodelab
目錄中存放本次學(xué)習(xí)的案例代碼
使用Navigation
在Rally
項目中使用Navigation
餐胀,遵循以下幾個步驟:
- 添加
Navigation
依賴項 - 設(shè)置
NavController
和NavHost
- 準(zhǔn)備路線
- 用
Navigation
替換原來的跳轉(zhuǎn)方式
添加依賴項
dependencies {
...
//目前最新穩(wěn)定版本
implementation "androidx.navigation:navigation-compose:2.4.2"
}
設(shè)置NavController和NavHost
NavController
是在 Compose 中使用 Navigation 時的核心組件:可以跟蹤組成應(yīng)用屏幕的可組合項的返回堆棧以及每個屏幕的狀態(tài)
NavController
執(zhí)行具體的頁面切換工作,所以必須先創(chuàng)建它才能進(jìn)行導(dǎo)航
在 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
可組合項相關(guān)聯(lián)蜈垮。NavHost
將 NavController
與導(dǎo)航圖相關(guān)聯(lián)雕什,導(dǎo)航圖用于指定您應(yīng)能夠在其間進(jìn)行導(dǎo)航的可組合項目的地
可組合項之間進(jìn)行導(dǎo)航時,NavHost
的內(nèi)容會自動進(jìn)行重組苹粟。導(dǎo)航圖中的每個可組合項目的地都與一個路線相關(guān)聯(lián)
//新建的方法有滑,代碼是從RallyApp中copy
//在該方法中修改完成跳轉(zhuǎn)邏輯
@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)) {
...
}
}
}
準(zhǔn)備路線
Rally
應(yīng)用程序具有三個界面:
- 概覽 - 所有賬戶和賬單的概覽
- 賬戶 - 查看所有賬戶信息
- 賬單 - 查看所有賬單信息
三個界面都是通過可組合項構(gòu)建的,我們需要將這些界面映射至Navigaiton
目的地中嵌削,并且將Overview
作為起始目的地
在 Compose 中使用 Navigation 時毛好,路線是一個 String
望艺,用于定義指向可組合項的路徑。我們可以將其視為指向特定目的地的隱式深層鏈接肌访。每個目的地都應(yīng)該有一條唯一的路線
本案例中荣茫,我們將使用RallyScreen
的name
屬性作為路線
在RallyAppWithNavigation
中創(chuàng)建的NavHost
取代之前的Box
,并傳入navController
场靴。此處以外NavHost
還需要一個String類型的startDestination
啡莉,我們傳入RallyScreen.Overview.name
。此外旨剥,創(chuàng)建一個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
中構(gòu)建導(dǎo)航圖
NavHost
創(chuàng)建使用 lambda 來構(gòu)建導(dǎo)航圖咧欣。我們可以使用 composable()
方法向?qū)Ш浇Y(jié)構(gòu)添加內(nèi)容。此方法需要提供一個路線以及應(yīng)關(guān)聯(lián)到相應(yīng)目的地的可組合項:
@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)
}
}
}
}
然而此時運行項目轨帜,點擊導(dǎo)航欄元素并不會發(fā)生界面跳轉(zhuǎn)
用Navigation替換原來的跳轉(zhuǎn)方式
在Navigation
中實現(xiàn)具體的頁面切換工作是NavCotroller
魄咕,NavCotroller
通過navigation()
方法進(jìn)行切換顯示的可組合項,使用navigate()
接受代表目的地路線的單個 String
參數(shù)
我們需要在RallyTabRow
的onTabSelected
事件中添加跳轉(zhuǎn)邏輯
@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
狀態(tài)不再更新哮兰,也就是RallyTabRow
內(nèi)容的選中展開和收合功能不會運轉(zhuǎn)。如果要使得RallyTabRow
啟用這項功能就需要更新currentScreen
狀態(tài)
不過在Navigation
中保留了返回堆棧苟弛,并可以將返回堆棧元素作為狀態(tài)返回喝滞,通過這個狀態(tài),我們可以對返回堆棧的變更做出反應(yīng)膏秫,比如獲取當(dāng)前的路徑
@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"按鈕是可點擊的缤削,但是不會進(jìn)行跳轉(zhuǎn)操作
OverviewBody
可以接受幾個函數(shù)作為點擊事件的回調(diào)窘哈。我們實現(xiàn)onClickSeeAllAccounts
和onClickSeeAllBills
實現(xiàn)導(dǎo)航到相關(guān)目的地
OverviewBody(
onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
onClickSeeAllBills = { navController.navigate(Bills.name) },
)
示例中大量使用單向數(shù)據(jù)流,通過事件上傳亭敢、狀態(tài)下流的做法滚婉,提供了可組合項的復(fù)用性
參數(shù)導(dǎo)航
Navigation Compose 還支持在可組合項目的地之間傳遞參數(shù)。為此帅刀,您需要向路線中添加參數(shù)占位符
參數(shù)導(dǎo)航使得路線動態(tài)化让腹,通過將一個或多個參數(shù)傳遞到路由并調(diào)整參數(shù)類型或默認(rèn)值來使路由行為動態(tài)化
我們通過為Rally
增加點擊賬戶,跳轉(zhuǎn)至顯示單個帳戶的詳細(xì)信息界面來學(xué)習(xí)該內(nèi)容
在NavHost
中添加新的路線 ("$accountsName/{name}"
) 劝篷,同時我們還要指定傳遞參數(shù)的類型
@Composable
fun RallyAppWithNavigation() {
...
Scaffold(
...
) { innerPadding ->
NavHost(
navController = navController,
startDestination = RallyScreen.Overview.name,
modifier = Modifier.padding(innerPadding)
) {
...
val accountsName = RallyScreen.Accounts.name
composable("$accountsName/{name}",
//傳遞的參數(shù)可能不止一個哨鸭,所以使用List集合
arguments = listOf(
//名字為name的參數(shù)類型為String
navArgument("name") {
type = NavType.StringType
}
)
) { ... }
}
}
}
通過向路線中添加參數(shù)占位符的方式傳遞參數(shù),如上所示
$accountsName/{name}
使用美元符號
$
來轉(zhuǎn)義變量名娇妓,{argument}
表示一個變量
composable
會接收到NavBackStackEntry
對象像鸡,NavBackStackEntry
會根據(jù)指定的路徑和參數(shù)對進(jìn)行分析
我們可以使用NavBackStackEntry
獲取參數(shù)值,即name
,然后根據(jù)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 還支持可選的導(dǎo)航參數(shù)只估≈救海可選參數(shù)與必需參數(shù)有以下兩點不同:
- 可選參數(shù)必須使用查詢參數(shù)語法 (
"?argName={argName}"
) 來添加- 可選參數(shù)必須具有
defaultValue
集或nullable = true
(將默認(rèn)值隱式設(shè)置為null
)多個可選參數(shù)之間用
&
連接,中間不能存在空格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") //可選類型可以調(diào)換順序 navController.navigate("$accountsName/$name?arg2=20&arg1=10")
默認(rèn)情況下蛔钙,所有參數(shù)都會被解析為字符串
導(dǎo)航至SingleAccountBody
若要將參數(shù)傳遞到目的地锌云,我們需要在 navigate
中根據(jù)參數(shù)位置填寫具體的值
我們需要在OverviewBody
中的onAccountClick
和AccountsBody
中的onAccountClick
事件中添加跳轉(zhuǎn)邏輯
@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")
})
}
...
}
}
}
此時運行應(yīng)用程序時,單擊每個帳戶并將進(jìn)入一個屏幕吁脱,顯示給定帳戶的數(shù)據(jù)
深層鏈接
除了參數(shù)導(dǎo)航之外桑涎,您還可以使用 深層鏈接 將應(yīng)用中的目標(biāo)公開給第三方應(yīng)用
添加 intent-filter
首先,將深層鏈接添加至 AndroidManifest.xml
兼贡,我們需要使用VIEW
建立RallyActivity
的意圖選擇器攻冷,并指定類別為BROWSABLE
和DEFAULT
然后使用data
標(biāo)簽中指定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}參數(shù)
回應(yīng)深層鏈接
現(xiàn)在我們可以在RallyActivity
中回應(yīng)傳入的意圖
在composable
中使用 navDeepLink
函數(shù)增加deepLinks
參數(shù),在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")
})
}
我們可以在當(dāng)前應(yīng)用或別的應(yīng)用中使用深層鏈接方式跳轉(zhuǎn)至該頁面
當(dāng)前應(yīng)用使用深層鏈接
navController.navigate(Uri.parse("rally://accounts/$name"))
其他應(yīng)用使用深層鏈接
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