02_Compose導(dǎo)航Navigation

導(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 !

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載铆隘,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者膀钠。
  • 序言:七十年代末裹虫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子雳窟,更是在濱河造成了極大的恐慌匣屡,老刑警劉巖砚蓬,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異惩坑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)趾痘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門扼脐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奋刽,“玉大人,你說我怎么就攤上這事肚吏∠粱辏” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵斋泄,是天一觀的道長镐牺。 經(jīng)常有香客問我,道長募胃,這世上最難降的妖魔是什么畦浓? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任讶请,我火速辦了婚禮,結(jié)果婚禮上抹蚀,老公的妹妹穿的比我還像新娘。我一直安慰自己环壤,他們只是感情好郑现,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布荧降。 她就那樣靜靜地躺著,像睡著了一般辛友。 火紅的嫁衣襯著肌膚如雪剪返。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天邑滨,我揣著相機(jī)與錄音掖看,去河邊找鬼面哥。 笑死,一個(gè)胖子當(dāng)著我的面吹牛尚卫,可吹牛的內(nèi)容都是我干的焕毫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼循签,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼疙咸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起乞旦,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤兰粉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后玖姑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年甜孤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了畏腕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡二跋,死狀恐怖流昏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谚鄙,我是刑警寧澤刁绒,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布知市,位于F島的核電站,受9級(jí)特大地震影響嫂丙,放射性物質(zhì)發(fā)生泄漏跟啤。R本人自食惡果不足惜唉锌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一竿奏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绿语,春花似錦候址、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽良漱。三九已至,卻和暖如春矾兜,著一層夾襖步出監(jiān)牢的瞬間患久,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國打工返帕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留篙挽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓链韭,卻偏偏與公主長得像煮落,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子儡陨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容