使用 Compose 進行導覽

Navigation 元件支援 Jetpack 編寫應用程式。您可以在可組合項之間導覽 同時運用 Navigation 元件的基礎架構 接著介紹網際網路通訊層 包括兩項主要的安全防護功能

設定

如要支援 Compose,請在應用程式模組的 build.gradle 檔案:

Groovy

dependencies {
    def nav_version = "2.8.0"

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

Kotlin

dependencies {
    val nav_version = "2.8.0"

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

開始使用

在應用程式中實作導覽時,請實作導覽主機。 圖表和控制器詳情請參閱導覽總覽。

如要瞭解如何在 Compose 中建立 NavController,請參閱 Compose 「建立導覽控制器」一節。

建立 NavHost

如要瞭解如何在 Compose 中建立 NavHost,請參閱「Compose」一節 「設計導覽圖」中所述的步驟。

如要瞭解如何前往可組合項,請參閱前往 目的地採用的架構 說明文件。

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

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

根據預設,系統會將所有引數剖析為字串。的 arguments 參數 composable() 接受 NamedNavArgument 物件清單。你可以 使用 navArgument() 方法快速建立 NamedNavArgument 然後指定確切的 type

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

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

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

如要將引數傳遞至目的地,您需要在進行 navigate 呼叫時將引數附加到路徑中:

navController.navigate("profile/user1234")

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

瀏覽時擷取複雜資料

強烈建議您在瀏覽時不要傳遞複雜的資料物件,而是在執行導覽動作時,將最少必要資訊 (例如專屬 ID 或其他形式的 ID) 做為引數傳遞:

// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")

複雜的物件應以資料形式儲存在單一可靠資料來源中,例如 資料層完成導覽後,一旦到達目的地,即可使用傳遞的 ID 從單一真實資訊來源載入必要資訊。如要擷取 ViewModel 中 存取資料層,請使用 ViewModelSavedStateHandle

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

這種方法有助於防止在設定變更期間發生資料遺失的情況,以及在相關物件更新或變化時出現不一致的問題。

如要深入瞭解為何應避免將複雜資料做為引數傳遞,以及支援的引數類型清單,請參閱在目的地之間傳遞資料

新增選用引數

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

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

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

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

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

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

Navigation Compose 支援隱含深層連結,這些連結也可以被定義為 composable() 函式的一部分。其 deepLinks 參數接受資訊清單 NavDeepLink 物件,可以使用 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 類型與 可組合函式。根據預設,這些深層連結不會出現在外部應用程式中。目的地: 您必須把適當的深層連結新增到外部連結 <intent-filter> 元素新增至應用程式的 manifest.xml 檔案。為了深入瞭解 連結,應該在 資訊清單的 <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,以便在深層連結目的地開啟應用程式。

巢狀導覽

如要瞭解如何建立巢狀導覽圖,請參閱 巢狀結構圖

與底部導覽列整合

您可以在可組合項階層中定義較高層級的 NavController,藉此將 Navigation 與底部導覽元件等其他元件建立連結。這樣做可讓您透過選取畫面底部的圖示進行瀏覽 。

如要使用 BottomNavigationBottomNavigationItem 元件,請在 Android 應用程式中加入 androidx.compose.material 依附元件。

Groovy

dependencies {
    implementation "androidx.compose.material:material:1.7.1"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.7.1")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

如要將底部導覽列中的項目與導覽圖表中的路徑建立連結,建議您定義包含路徑和目的地字串資源 ID 的密封類別,例如下方顯示的 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 使用:

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

BottomNavigation 可組合項中,使用 currentBackStackEntryAsState() 函式取得目前的 NavBackStackEntry。此項目可讓您存取目前的 NavDestination。您在各個 Pod 的所選狀態 接著,就可以比較項目的路徑來確定 BottomNavigationItem 與目前目的地及其父項目的地之間的路徑 在使用巢狀導覽的情況下, NavDestination 階層。

該項目的路徑也用於將 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 中的類型安全性

這個網頁上的程式碼並非類型安全。您可以呼叫 navigate() 函式中存在不存在的路徑或引數不正確。不過, 在執行階段將導覽程式碼建構為類型安全。如此一來,即可避免當機,也能確保以下事項:

  • 導覽至目的地或導覽圖形時,提供的引數是正確的類型,且包含所有必要引數。
  • SavedStateHandle 擷取的引數是正確的類型。

詳情請參閱「Kotlin DSL 和 Navigation 中的類型安全 撰寫

互通性

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

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

因此,如要混合 Compose 和 Views 應用程式,建議您使用以片段為基礎的 Navigation 元件。片段接著會保留以 View 為基礎的 畫面、Compose 畫面,以及同時使用 View 和 Compose 的畫面。每個 Fragment 的內容位於 Compose 中,下一步是連結所有這些畫面 並與 Navigation Compose 搭配使用,並移除所有 Fragment

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

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

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

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

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

測試

將導覽程式碼從可組合函式目的地分離,以便進行測試 每個可組合項,與 NavHost 可組合項分開。

因此,請勿將 navController 直接傳遞到 可組合項,而是將導覽回呼做為參數傳遞。這樣一來, 所有可組合函式都可個別測試,因為這類功能不需要 測試中的 navController 例項。

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

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

例如,Profile 可組合函式會擷取 userId 做為輸入內容,並允許 使用者瀏覽朋友的個人資料頁面時,簽名可以是:

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

這樣一來,Profile 可組合項可獨立於 Navigation 運作,因此可以獨立對其進行測試。composable lambda 會封裝使用最少的邏輯,以彌補 Navigation API 與可組合項之間的差距:

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

建議您編寫涵蓋應用程式導覽要求的測試,測試 NavHost 時,導覽動作會傳遞至可組合項及個別畫面的可組合項。

測試 NavHost

如要開始測試 NavHost,請新增下列導覽測試 依附元件:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

您可以設定 NavHost 測試對象,並將 navController 的執行個體傳遞給該對象。因此,導覽面板 測試成果提供 TestNavHostController。用於驗證應用程式起始目的地和 NavHost 的 UI 測試如下所示:

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupAppNavHost() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }

    // Unit test
    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}

測試導覽動作

您可以透過多種方式來測試導覽的實作情形,具體方法如下:點按 UI 元素,然後驗證顯示的目的地,或比較預期路徑和目前路徑。

如要測試導入應用程式,建議您按一下使用者介面。如要瞭解如何獨立測試個別可組合函式,請務必查看「在 Jetpack Compose 程式碼研究室中測試」。

您也可以使用 navControllercurrentBackStackEntry,使用 navController 來比較目前的 String (字串) 路徑與預期路徑,藉此檢查斷言。

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

如要進一步瞭解 Compose 測試的基本概念,請參閱 測試 Compose 版面配置在 Jetpack Compose 中測試 程式碼研究室。 如要進一步瞭解導覽程式碼的進階測試,請參閱「測試導覽」指南。

瞭解詳情

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

如要瞭解如何設計應用程式導覽以配合不同螢幕的大小、方向和板型規格,請參閱「回應式使用者介面 (UI) 導覽功能」。

如要瞭解更進階的 Compose 導覽實作,請參閱 模組化應用程式,包括巢狀圖和底部導覽列等概念 請前往 GitHub 查看 Now in Android 應用程式。

範例