導(dǎo)航Navigation
(1)依賴
????在Composable之間進(jìn)行切換扣孟,就需要用到導(dǎo)航Navigation組件。它是一個(gè)庫玫霎,并不是系統(tǒng)Framework里的,所以在使用前,需要添加依賴庶近,如下:
dependencies {
def nav_version = "2.5.3"
implementation("androidx.navigation:navigation-compose:$nav_version")
}
(2)NavController
????NavController是導(dǎo)航組件的中心API,它是有狀態(tài)的鼻种。通過Stack保存著各種Composable組件的狀態(tài),以方便在不同的Screen之間切換叉钥。創(chuàng)建一個(gè)NavController的方式如下:
val navController = rememberNavController()
(3)NavHost
???? 每一個(gè)NavController都必須關(guān)聯(lián)一個(gè)NavHost組件罢缸。NavHost像是一個(gè)帶著導(dǎo)航icon的NavController。每一個(gè)icon(姑且這么叫沼侣,也可以是name)都對(duì)應(yīng)一個(gè)目的頁面(Composable組件)。這里引進(jìn)一個(gè)新術(shù)語:路由Route养铸,它是指向一個(gè)目的頁面的路徑轧膘,可以有很深的層次。使用示例:
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
????切換到另外一個(gè)Composable組件:
navController.navigate("friendslist")
????在導(dǎo)航前鳞滨,清除某些back stack:
// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home")
}
????清除所有back stack蟆淀,包括"home":
// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home") { inclusive = true }
}
????singleTop模式:
// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
launchSingleTop = true
}
????這里再引進(jìn)一個(gè)術(shù)語:?jiǎn)我豢尚艁碓丛瓌t,the single source of truth principle褒链。應(yīng)用在導(dǎo)航這里疑苔,即是說導(dǎo)航的切換應(yīng)該盡可能的放在更高的層級(jí)上。例如惦费,一個(gè)Button的點(diǎn)擊觸發(fā)了頁面的跳轉(zhuǎn)薪贫,你可以把跳轉(zhuǎn)代碼寫在Button的onClick回調(diào)里『罄祝可如果有多個(gè)Button呢?或者有多個(gè)觸發(fā)點(diǎn)呢勉抓?每個(gè)地方寫一次固然可以實(shí)現(xiàn)相應(yīng)的功能贾漏,但如果只寫一次不是更好嗎藕筋?通過將跳轉(zhuǎn)代碼寫在一個(gè)較高層級(jí)的函數(shù)里,傳遞相應(yīng)的lambda到各個(gè)觸發(fā)點(diǎn)伍掀,就能解決這個(gè)問題暇藏。示例如下:
@Composable
fun MyAppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = "profile"
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable("profile") {
ProfileScreen(
onNavigateToFriends = { navController.navigate("friendsList") },
/*...*/
)
}
composable("friendslist") { FriendsListScreen(/*...*/) }
}
}
@Composable
fun ProfileScreen(
onNavigateToFriends: () -> Unit,
/*...*/
) {
/*...*/
Button(onClick = onNavigateToFriends) {
Text(text = "See friends list")
}
}
????上面示例的跳轉(zhuǎn)盐碱,是在名為"profile"的Composable里。比起B(yǎng)utton瓮顽,這是一個(gè)相對(duì)較高的層級(jí)。
????這里再引入一個(gè)術(shù)語:狀態(tài)提升hoist state缕贡,將可組合函數(shù)(Composable Function拣播,后續(xù)簡(jiǎn)稱CF)暴露給Caller贮配,該Caller知道如何處理相應(yīng)的邏輯,是狀態(tài)提升的一種實(shí)踐方式牧嫉。例如上例中酣藻,將Button的點(diǎn)擊事件鳍置,暴露給了NavHost。也即是說税产,如果需要參數(shù),也是在NavHost中處理撞羽,不需要關(guān)心具體Button的可能狀態(tài)诀紊。
(4)帶參數(shù)的導(dǎo)航
????導(dǎo)航是可以攜帶參數(shù)的,使用語法如下:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
????使用確切的類型:
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
????其中navArgument()方法創(chuàng)建的是一個(gè)NamedNavArgument對(duì)象笤喳。
????如果想提取參數(shù)碌宴,那么:
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
????其中backStackEntry是NavBackStackEntry類型的。
????導(dǎo)航時(shí)呜象,傳入?yún)?shù):
navController.navigate("profile/user1234")
????注意點(diǎn):使用導(dǎo)航傳遞數(shù)據(jù)時(shí)八孝,應(yīng)該傳遞一些簡(jiǎn)單的、必要的數(shù)據(jù)子姜,如標(biāo)識(shí)ID等楼入。傳遞復(fù)雜的數(shù)據(jù)是強(qiáng)烈不建議的。如果有這樣的需求遥赚,可以將這些數(shù)據(jù)保存在數(shù)據(jù)層阐肤。導(dǎo)航到新頁面后,根據(jù)ID到數(shù)據(jù)層獲取愧薛。
(5)可選參數(shù)
????添加可選參數(shù)衫画,有兩點(diǎn)要求削罩。一是必須使用問號(hào)語法费奸,二是必須提供默認(rèn)值进陡。示例如下:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
(6)深層鏈接Deep Link
????Deep Link可以響應(yīng)其他頁面或者外部App的跳轉(zhuǎn)。實(shí)現(xiàn)自定義協(xié)議是它的使用場(chǎng)景之一换况。一個(gè)示例:
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
????navDeepLink函數(shù)會(huì)創(chuàng)建一個(gè)NavDeepLink對(duì)象盗蟆,它負(fù)責(zé)管理深層鏈接喳资。
????但是上面這種方式只能響應(yīng)本App內(nèi)的跳轉(zhuǎn),如果想接收外部App的請(qǐng)求鲜滩,需要在manifest中配置节值,如下:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
????Deep link也適用于PendingIntent,示例:
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
(7)嵌套導(dǎo)航
????一些大的模塊嗓蘑,可能會(huì)包含許多小的模塊匿乃。那么此時(shí)就需要用到嵌套導(dǎo)航了。嵌套導(dǎo)航有助于模塊化的細(xì)致劃分泄隔。使用示例:
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") { ... }
}
...
}
????將它作為一個(gè)擴(kuò)展函數(shù)實(shí)現(xiàn)佛嬉,以方便使用闸天,如下:
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
}
????在NavHost中使用它:
NavHost(navController, startDestination = "home") {
...
loginGraph(navController)
...
}
(8)與底部導(dǎo)航欄的集成
????先添加底部導(dǎo)航欄所需的依賴:
dependencies {
implementation("androidx.compose.material:material:1.3.1")
}
android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
kotlinOptions {
jvmTarget = "1.8"
}
}
????創(chuàng)建sealed Screen 号枕,如下:
sealed class Screen(val route: String, @StringRes val resourceId: Int) {
object Profile : Screen("profile", R.string.profile)
object FriendsList : Screen("friendslist", R.string.friends_list)
}
????BottomNavigationItem需要用到的items:
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
????最終示例:
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
composable(Screen.Profile.route) { Profile(navController) }
composable(Screen.FriendsList.route) { FriendsList(navController) }
}
}
(9)NavHost的使用限制
????如果想用NavHost作為導(dǎo)航葱淳,那么必須所有的組件都是Composable。如果是View和ComposeView的混合模式艳狐,即頁面即有原來的View體系皿桑,又有ComposeView,是不能使用NavHost的镀虐。這種情況下沟绪,使用Fragment來實(shí)現(xiàn)。
???? Fragment中雖然不能直接使用NavHost恨旱,但也可以使用Compose導(dǎo)航功能坝疼。首先創(chuàng)建一個(gè)Composable項(xiàng),如下:
@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
????然后仪芒,在Fragment中腿椎,使用它來實(shí)現(xiàn)導(dǎo)航功能,如下:
class MyFragment : Fragment() {
override fun onCreateView(/* ... */): View {
return ComposeView(requireContext()).apply {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
}
}
????Over !