02_Compose導(dǎo)航Navigation

導(dǎo)航Navigation

(1)依賴

????在Composable之間進(jìn)行切換,就需要用到導(dǎo)航Navigation組件。它是一個庫,并不是系統(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)建一個NavController的方式如下:

val navController = rememberNavController()

(3)NavHost

???? 每一個NavController都必須關(guān)聯(lián)一個NavHost組件。NavHost像是一個帶著導(dǎo)航icon的NavController。每一個icon(姑且這么叫,也可以是name)都對應(yīng)一個目的頁面(Composable組件)。這里引進(jìn)一個新術(shù)語:路由Route,它是指向一個目的頁面的路徑,可以有很深的層次。使用示例:

NavHost(navController = navController, startDestination = "profile") {
    composable("profile") { Profile(/*...*/) }
    composable("friendslist") { FriendsList(/*...*/) }
    /*...*/
}

????切換到另外一個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)一個術(shù)語:單一可信來源原則,the single source of truth principle。應(yīng)用在導(dǎo)航這里,即是說導(dǎo)航的切換應(yīng)該盡可能的放在更高的層級上。例如,一個Button的點(diǎn)擊觸發(fā)了頁面的跳轉(zhuǎn),你可以把跳轉(zhuǎn)代碼寫在Button的onClick回調(diào)里??扇绻卸鄠€Button呢?或者有多個觸發(fā)點(diǎn)呢?每個地方寫一次固然可以實(shí)現(xiàn)相應(yīng)的功能,但如果只寫一次不是更好嗎?通過將跳轉(zhuǎn)代碼寫在一個較高層級的函數(shù)里,傳遞相應(yīng)的lambda到各個觸發(fā)點(diǎn),就能解決這個問題。示例如下:

@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,這是一個相對較高的層級。
????這里再引入一個術(shù)語:狀態(tài)提升hoist state,將可組合函數(shù)(Composable Function,后續(xù)簡稱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)建的是一個NamedNavArgument對象。
????如果想提取參數(shù),那么:

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

????其中backStackEntry是NavBackStackEntry類型的。
????導(dǎo)航時,傳入?yún)?shù):

navController.navigate("profile/user1234")

????注意點(diǎn):使用導(dǎo)航傳遞數(shù)據(jù)時,應(yīng)該傳遞一些簡單的、必要的數(shù)據(jù),如標(biāo)識ID等。傳遞復(fù)雜的數(shù)據(jù)是強(qiáng)烈不建議的。如果有這樣的需求,可以將這些數(shù)據(jù)保存在數(shù)據(jù)層。導(dǎo)航到新頁面后,根據(jù)ID到數(shù)據(jù)層獲取。

(5)可選參數(shù)

????添加可選參數(shù),有兩點(diǎn)要求。一是必須使用問號語法,二是必須提供默認(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é)議是它的使用場景之一。一個示例:

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ù)會創(chuàng)建一個NavDeepLink對象,它負(fù)責(zé)管理深層鏈接。
????但是上面這種方式只能響應(yīng)本App內(nèi)的跳轉(zhuǎn),如果想接收外部App的請求,需要在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)航

????一些大的模塊,可能會包含許多小的模塊。那么此時就需要用到嵌套導(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") { ... }
    }
    ...
}

????將它作為一個擴(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)建一個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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容