Navigation 元件支援 Jetpack Compose 應用程式。您可以在不同可組合項之間進行導覽,同時也可運用 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") }
開始使用
在應用程式中實作導覽功能時,請實作導覽主機、圖表和控制器。詳情請參閱「導覽」總覽。
建立 NavController
如要瞭解如何在 Compose 中建立 NavController
,請參閱「建立導覽控制器」一文的 Compose 部分。
建立 NavHost
如要瞭解如何在 Compose 中建立 NavHost
,請參閱「設計導覽圖」中的 Compose 部分。
導覽至可組合函式
如要瞭解如何前往可組合項,請參閱架構說明文件中的前往目的地。
使用引數進行瀏覽
如要瞭解如何在可組合函式目的地之間傳遞引數,請參閱「設計導覽圖」中的 Compose 部分。
瀏覽時擷取複雜資料
強烈建議您在瀏覽時不要傳遞複雜的資料物件,而是在執行導覽動作時,將最少必要資訊 (例如專屬 ID 或其他形式的 ID) 做為引數傳遞:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
應採用單一真實資訊來源 (例如資料層) 的形式儲存複雜物件。完成導覽後,一旦到達目的地,即可使用傳遞的 ID 從單一真實資訊來源載入必要資訊。如要擷取 ViewModel
中負責存取資料層的引數,請使用 ViewModel
的 SavedStateHandle
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
這種方法有助於防止在設定變更期間發生資料遺失的情況,以及在相關物件更新或變化時出現不一致的問題。
如要深入瞭解為何應避免將複雜資料做為引數傳遞,以及支援的引數類型清單,請參閱在目的地之間傳遞資料。
深層連結
Navigation Compose 支援深層連結,這些連結也可以被定義為 composable()
函式的一部分。其 deepLinks
參數接受 NavDeepLink
物件清單,您可以使用 navDeepLink()
方法快速建立這些物件:
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().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/profile/$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 與底部導覽元件等其他元件建立連結。這樣做可讓您透過選取底部列中的圖示進行瀏覽。
如要使用 BottomNavigation
和 BottomNavigationItem
元件,請在 Android 應用程式中加入 androidx.compose.material
依附元件。
Groovy
dependencies { implementation "androidx.compose.material:material:1.7.5" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.7.5") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
如要將底部導覽列中的項目與導覽圖表中的路徑建立連結,建議您定義包含路徑類別和圖示的類別,例如下方顯示的 TopLevelRoute
。
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
然後將這些路徑放在清單中,供 BottomNavigationItem
使用:
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
在 BottomNavigation
可組合項中,使用 currentBackStackEntryAsState()
函式取得目前的 NavBackStackEntry
。此項目可讓您存取目前的 NavDestination
。透過 NavDestination
階層,將該項目的路徑與當前目的地及其父項目的地的路徑相較 (藉此處理使用巢狀導覽時的情況),可以確定每個 BottomNavigationItem
的選取狀態。
該項目的路徑也用於將 onClick
lambda 連線至 navigate
呼叫,這樣一來,輕觸項目便會前往該項目。透過使用 saveState
和 restoreState
旗標,當您在底部導覽項目之間切換時,系統會正確儲存和還原該項目的狀態和返回堆疊。
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.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 = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
在這裡,您可以利用 NavController.currentBackStackEntryAsState()
方法從 NavHost
函式中移出 navController
狀態,並與 BottomNavigation
元件共用該資訊。這表示 BottomNavigation
會自動取得最新狀態。
互通性
如果您想將 Navigation 元件與 Compose 搭配使用,有兩種做法:
- 使用片段的 Navigation 元件定義導覽圖表。
- 使用 Compose 目的地在 Compose 中定義帶有
NavHost
的導覽圖表。只有在導覽圖表中的所有畫面都是可組合項時,才能採用這種做法。
因此,如要混合 Compose 和 Views 應用程式,建議您使用以片段為基礎的 Navigation 元件。片段能夠容納以 View 為基礎的畫面、Compose 畫面,以及同時使用 Views 和 Compose 的畫面。當 Compose 中包含每個片段的內容後,下一步就是將這些畫面與 Navigation Compose 建立連結,並移除所有片段。
使用片段的 Navigation 在 Compose 中導覽
如要變更 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 階層。但是,公開簡單的函式可重複使用且可進行測試。
測試
將 Navigation 程式碼與可組合目的地分離,以便分別測試每個可組合項,與 NavHost
可組合項分開。
這表示您不應將 navController
直接傳遞給任何可組合項,而是將導覽回呼做為參數傳遞。這樣一來,您所有可組合項都能獨立測試,因為這類測試不需要 navController
的執行個體。
composable
lambda 提供的間接層級可讓您從可組合項中將 Navigation 程式碼區隔出來。這在兩個方面起作用:
- 僅將剖析的引數傳遞至可組合項
- 傳遞應由可組合項 (而非
NavController
本身) 觸發的 lambda 進行導覽。
舉例來說,ProfileScreen
可組合項可擷取 userId
做為輸入內容,讓使用者前往好友的個人資料頁面,簽名如下:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
這樣一來,ProfileScreen
可組合項可獨立於 Navigation 運作,因此可以獨立對其進行測試。composable
lambda 會封裝使用最少的邏輯,以彌補 Navigation API 與可組合項之間的差距:
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
建議您撰寫涵蓋應用程式導覽要求的測試,方法是測試 NavHost
,也就是傳遞至可組合項和個別畫面可組合項的導覽動作。
測試 NavHost
如要開始測試 NavHost
,請新增下列導覽測試依附元件:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
將應用程式的 NavHost
納入接受 NavHostController
做為參數的可組合項。
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
您現在可以傳遞導覽測試成果 TestNavHostController
的執行個體,測試 AppNavHost
以及 NavHost
中定義的所有導覽邏輯。用於驗證應用程式起始目的地和 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 程式碼研究室中測試」的程式碼研究室。
您也可以使用 navController
的 currentBackStackEntry
,使用 navController
來比較目前的路徑與預期路徑,藉此檢查斷言。
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
如要進一步瞭解 Compose 測試基本概念,請參閱「測試 Compose 版面配置」和「在 Jetpack Compose 程式碼研究室中測試」程式碼研究室。如要進一步瞭解導覽程式碼的進階測試,請參閱「測試導覽」指南。
瞭解詳情
如要進一步瞭解 Jetpack Navigation,請參閱「開始使用導覽元件」一文,或造訪 Jetpack Compose Navigation 程式碼研究室。
如要瞭解如何設計應用程式導覽,以配合不同螢幕的大小、方向和板型規格,請參閱「回應式 UI 導覽功能」。
如要進一步瞭解模組化應用程式中進階 Compose 導覽的實作方式,包括巢狀結構圖和底部導覽列整合等概念,請參閱 GitHub 上的 Now in Android 應用程式。
範例
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- Compose 中的質感設計 2
- 將 Jetpack Navigation 遷移至 Navigation Compose
- 在何種情況下提升狀態