Navigation 元件 為 Jetpack Compose 應用程式提供支援。您可以瀏覽不同可組合項,同時也可運用 Navigation 元件的基礎架構和功能。
設定
如要支援 Compose,請在應用程式模組的 build.gradle
檔案中使用下列依附元件:
Groovy
dependencies { def nav_version = "2.5.3" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.5.3" implementation("androidx.navigation:navigation-compose:$nav_version") }
開始使用
NavController
是 Navigation 元件的中心 API。這個 API 可以設定狀態,並且持續追蹤組成應用程式畫面的可組合項返回堆疊,以及各個畫面的狀態。
您可以在可組合項中使用 rememberNavController()
方法建立 NavController
:
val navController = rememberNavController()
您必須在可組合項階層中的某個位置建立 NavController
,讓所有需要參照其的可組合項都能進行存取。這種做法符合狀態升降的原則,讓您能使用 NavController
及其透過 currentBackStackEntryAsState()
提供的狀態,做為更新畫面外可組合項的可靠資料來源。如需這項功能的範例,請參閱與底部導覽列整合一文。
建立 NavHost
每個 NavController
都必須與單一 NavHost
可組合項建立關聯。NavHost
會將 NavController
連結至導覽圖表,該圖表指定了您可在哪些可組合目的地之間進行瀏覽。您在可組合項之間瀏覽時,系統會自動重組 NavHost
的內容。導覽圖表中的每個可組合目的地都與一個「路徑」相關聯。
建立 NavHost
時,需要使用先前透過 rememberNavController()
建立的 NavController
,以及圖表起始目的地的路徑。建立 NavHost
時,會使用 Navigation Kotlin DSL 的 lambda 語法來建構導覽圖表。您可以使用 composable()
方法新增至導覽結構。這個方法需要您提供路徑和應連結至目的地的可組合項:
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
前往可組合項
如要前往導覽圖表中的可組合目的地,必須使用 navigate
方法。navigate
採用代表目的地路徑的單一 String
參數。如要從導覽圖表中的某個可組合項開始瀏覽,請呼叫 navigate
:
navController.navigate("friendslist")
根據預設,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 指南。
其他可組合函式觸發的 Navigate 呼叫
NavController
的 navigate
函式會修改 NavController
的內部狀態。在盡可能遵守單一可靠資料來源原則的情況下,只有能提升 NavController
執行個體的可組合函式或狀態容器,以及使用 NavController
做為參數,才能發出導覽呼叫。位於 UI 階層較低位置的其他可組合函式所觸發的導覽事件,必須使用函式向呼叫端正確揭露這些事件。
以下範例顯示 MyAppNavHost
可組合函式做為 NavController
執行個體的單一可靠資料來源。ProfileScreen
會揭露事件,做為使用者輕觸特定按鈕時呼叫的函式。MyAppNavHost
具備在應用程式中瀏覽至不同畫面的路徑,且會在呼叫 ProfileScreen
時將導覽呼叫帶往正確目的地。
@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")
}
}
您應該只將 navigate()
當做回呼的一部分來使用,而不是將其做為可組合項的一部分進行呼叫,以免在每次重組時都呼叫 navigate()
。
最佳做法
向呼叫端揭露來自可組合函式的事件,且呼叫端知道要如何在應用程式中處理特定邏輯,是在 Compose 中提升狀態的最佳做法。
儘管將事件揭露做為個別 lambda 參數可能會使函式簽章超載,但可以提高可組合函式責任的瀏覽權限。這種做法的實用效益一目瞭然。
其他可減少函式宣告參數數量的替代方案,一開始可能會比較容易編寫,但長期下來會隱藏一些缺點。舉例來說,您可以建立 ProfileScreenEvents
這類的包裝函式類別,將所有事件集中在一處。這麼做會降低可組合項在定義函式時的瀏覽權限,並在專案中新增另一個類別和多個方法,而且每次以任何方式呼叫該可組合函式時,都必須建立及記錄該類別的執行個體。此外,為了盡可能重複使用該包裝函式類別,此模式建議將該類別的執行個體向下傳遞至 UI 階層,而非按照最佳做法,僅傳遞至需要的可組合項。
使用引數進行瀏覽
Navigation Compose 也支援在可組合的目的地之間傳送引數。做法是在路徑中加入引數預留位置,這個做法與使用基本導覽程式庫時,在深層連結中加入引數的做法相似:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
根據預設,系統會將所有引數剖析為字串。composable()
的 arguments
參數接受 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 中負責存取資料層的引數,可以使用 ViewModel’s
SavedStateHandle
:
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
或含有nullability = 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 類型與可組合項建立關聯。根據預設,這些深層連結不會出現在外部應用程式中。如要這些深層連結可對外使用,您必須在應用程式的 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
的擴充方法,您可以將該方法與預先建構的 navigation
、composable
和 dialog
擴充方法搭配使用:
NavHost(navController, startDestination = "home") {
...
loginGraph(navController)
...
}
與底部導覽列整合
您可以在可組合項階層中定義較高層級的 NavController
,藉此將 Navigation 與底部導覽元件等其他元件建立連結。這樣做可讓您透過選取底部列中的圖示進行瀏覽。
如要使用 BottomNavigation
和 BottomNavigationItem
元件,請在 Android 應用程式中加入 androidx.compose.material
依附元件。
Groovy
dependencies { implementation "androidx.compose.material:material:1.3.1" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.4.2" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.3.1") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.4.2" } 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
。透過 NavDestination
階層,將該項目的路徑與當前目的地及其父項目的地的路徑相較 (藉此處理使用巢狀導覽時的情況),可以確定每個 BottomNavigationItem
的選取狀態。
該項目的路徑也用於將 onClick
lambda 連線至 navigate
呼叫,這樣一來,輕觸項目便會前往該項目。透過使用 saveState
和 restoreState
旗標,當您在底部導覽項目之間切換時,系統會正確儲存和還原該項目的狀態和返回堆疊。
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()
函式。不過,您可以在執行階段建構 Navigation 程式碼以實現類型安全。如此一來,即可避免當機,也能確保以下事項:
- 導覽至目的地或導覽圖形時,提供的引數是正確的類型,且包含所有必要引數。
- 從
SavedStateHandle
擷取的引數是正確的類型。
如要進一步瞭解相關資訊,請參閱導覽類型安全說明文件。
互通性
如果您想將 Navigation 元件與 Compose 搭配使用,有兩種做法:
- 使用片段的 Navigation 元件定義導覽圖表。
- 使用 Compose 目的地在 Compose 中定義帶有
NavHost
的導覽圖表。只有在導覽圖表中的所有畫面都是可組合項時,才能採用這種做法。
因此,如要混合 Compose 和 Views 應用程式,建議您使用以片段為基礎的 Navigation 元件。片段能夠容納以 View 為基礎的畫面、Compose 畫面,以及同時使用 Views 和 Compose 的畫面。當 Compose 中包含每個片段的內容後,下一步就是將這些畫面與 Navigation Compose 建立連結,並移除所有片段。
使用片段的 Navigation 在 Compose 中導覽
如要變更 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
可組合項分開。
這表示您不應將 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 程式碼研究室中測試」。
您也可以使用 navController
的 currentBackStackEntry
,使用 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) 導覽功能」。
如要進一步瞭解模組化應用程式中進階 Navigation Compose 的實作方式,包括巢狀結構圖和底部導覽列整合等概念,請參閱 Now in Android 存放區。