Thành phần điều hướng hỗ trợ Jetpack Ứng dụng Compose. Bạn có thể di chuyển giữa các thành phần kết hợp đồng thời tận dụng cơ sở hạ tầng của thành phần Điều hướng và các tính năng AI mới.
Thiết lập
Để hỗ trợ Compose, hãy sử dụng phần phụ thuộc sau trong phần
Tệp 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") }
Bắt đầu
Khi triển khai tính năng điều hướng trong một ứng dụng, hãy triển khai một thành phần lưu trữ điều hướng, biểu đồ và bộ điều khiển. Để biết thêm thông tin, hãy xem nội dung tổng quan về Điều hướng.
Tạo NavController
Để biết thông tin về cách tạo NavController
trong Compose, hãy xem tài liệu Compose
phần Tạo trình điều khiển điều hướng.
Tạo NavHost
Để biết thông tin về cách tạo NavHost
trong Compose, hãy xem phần Compose
về Thiết kế biểu đồ điều hướng.
Điều hướng đến một thành phần kết hợp
Để biết thông tin về cách điều hướng đến một Thành phần kết hợp, hãy xem bài viết Điều hướng đến một Thành phần kết hợp đích trong cấu trúc tài liệu.
Điều hướng bằng đối số
Thành phần Điều hướng trong Compose cũng hỗ trợ truyền đối số giữa các đích đến có khả năng kết hợp. Để thực hiện việc này, bạn cần thêm trình giữ chỗ đối số vào tuyến của mình, tương tự như cách bạn thêm đối số vào đường liên kết sâu khi sử dụng thư viện điều hướng cơ sở:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
Theo mặc định, tất cả các đối số được phân tích cú pháp dưới dạng chuỗi. Tham số arguments
của
composable()
chấp nhận danh sách đối tượng NamedNavArgument
. Bạn có thể
tạo nhanh một NamedNavArgument
bằng cách sử dụng phương thức navArgument()
và
sau đó chỉ định chính xác type
:
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
Bạn nên trích xuất đối số từ NavBackStackEntry
có trong lambda của hàm composable()
.
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
Để truyền đối số đến đích, bạn cần thêm đối số đó vào tuyến đường khi thực hiện lệnh gọi navigate
:
navController.navigate("profile/user1234")
Để biết danh sách các loại được hỗ trợ, hãy xem bài viết Truyền dữ liệu giữa các đích đến.
Truy xuất dữ liệu phức tạp khi điều hướng
Bạn không nên truyền các đối tượng dữ liệu phức tạp khi điều hướng, mà thay vào đó hãy truyền thông tin tối thiểu cần thiết, chẳng hạn như giá trị nhận dạng duy nhất hoặc hình thức mã nhận dạng khác, làm đối số khi thực hiện các thao tác điều hướng:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")
Bạn nên lưu trữ các đối tượng phức tạp dưới dạng dữ liệu trong một nguồn đáng tin cậy, chẳng hạn như
lớp dữ liệu. Khi đã tới đích đến sau khi điều hướng, bạn có thể tải thông tin cần thiết từ một nguồn đáng tin cậy thông qua mã nhận dạng được truyền. Để truy xuất các đối số trong ViewModel
chịu trách nhiệm cho
truy cập vào lớp dữ liệu, hãy sử dụng SavedStateHandle
của ViewModel
:
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)
// …
}
Cách này giúp ngăn chặn tình trạng mất dữ liệu trong quá trình thay đổi cấu hình và mọi sự không nhất quán khi đối tượng được đề cập đang được cập nhật hoặc thay đổi.
Để hiểu rõ hơn về lý do bạn nên tránh truyền dữ liệu phức tạp làm đối số, cũng như danh sách các loại đối số được hỗ trợ, hãy xem bài viết Truyền dữ liệu giữa các đích đến.
Thêm đối số không bắt buộc
Điều hướng Compose cũng hỗ trợ các đối số điều hướng không bắt buộc. Các đối số không bắt buộc khác với các đối số bắt buộc theo 2 cách:
- Bạn phải đưa các đối số này vào bằng cú pháp tham số truy vấn (
"?argName={argName}"
) - Các đối số này phải có tập hợp
defaultValue
hoặc cónullable = true
(theo mặc định sẽ đặt giá trị mặc định lànull
)
Tức là bạn phải thêm tất cả đối số không bắt buộc vào hàm composable()
một cách rõ ràng dưới dạng một danh sách:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
Giờ đây, ngay cả khi không có đối số nào được truyền đến đích thì hệ thống vẫn sử dụng defaultValue
"user1234".
Cấu trúc của quá trình xử lý đối số thông qua tuyến nghĩa là thành phần kết hợp vẫn hoàn toàn độc lập với thành phần Điều hướng và giúp các thành phần này dễ kiểm thử hơn nhiều.
Liên kết sâu
Thành phần Điều hướng trong Compose hỗ trợ các đường liên kết sâu ngầm ẩn cũng có thể được xác định dưới dạng một phần của hàm composable()
. Tham số deepLinks
của hàm chấp nhận danh sách
Bạn có thể tạo nhanh các đối tượng NavDeepLink
bằng
navDeepLink()
:
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
Các đường liên kết sâu này cho phép bạn liên kết một URL, hành động hoặc loại MIME cụ thể với
thành phần kết hợp. Theo mặc định, các đường liên kết sâu này không hiển thị với các ứng dụng bên ngoài. Người nhận
có thể truy cập vào những đường liên kết sâu này bên ngoài. Bạn phải thêm đường liên kết thích hợp
Phần tử <intent-filter>
vào tệp manifest.xml
của ứng dụng. Để bật tính năng nghiên cứu sâu
trong ví dụ trước, bạn nên thêm đoạn mã sau bên trong
Phần tử <activity>
của tệp kê khai:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
Thành phần điều hướng sẽ tự động liên kết sâu vào các thành phần kết hợp đó khi đường liên kết sâu được một ứng dụng khác kích hoạt.
Bạn cũng có thể sử dụng các đường liên kết sâu này để tạo PendingIntent
bằng đường liên kết sâu phù hợp từ một thành phần kết hợp:
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)
}
Sau đó, bạn có thể sử dụng deepLinkPendingIntent
này như bất kỳ PendingIntent
nào khác để mở ứng dụng của mình tại đích đến của đường liên kết sâu.
Thành phần Điều hướng được lồng
Để biết thông tin về cách tạo biểu đồ điều hướng lồng nhau, hãy xem Biểu đồ lồng ghép.
Tích hợp với thanh điều hướng dưới cùng
Bằng cách xác định NavController
ở cấp cao hơn trong hệ phân cấp thành phần kết hợp, bạn có thể kết nối thành phần Điều hướng với các thành phần khác như thành phần điều hướng dưới cùng. Thao tác này cho phép bạn điều hướng bằng cách chọn các biểu tượng ở dưới cùng
thanh.
Để sử dụng các thành phần BottomNavigation
và BottomNavigationItem
, hãy thêm phần phụ thuộc androidx.compose.material
vào ứng dụng Android.
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" } }
Để liên kết các mục trong thanh điều hướng dưới cùng với các tuyến trong biểu đồ điều hướng, bạn nên xác định một lớp kín, chẳng hạn như Screen
được thấy ở đây, chứa mã tài nguyên Chuỗi và tuyến cho các đích đến.
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)
}
Sau đó, hãy đặt các mục này vào danh sách mà BottomNavigationItem
có thể sử dụng:
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
Trong thành phần kết hợp BottomNavigation
, hãy sử dụng hàm currentBackStackEntryAsState()
để nhận NavBackStackEntry
hiện tại. Mục nhập này cung cấp cho bạn quyền truy cập vào NavDestination
hiện tại. Trạng thái đã chọn của mỗi
Sau đó, bạn có thể xác định BottomNavigationItem
bằng cách so sánh tuyến của mục
với tuyến đường của đích đến hiện tại và đích đến gốc của nó đến
xử lý các trường hợp khi bạn đang sử dụng điều hướng lồng nhau bằng cách sử dụng thuộc tính
NavDestination
.
Tuyến của mục này cũng được dùng để kết nối hàm lambda onClick
với lệnh gọi đến navigate
sao cho thao tác nhấn vào mục sẽ điều hướng đến mục đó. Bằng cách sử dụng cờ saveState
và restoreState
, trạng thái và ngăn xếp lui của mục đó sẽ được lưu và khôi phục chính xác khi bạn hoán đổi giữa các mục điều hướng dưới cùng.
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) }
}
}
Tại đây, bạn sẽ tận dụng phương thức NavController.currentBackStackEntryAsState()
để nâng trạng thái navController
khỏi hàm NavHost
và chia sẻ với thành phần BottomNavigation
. Điều này có nghĩa là BottomNavigation
tự động có trạng thái cập nhật mới nhất.
An toàn về loại cho thành phần Điều hướng trong Compose
Mã trên trang này không an toàn về loại. Bạn có thể gọi navigate()
có tuyến không tồn tại hoặc đối số không chính xác. Tuy nhiên, bạn có thể
cấu trúc mã điều hướng của bạn để an toàn về kiểu trong thời gian chạy. Nhờ vậy, bạn có thể tránh sự cố và đảm bảo rằng:
- Các đối số, mà bạn cung cấp khi di chuyển đến một đích đến hoặc biểu đồ điều hướng, đều thuộc loại phù hợp và có tất cả đối số bắt buộc.
- Các đối số bạn truy xuất từ
SavedStateHandle
có loại chính xác.
Để biết thêm thông tin về vấn đề này, hãy xem bài viết An toàn về kiểu trong Kotlin DSL và Navigation Soạn thư.
Khả năng tương thích
Nếu muốn sử dụng thành phần Điều hướng trong Compose, bạn có 2 lựa chọn:
- Xác định biểu đồ điều hướng có thành phần Điều hướng cho các mảnh.
- Xác định biểu đồ điều hướng có
NavHost
trong Compose bằng cách sử dụng đích đến Compose. Điều này chỉ có thể xảy ra nếu tất cả các màn hình trong biểu đồ điều hướng đều có thể kết hợp.
Do đó, ứng dụng Compose và Khung hiển thị kết hợp nên sử dụng Thành phần điều hướng dựa trên Mảnh. Sau đó, các Mảnh sẽ lưu giữ dựa trên Chế độ xem màn hình, màn hình Compose và màn hình sử dụng cả Khung hiển thị và Compose. Sau mỗi lần Nội dung của Mảnh nằm trong Compose, bước tiếp theo là liên kết tất cả các màn hình đó cùng với Navigation Compose (Điều hướng Compose) và xoá tất cả các Mảnh.
Di chuyển từ Compose bằng thành phần Điều hướng cho mảnh
Để thay đổi điểm đến bên trong mã Compose, bạn hiển thị các sự kiện có thể chuyển tới cũng như kích hoạt bởi thành phần kết hợp bất kỳ trong hệ phân cấp:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
Trong phân đoạn của bạn, bạn tạo cầu nối giữa Compose và thành phần Điều hướng dựa trên phân đoạn bằng cách tìm NavController
và điều hướng đến đích:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
Ngoài ra, bạn có thể chuyển NavController
xuống theo hệ thống phân cấp của Compose.
Tuy nhiên, việc hiển thị các hàm đơn giản sẽ giúp dễ sử dụng lại và kiểm thử hơn.
Thử nghiệm
Phân tách mã điều hướng từ các đích đến có thể kết hợp để cho phép kiểm thử
từng thành phần kết hợp riêng biệt, tách biệt với thành phần kết hợp NavHost
.
Điều này có nghĩa là bạn không nên truyền navController
trực tiếp vào bất kỳ
thành phần kết hợp và truyền lệnh gọi lại điều hướng dưới dạng tham số. Điều này cho phép
tất cả thành phần kết hợp của bạn đều có thể kiểm thử được riêng lẻ, vì chúng không yêu cầu
thực thể của navController
trong kiểm thử.
Mức độ gián tiếp do hàm lambda composable
cung cấp là điều cho phép bạn
tách riêng mã Navigation khỏi chính thành phần kết hợp đó. Cách này hoạt động theo hai
hướng:
- Chỉ chuyển đối số được phân tích cú pháp vào thành phần kết hợp của bạn
- Chuyển các lambda nên được thành phần kết hợp kích hoạt để điều hướng, thay vì chính
NavController
.
Ví dụ: một thành phần kết hợp Profile
lấy userId
làm dữ liệu đầu vào và cho phép
người dùng điều hướng đến trang hồ sơ của bạn bè có thể có chữ ký:
@Composable
fun Profile(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
Theo đó, thành phần kết hợp Profile
hoạt động độc lập với thành phần Điều hướng nên có thể được kiểm thử độc lập. Lambda composable
sẽ đóng gói logic tối thiểu cần thiết để thu hẹp khoảng cách giữa các Navigation API (API Điều hướng) và thành phần kết hợp của bạn:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
navController.navigate("profile?userId=$friendUserId")
}
}
Bạn nên viết các kiểm thử đáp ứng các yêu cầu điều hướng trong ứng dụng bằng cách kiểm thử NavHost
, các thao tác điều hướng được truyền đến thành phần kết hợp cũng như các thành phần kết hợp màn hình riêng lẻ.
Kiểm thử NavHost
Để bắt đầu kiểm thử NavHost
, hãy thêm kiểm thử điều hướng sau đây
phần phụ thuộc:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Bạn có thể thiết lập đối tượng kiểm thử NavHost
và truyền một thực thể của thực thể navController
đến đối tượng đó. Để làm được điều này, điều hướng
cấu phần phần mềm kiểm thử cung cấp TestNavHostController
. Quy trình kiểm thử giao diện người dùng xác minh đích bắt đầu của ứng dụng và NavHost
sẽ có dạng như sau:
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()
}
}
Kiểm thử thao tác điều hướng
Bạn có thể kiểm thử hoạt động triển khai tính năng điều hướng theo nhiều cách, chẳng hạn như nhấp vào thành phần trên giao diện người dùng, sau đó xác minh đích đến đã hiển thị hoặc bằng cách so sánh tuyến dự kiến với tuyến hiện tại.
Khi muốn kiểm thử việc triển khai ứng dụng cụ thể của mình, bạn nên kiểm thử theo cách nhấp vào giao diện người dùng. Để tìm hiểu cách kiểm thử một cách độc lập điều này cùng với các hàm có khả năng kết hợp riêng lẻ, hãy nhớ tham khảo lớp học lập trình Kiểm thử trong Jetpack Compose.
Bạn cũng có thể sử dụng navController
để kiểm tra các câu nhận định của mình bằng cách so sánh tuyến đường Chuỗi hiện tại với tuyến đường dự kiến bằng cách sử dụng currentBackStackEntry
của navController
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
val route = navController.currentBackStackEntry?.destination?.route
assertEquals(route, "profiles")
}
Để được hướng dẫn thêm về thông tin cơ bản về kiểm thử Compose, hãy xem Kiểm thử bố cục Compose và Kiểm thử trong Jetpack Compose lớp học lập trình này. Để tìm hiểu thêm về kiểm thử nâng cao đối với mã điều hướng, vui lòng tham khảo hướng dẫn Kiểm thử thành phần Navigation (Điều hướng).
Tìm hiểu thêm
Để tìm hiểu thêm về thành phần Điều hướng trong Jetpack, hãy xem Bắt đầu với thành phần Điều hướng hoặc tham gia Lớp học lập trình về thành phần Điều hướng trong Jetpack Compose.
Để tìm hiểu cách thiết kế tính năng điều hướng trong ứng dụng sao cho phù hợp với nhiều kích thước màn hình, hướng và các hệ số hình dạng, hãy xem bài viết Điều hướng trên giao diện người dùng thích ứng.
Để tìm hiểu cách triển khai điều hướng Compose nâng cao hơn trong một ứng dụng được mô-đun hoá, bao gồm các khái niệm như biểu đồ lồng nhau và thanh điều hướng dưới cùng tích hợp, hãy xem ứng dụng Now in Android trên GitHub.
Mẫu
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Material Design 2 trong Compose
- Di chuyển thành phần Điều hướng Jetpack sang Điều hướng trong Compose
- Vị trí để chuyển trạng thái lên trên