使用 Compose 瀏覽內容

Navigation 元件Jetpack Compose 應用程式提供支援。您可以瀏覽不同可組合元件,同時也可運用 Navigation 元件的基礎架構和功能。

設定

如要支援 Compose,請在應用程式模組的 build.gradle 檔案中使用下列依附元件:

Groovy

dependencies {
    def nav_version = "2.4.2"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    def nav_version = "2.4.2"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

開始

NavController 是 Navigation 元件的中心 API。它可以設定狀態,可追蹤組成應用程式熒幕可組成元件的返回堆疊,以及各個熒幕的狀態。

您可以在可組合元件中使用 rememberNavController() 方法,建立 NavController

val navController = rememberNavController()

您必須在可組合元件階層中的某個位置建立 NavController,讓所有需要參照其的可組合元件都能對其進行存取。這種做法符合狀態升降的原則,您可以使用 NavController 和它透過 currentBackStackEntryAsState() 提供的狀態,作為更新熒幕外可組合元件的真實來源。如需這項功能的範例,請參閱與底部導覽列整合一文。

建立 NavHost

每個 NavController 都必須與一個 NavHost 建立關聯。NavHost 會將 NavController 連結至導覽圖表,該圖表指定了您可在哪些可組合目的地間進行瀏覽。在您瀏覽可組合元件時,系統會自動 重寫 NavHost 的內容。導覽圖表中的每個可組合目的地都與一個 路線 相關聯。

建立 NavHost 時,需要使用先前透過 rememberNavController() 建立的 NavController,以及圖表的起始目的地的路徑。建立 NavHost 時,會使用 導覽 Kotlin DSL 的 lambda 語法來建構導覽圖表。您可以使用 composable() 方法新增至導覽結構。這個方法需要您提供路徑和應連結至目的地的可組合元件:

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

如要前往導覽圖表中的可組合目的地,您必須使用 navigate() 方法。navigate() 採用代表目的地路徑的單一 String 參數。如要瀏覽導覽圖表中的可組合元件,請呼叫 navigate()

@Composable
fun Profile(navController: NavController) {
    /*...*/
    Button(onClick = { navController.navigate("friendslist") }) {
        Text(text = "Navigate next")
    }
    /*...*/
}

您應該只將 navigate() 當做回呼的一部分來使用,而不是將其作為可組合元件的一部分進行呼叫,以免每次重編時都呼叫 navigate()

根據預設,navigate() 會將新的目的地新增至返回堆疊中。您可以為 navigate() 呼叫附加其他導覽選項,藉此修改 navigate 的行為:

// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
    popUpTo("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 }
}

// 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
}

如需更多使用案例,請參閱 popUpTo 指南

Navigation Compose 也支援在可組合的目的地之間傳送引數。做法是在路徑中加入引數預留位置,這個做法與使用基本導覽程式庫時,在深層連結中加入引數的做法相似:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

根據預設,系統會將所有引數剖析為字串。您可以使用 arguments 參數指定其他類型,以設定 type

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

您應從 NavBackStackEntry 中擷取 NavArguments,前者在 composable() 函式的 lambda 中可用。

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

如要將引數傳遞至目的地,您需要將該值新增至路徑中,以取代呼叫 navigate 的預留位置:

navController.navigate("profile/user1234")

如需受到支援的類型清單,請參閱 在目的地之間傳輸資料一文。

新增選用引數

Navigation Compose 也支援選用的導覽引數。選用引數與必要引數有以下兩種不同:

  • 必須使用查詢參數語法 ("?argName={argName}") 方可加入選用引數
  • 選用引數必須設定 defaultValue 或含有 nullability = true(以隱含方式將預設值設為 null

這表示所有選用引數都必須以清單形式明確地加入到 composable() 函式中:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

現在,即使沒有傳送至目的地的引數,也會改用「me」的 defaultValue

透過路徑處理引數的結構代表您的可組合元件在 Navigation 中處於完全獨立的狀態,而且可以進行測試。

Navigation Compose 支援隱式深層連結,這些連結也可以被定義為 composable() 功能的一部分。使用 navDeepLink() 將深層連結新增為清單:

val uri = "https://www.example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

這些深層連結可讓您將特定網址、動作和/或 MIME 類型與可組合元件建立關聯。根據預設,這些深層連結不會出現在外部應用程式中。如要這些深層連結可對外使用,您必須在應用程式的 manifest.xml 檔案中加入適當的 <intent-filter> 元素。如要啟用上方的深層連結,請在資訊清單的 <activity> 元素中加入以下內容:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

當其他應用程式觸發深層連結時,Navigation 會自動深層連結到可組合元件。

若使用可組合元件中的適當深層連結,這些深層連結也能用來建構 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)
}

接著,您可以像使用其他 PendingIntent 一樣使用這個 deepLinkPendingIntent,以便在深層連結目的地開啟應用程式。

巢狀 Navigation

目的地可分成 巢狀圖形,以將應用程式 UI 中的特定流程模組化。例如獨立登入流程。

巢狀圖形會封裝其目的地。與根圖表一樣,巢狀圖表必須有一個目的地做為其路徑的起點。當您瀏覽至與巢狀圖表相關聯的路線時,系統會導航到這個目的地。

如要在 NavHost 中新增巢狀圖表,可以使用 navigation 延伸函式:

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") { ... }
    }
    ...
}

在圖表放大時,我們強烈建議您將導覽圖表分割為多個方法。這也可讓多個模組展示出它們的導覽圖表。

fun NavGraphBuilder.loginGraph(navController: NavController) {
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
}

透過將該方法成為 NavGraphBuilder 上的擴充方法,您可以將該方法與預先建立的 navigationcomposabledialog 擴充方法搭配使用:

NavHost(navController, startDestination = "home") {
    ...
    loginGraph(navController)
    ...
}

與底部導覽列整合

您可以在可組合元件階層中定義較高層級的 NavController,以便將 Navigation 與其他元件(例如 BottomNavBar))建立連結。這樣做可以讓您選取底部列中的圖示進行瀏覽。

如要將底部導覽列中的項目與導覽圖表中的路徑建立連結,建議您定義密封類別,例如這裡顯示的 Screen。該密封類別包含路徑和目的地的字串資源 ID。

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 使用:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

BottomNavigation 可組合元件中,使用 currentBackStackEntryAsState() 函式取得目前的 NavBackStackEntry。此項目可讓您存取目前的 NavDestination。透過 hierarchy 協助程式方法,將該項目的路徑與當前目的地的路徑和其父目的地的路徑對比,可以確定每個BottomNavigationItem 的選取狀態(目的是在您使用 巢狀導覽 時可以處理案例)。

該項目的路徑也用於將 onClick lambda 連線至 navigate 呼叫,這樣一來,輕觸項目便會前往該項目。透過使用 saveStaterestoreState 旗標,當您在底部導覽項目之間切換時,系統會正確儲存和還原該項目的狀態和返回堆疊。

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) }
  }
}

在這裡,您可以利用 NavController.currentBackStackEntryAsState() 方法從 NavHost 函式中移出 NavController 狀態,並與 BottomNavigation 元件共用該資訊。這表示 BottomNavigation 會自動取得最新狀態。

互通性

如果您想將 Navigation 元件與 Compose 搭配使用,有兩種做法:

  • 使用片段的 Navigation 元件定義導覽圖表。
  • 使用 Compose 目的地在 Compose 中定義帶有 NavHost 的導覽圖表。只有在導覽圖表中的所有熒幕都可能夠組合時,才能採用這種做法。

因此,建議採用混合式應用程式也就是使用以片段為基礎的 Navigation 元件,並使用片段來保留以檢視為基礎的熒幕、Compose 熒幕,以及同時使用檢視表和 Compose 的熒幕。一旦應用程式中的每個熒幕片段都是可組合元件的包裝函式,下一步就是將所有熒幕與 Navigation Compos 連結,並移除所有片段。

如要變更 Compose 程式碼中的目的地,您必須公開可傳遞到階層中的任何一個可組合元件並被其觸發:

@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

在片段中,您可以找到 NavController 並瀏覽至目的地,藉此在 Compose 和片段式 Navigation 元件之間搭橋:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

或者,您也可以將 NavController 傳遞給您的 Compose 階層。但是,公開簡單的函式可重複使用且可進行測試。

測試

我們強烈建議您將 Navigation 程式碼與可組合目的地隔離開,以便分別測試各個可組合元件,與 NavHost 的可組合元件分離。

composable lambda 提供的間接層級可讓您區分 Navigation 程式碼和可組合元件本身。這在兩個方面起作用:

  • 僅將剖析的引數傳遞至可組合元件
  • 傳送應該由可組合元件(而非 NavController 本身)觸發的 lambda 進行導航。

舉例來說,Profile 可組合元件可擷取 userId 做為輸入內容,讓使用者前往好友的個人資料頁面,簽名如下:

@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

我們發現 Profile 可組合元件可獨立運作,因此可以獨立對其進行測試。composable lambda 會封裝使用最少的邏輯,以彌補 Navigation API 與可組合元件之間的差距:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

瞭解詳情

如要進一步瞭解 Jetpack Navigation,請參閱 開始使用 Navigation 元件,或造訪 Jetpack Compose Navigation 程式碼研究室

如要瞭解如何設計應用程式導覽以配合不同螢幕的大小、方向和板型規格,請參閱 回應式 UI 的 Navigation