1. Giới thiệu
Lần cập nhật gần đây nhất: ngày 25 tháng 07 năm 2022
Bạn cần
- Phiên bản Android Studio mới nhất
- Kiến thức về Kotlin và các biểu thức lambda theo sau (trailing lambda)
- Hiểu biết cơ bản về thành phần điều hướng và các yêu cầu của nó, chẳng hạn như ngăn xếp lui
- Hiểu biết cơ bản về Compose
- Hãy cân nhắc việc hoàn thành lớp học lập trình Jetpack Compose cơ bản trước khi tham gia phần này.
- Hiểu biết cơ bản về việc quản lý trạng thái trong Compose
- Hãy cân nhắc việc hoàn thành Lớp học lập trình về Trạng thái trong Jetpack Compose trước khi tham gia lớp học này
Điều hướng bằng Compose
Navigation (Điều hướng) là một thư viện Jetpack, cho phép bạn di chuyển từ một điểm đích này tới một điểm đích khác trong ứng dụng của mình. Thư viện Navigation cũng cung cấp một cấu phần phần mềm cụ thể, cho phép điều hướng bằng Jetpack Composemột cách rõ ràng và nhất quán. Cấu phần mềm này (navigation-compose
) là trọng tâm của lớp học lập trình này.
Bạn sẽ thực hiện
Bạn sẽ dùng nghiên cứu về giao diện Material của Rally làm cơ sở cho lớp học lập trình này để triển khai thành phần điều hướng của Jetpack và cho phép điều hướng giữa các màn hình có khả năng kết hợp của ứng dụng Rally.
Kiến thức bạn sẽ học được
- Kiến thức cơ bản về cách sử dụng thành phần Jetpack Navigation trong Jetpack Compose
- Điều hướng giữa các thành phần kết hợp (composable)
- Tích hợp một thành phần điều hướng có thể kết hợp, dạnh thẻ tuỳ chỉnh vào hệ thống điều hướng
- Điều hướng chứa đối số
- Điều hướng thông qua liên kết sâu
- Kiểm thử tính năng điều hướng
2. Thiết lập
Để làm theo, hãy sao chép điểm xuất phát (nhánh main
) của lớp học lập trình này.
$ git clone https://github.com/googlecodelabs/android-compose-codelabs.git
Ngoài ra, bạn có thể tải 2 tệp zip xuống:
Sau khi tải mã xuống, bạn hãy mở thư mục dự án NavigationCodelab trong Android Studio. Bây giờ, bạn đã sẵn sàng để bắt đầu.
3. Tổng quan về ứng dụng Rally
Bước đầu tiên, bạn sẽ làm quen với ứng dụng Rally và cơ sở mã của nó. Chạy và khám phá sơ về ứng dụng.
Rally có ba màn hình chính dưới dạng các thành phần kết hợp:
OverviewScreen
— mô tả tổng quan về tất cả thông báo và giao dịch tài chínhAccountsScreen
— thông tin chi tiết về các tài khoản hiện cóBillsScreen
— khoản chi phí theo kế hoạch
Ở đầu màn hình, Rally hiện sử dụng một thành phần điều hướng có thể kết hợp, dạnh thẻ tuỳ chỉnh (RallyTabRow
) để di chuyển giữa 3 màn hình này. Thao tác nhấn vào từng biểu tượng sẽ mở rộng lựa chọn hiện tại và đưa bạn đến một màn hình tương ứng.
Khi điều hướng đến màn hình kết hợp này, bạn cũng có thể coi chúng là các đích đến điều hướng, vì chúng ta muốn truy cập vào từng đích tại một điểm cụ thể. Các đích đến này được xác định trước trong tệp RallyDestinations.kt
.
Bên trong, bạn sẽ thấy cả ba đích đến chính được xác định là đối tượng (Overview, Accounts
và Bills
) cũng như SingleAccount
sẽ được thêm vào ứng dụng sau này. Mỗi đối tượng mở rộng từ giao diện RallyDestination
, và chứa thông tin cần thiết về từng đích đến cho mục đích điều hướng:
- Một
icon
cho thanh trên cùng - Chuỗi
route
(cần thiết cho Compose Navigation làm đường dẫn đến đích đó) - Một
screen
đại diện cho toàn bộ thành phần kết hợp của đích đến này
Khi chạy ứng dụng, bạn sẽ nhận thấy là thực sự có thể di chuyển giữa các đích đến hiện sử dụng thanh trên cùng. Tuy nhiên, trên thực tế, ứng dụng không sử dụng thành phần Navigation (Điều hướng) trong Compose mà thay vào đó, cơ chế điều hướng hiện tại dựa vào một số thao tác chuyển đổi thủ công giữa các thành phần kết hợp và kích hoạt tính năng kết hợp lại để hiển thị nội dung mới. Do đó, mục tiêu của lớp học lập trình này là di chuyển và triển khai thành công Compose Navigation.
4. Di chuyển đến Compose Navigation
Để di chuyển cơ bản sang Jetpack Compose, hãy làm theo các bước sau:
- Thêm phần phụ thuộc Navigation Compose mới nhất
- Thiết lập
NavController
- Thêm một
NavHost
rồi tạo biểu đồ điều hướng - Chuẩn bị tuyến đường để di chuyển giữa các đích đến khác nhau trong ứng dụng
- Thay thế cơ chế điều hướng hiện tại bằng tính năng Compose Navigation
Hãy cùng xem chi tiết các bước này.
Thêm phần phụ thuộc Navigation
Mở tệp bản dựng của ứng dụng tại app/build.gradle
. Trong mục phần phụ thuộc, hãy thêm phần phụ thuộc navigation-compose
.
dependencies {
implementation "androidx.navigation:navigation-compose:{latest_version}"
// ...
}
Bạn có thể tìm phiên bản navigation-compose mới nhất tại đây.
Bây giờ, bạn có thể đồng bộ hoá dự án và bắt đầu sử dụng thành phần Navigation trong Compose.
Thiết lập NavController
NavController
là thành phần trung tâm khi sử dụng thành phần Navigation trong Compose. Thành phần này giúp theo dõi các mục nhập có thể kết hợp của ngăn xếp lui, di chuyển ngăn xếp tiến, cho phép thao tác ngăn xếp lui và di chuyển giữa các trạng thái của đích đến. Vì NavController
là trọng tâm của điều hướng, bạn phải tạo nó ở bước đầu tiên trong việc thiết lập Compose Navigation.
Nhận được NavController
bằng cách gọi hàm rememberNavController()
. Thao tác này sẽ tạo và ghi nhớ một NavController
vẫn tồn tại sau khi thay đổi cấu hình (sử dụng rememberSaveable
).
Bạn phải luôn tạo và đặt NavController
ở cấp cao nhất trong hệ phân cấp có thể kết hợp, thường nằm trong thành phần kết hợp App
của bạn. Theo đó, tất cả thành phần kết hợp cần tham chiếu đến NavController
đều có quyền truy cập. Điều này tuân theo các nguyên tắc chuyển trạng thái lên trên (state hoisting) và đảm bảo NavController
là nguồn đáng tin cậy chính để di chuyển giữa các màn hình có thể kết hợp cũng như để duy trì ngăn xếp lui.
Mở RallyActivity.kt
. Tìm nạp NavController
bằng rememberNavController()
trong RallyApp
vì đây là thành phần kết hợp gốc và điểm truy cập cho toàn bộ ứng dụng:
import androidx.navigation.compose.rememberNavController
// ...
@Composable
fun RallyApp() {
RallyTheme {
var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
val navController = rememberNavController()
Scaffold(
// ...
) {
// ...
}
}
Tuyến đường trong Compose Navigation
Như đã đề cập trước đó, Ứng dụng Rally có ba đích đến chính và một đích đến khác sẽ được thêm vào sau này (SingleAccount
). Các giá trị này được quy định trong RallyDestinations.kt
. và chúng tôi đã đề cập về việc mỗi đích có một icon
, route
và screen
đã xác định:
Bước tiếp theo là thêm các đích đến này vào biểu đồ điều hướng, trong đó Overview
là đích đến bắt đầu khi khởi chạy ứng dụng.
Khi sử dụng thành phần Navigation trong Compose, mỗi đích đến có thể kết hợp trong biểu đồ điều hướng của bạn sẽ được liên kết với một tuyến đường. Các tuyến đường được biểu thị dưới dạng Chuỗi xác định đường dẫn đến thành phần kết hợp và hướng dẫn navController
truy cập vào đúng vị trí. Bạn có thể coi đây là một đường liên kết sâu ngầm ẩn dẫn đến một điểm đến cụ thể. Mỗi điểm đến phải có một tuyến đường riêng.
Để thực hiện việc này, chúng ta sẽ sử dụng thuộc tính route
của mỗi đối tượng RallyDestination
. Ví dụ như Overview.route
là tuyến đường sẽ đưa bạn đến thành phần kết hợp màn hình Overview
.
Gọi thành phần kết hợp NavHost bằng biểu đồ điều hướng
Bước tiếp theo là thêm NavHost
và tạo biểu đồ điều hướng.
3 phần chính của thành phần Navigation là NavController
, NavGraph
và NavHost
. NavController
luôn liên kết với một NavHost
có thể kết hợp. NavHost
đóng vai trò là vùng chứa và chịu trách nhiệm hiển thị đích đến hiện tại của biểu đồ. Khi bạn di chuyển giữa các thành phần có thể kết hợp, nội dung của NavHost
sẽ tự động
kết hợp lại. Nội dung này cũng liên kết NavController
với một biểu đồ điều hướng (NavGraph
) xác định các đích đến có thể kết hợp để di chuyển giữa những đích đến này. Về cơ bản, đó là một tập hợp các đích đến có thể tìm nạp được.
Quay lại thành phần kết hợp RallyApp
trong RallyActivity.kt
. Thay thế thành phần kết hợp Box
bên trong Scaffold
(chứa nội dung của màn hình hiện tại để chuyển đổi giữa các màn hình theo cách thủ công) bằng một NavHost
mới mà bạn có thể tạo bằng cách làm theo ví dụ về mã bên dưới.
Truyền vào tham số navController
đã tạo ở bước trước để kết nối với NavHost
này. Như đã đề cập trước đó, mỗi NavController
phải liên kết với một NavHost
duy nhất.
NavHost
cũng cần tuyến startDestination
để biết đích đến nào sẽ hiển thị khi ứng dụng được khởi chạy, do đó, hãy đặt giá trị này thành Overview.route
. Ngoài ra, truyền một Modifier
để chấp nhận khoảng đệm Scaffold
bên ngoài và áp dụng cho NavHost
.
Tham số cuối cùng builder: NavGraphBuilder.() -> Unit
chịu trách nhiệm xác định và tạo biểu đồ điều hướng. Hàm này sử dụng cú pháp lambda từ DSL Kotlin điều hướng, vì vậy nó có thể được chuyển dưới dạng trailing lambda (lambda theo sau) bên trong phần nội dung của hàm, và được kéo ra khỏi dấu ngoặc đơn:
import androidx.navigation.compose.NavHost
...
Scaffold(...) { innerPadding ->
NavHost(
navController = navController,
startDestination = Overview.route,
modifier = Modifier.padding(innerPadding)
) {
// builder parameter will be defined here as the graph
}
}
Thêm đích đến vào NavGraph
Bây giờ, bạn có thể định nghĩa biểu đồ điều hướng và các đích đến mà NavController
có thể điều hướng. Như đã đề cập, tham số builder
yêu cầu một hàm. Do đó, thành phần Navigation (Điều hướng) trong Compose cung cấp hàm mở rộng NavGraphBuilder.composable
để dễ dàng thêm từng đích đến có thể kết hợp vào biểu đồ điều hướng, đồng thời xác định thông tin điều hướng cần thiết.
Đích đầu tiên sẽ là Overview
. Vì vậy, bạn cần thêm đích này thông qua hàm mở rộng composable
và đặt Chuỗi duy nhất route
. Thao tác này chỉ thêm đích đến vào biểu đồ điều hướng. Vì vậy, bạn cũng cần xác định giao diện người dùng thực tế sẽ hiển thị khi di chuyển đến đích này. Bạn cũng có thể thực hiện việc này thông qua trailing lambda (lambda theo sau) bên trong phần thân hàm composable
, là một mẫu thường dùng trong Compose:
import androidx.navigation.compose.composable
// ...
NavHost(
navController = navController,
startDestination = Overview.route,
modifier = Modifier.padding(innerPadding)
) {
composable(route = Overview.route) {
Overview.screen()
}
}
Theo mẫu này, chúng ta sẽ thêm cả ba thành phần kết hợp màn hình chính dưới dạng ba đích đến:
NavHost(
navController = navController,
startDestination = Overview.route,
modifier = Modifier.padding(innerPadding)
) {
composable(route = Overview.route) {
Overview.screen()
}
composable(route = Accounts.route) {
Accounts.screen()
}
composable(route = Bills.route) {
Bills.screen()
}
}
Giờ thì hãy chạy ứng dụng – bạn sẽ thấy Overview
làm đích bắt đầu và giao diện người dùng tương ứng hiển thị.
Chúng ta đã đề cập trước thành phần kết hợp thanh tuỳ chỉnh, RallyTabRow
, trước đó đã xử lý việc điều hướng theo cách thủ công giữa các màn hình. Tại thời điểm này, trang web chưa được kết nối với tính năng điều hướng mới, vì vậy, bạn có thể xác minh rằng việc nhấp vào thẻ sẽ không thay đổi đích đến của thành phần kết hợp màn hình được hiển thị. Hãy khắc phục lỗi đó trong bước tiếp theo!
5. Tích hợp RallyTabRow với tính năng điều hướng
Trong bước này, bạn sẽ kết nối RallyTabRow
với navController
và biểu đồ điều hướng để cho phép điều hướng đến đúng đích đến.
Để làm việc này, bạn cần sử dụng navController
mới để xác định hành động điều hướng chính xác cho lệnh gọi lại onTabSelected
của RallyTabRow
. Lệnh gọi lại này xác định những gì sẽ xảy ra khi một biểu tượng thẻ cụ thể được chọn, sau đó thực hiện thao tác điều hướng thông qua navController.navigate(route)
.
Hãy làm theo hướng dẫn này trong RallyActivity
, tìm thành phần kết hợp RallyTabRow
và tham số gọi lại onTabSelected
của thành phần đó.
Vì chúng ta muốn thẻ điều hướng đến một đích đến cụ thể khi được nhấn, nên bạn cũng cần biết biểu tượng tab chính xác nào đã được chọn. Thật may là tham số onTabSelected: (RallyDestination) -> Unit
đã cung cấp điều này. Bạn sẽ sử dụng thông tin đó và tuyến RallyDestination
để hướng dẫn navController
, đồng thời gọi navController.navigate(newScreen.route)
khi một thẻ được chọn:
@Composable
fun RallyApp() {
RallyTheme {
var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
val navController = rememberNavController()
Scaffold(
topBar = {
RallyTabRow(
allScreens = rallyTabRowScreens,
// Pass the callback like this,
// defining the navigation action when a tab is selected:
onTabSelected = { newScreen ->
navController.navigate(newScreen.route)
},
currentScreen = currentScreen,
)
}
Nếu chạy ứng dụng ngay, bạn có thể xác minh rằng thao tác nhấn vào từng thẻ trong RallyTabRow
thực sự điều hướng đến đúng đích đến có thể kết hợp. Tuy nhiên, hiện có hai vấn đề mà bạn có thể nhận thấy:
- Việc nhấn vào cùng một thẻ trong một hàng sẽ khởi chạy nhiều bản sao có cùng một đích
- Giao diện người dùng của thẻ không khớp với đích đến được hiển thị – nghĩa là việc mở rộng và thu gọn các thẻ đã chọn không hoạt động như dự kiến:
Hãy khắc phục cả hai vấn đề này!
Ra mắt một bản sao của đích đến
Để khắc phục sự cố đầu tiên và đảm bảo sẽ có tối đa một bản sao của một đích đến nhất định ở đầu ngăn xếp lui, Compose Navigation API sẽ cung cấp cờ launchSingleTop
mà bạn có thể truyền đến hành động navController.navigate()
, như bên dưới:
navController.navigate(route) { launchSingleTop = true }
Vì bạn muốn hành vi này trên ứng dụng cho mọi đích đến, thay vì sao chép, hãy dán cờ này vào tất cả đích đến của bạn .Khi navigate(...)
gọi, bạn có thể trích xuất tiện ích mở rộng này vào tiện ích trợ giúp ở cuối RallyActivity
:
import androidx.navigation.NavHostController
// ...
fun NavHostController.navigateSingleTopTo(route: String) =
this.navigate(route) { launchSingleTop = true }
Giờ đây, bạn có thể thay thế lệnh gọi navController.navigate(newScreen.route)
bằng .navigateSingleTopTo(...)
. Chạy lại ứng dụng và xác minh rằng bạn sẽ chỉ nhận được một bản sao của một đích đến khi nhấp nhiều lần vào biểu tượng của đích đến đó ở thanh trên cùng:
@Composable
fun RallyApp() {
RallyTheme {
var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
val navController = rememberNavController()
Scaffold(
topBar = {
RallyTabRow(
allScreens = rallyTabRowScreens,
onTabSelected = { newScreen ->
navController
.navigateSingleTopTo(newScreen.route)
},
currentScreen = currentScreen,
)
}
Kiểm soát các chế độ điều hướng và trạng thái ngăn xếp lui
Ngoài launchSingleTop
, bạn cũng có thể sử dụng các cờ khác từ NavOptionsBuilder
để kiểm soát và tuỳ chỉnh thêm hành vi điều hướng. Vì RallyTabRow
hoạt động tương tự như một BottomNavigation
, nên bạn cũng nên cân nhắc xem có muốn lưu và khôi phục trạng thái của đích đến khi di chuyển đến và từ đích đến đó hay không. Ví dụ như nếu bạn cuộn xuống cuối phần Tổng quan rồi chuyển đến Tài khoản và quay lại, bạn có muốn giữ vị trí cuộn không? Bạn có muốn nhấn lại vào cùng một đích đến trong RallyTabRow
để tải lại trạng thái màn hình hay không? Đây đều là những câu hỏi xác đáng và cần được xác định theo các yêu cầu thiết kế đối với ứng dụng của riêng bạn.
Chúng tôi sẽ đề cập đến một số tuỳ chọn bổ sung mà bạn có thể sử dụng trong cùng một hàm mở rộng navigateSingleTopTo
:
launchSingleTop = true
– như đã đề cập, điều này đảm bảo sẽ có tối đa một bản sao của một đích nhất định ở đầu ngăn xếp lui- Trong ứng dụng Rally, điều này có nghĩa là việc nhấn lại vào cùng một thẻ nhiều lần sẽ không khởi chạy nhiều bản sao của cùng một đích đến
popUpTo(startDestination) { saveState = true }
– bật đích đến bắt đầu của biểu đồ lên để tránh tạo một ngăn xếp lớn các đích đến trên ngăn xếp lui khi bạn chọn các thẻ- Trong Rally, điều này có nghĩa là việc nhấn mũi tên quay lại từ bất kỳ đích đến nào sẽ kéo toàn bộ ngăn xếp lui về mục Overview (Tổng quan)
restoreState = true
– xác định xem thao tác điều hướng này có khôi phục mọi trạng thái đã lưu trước đó bằngPopUpToBuilder.saveState
hay thuộc tínhpopUpToSaveState
hay không. Lưu ý là nếu trước đó không có trạng thái nào được lưu bằng mã đích đến, thì điều này không có hiệu lực- Trong Rally, điều này có nghĩa là việc nhấn lại vào cùng một thẻ sẽ giữ lại dữ liệu và trạng thái trước đó của người dùng trên màn hình mà không cần tải lại.
Bạn có thể thêm lần lượt tất cả tuỳ chọn này vào mã, chạy ứng dụng sau mỗi tuỳ chọn và xác minh hành vi chính xác sau khi thêm từng cờ. Bằng cách đó, bạn sẽ có thể xem thực tế cách mỗi cờ thay đổi trạng thái điều hướng và ngăn xếp lui:
import androidx.navigation.NavHostController
import androidx.navigation.NavGraph.Companion.findStartDestination
// ...
fun NavHostController.navigateSingleTopTo(route: String) =
this.navigate(route) {
popUpTo(
this@navigateSingleTopTo.graph.findStartDestination().id
) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
Sửa giao diện người dùng thẻ
Ngay từ đầu lớp học lập trình, trong khi vẫn sử dụng cơ chế điều hướng thủ công, RallyTabRow
đã sử dụng biến currentScreen
để xác định xem nên mở rộng hay thu gọn từng thẻ.
Tuy nhiên, sau khi bạn thực hiện các thay đổi, currentScreen
sẽ không còn được cập nhật. Đây là lý do tại sao việc mở rộng và thu gọn các thẻ đã chọn bên trong RallyTabRow
không hoạt động nữa.
Để kích hoạt lại hành vi này bằng thành phần Navigation (Điều hướng) trong Compose, bạn cần biết đích đến nào đang được hiển thị tại mỗi thời điểm, hoặc theo thuật ngữ về điều hướng thì mục nào đang ở trên cùng ngăn xếp lui, sau đó cập nhật RallyTabRow
mỗi khi đích đến thay đổi.
Để nhận thông tin cập nhật theo thời gian thực về đích đến hiện tại từ ngăn xếp lui dưới dạng State
, bạn có thể sử dụng navController.currentBackStackEntryAsState()
rồi lấy destination:
hiện tại
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...
@Composable
fun RallyApp() {
RallyTheme {
val navController = rememberNavController()
val currentBackStack by navController.currentBackStackEntryAsState()
// Fetch your currentDestination:
val currentDestination = currentBackStack?.destination
// ...
}
}
currentBackStack?.destination
trả về NavDestination
.
. Để cập nhật lại currentScreen
đúng cách, bạn cần tìm cách kết hợp NavDestination
trở lại với một trong ba thành phần kết hợp màn hình chính của Rally. Bạn phải xác định đích đến nào đang hiển thị để có thể truyền thông tin này đến RallyTabRow.
. Như đã đề cập trước đó, mỗi đích đến có một tuyến duy nhất, do đó, chúng ta có thể sử dụng tuyến String (Chuỗi) này làm mã nhận dạng sắp xếp để thực hiện phép so sánh được xác minh và tìm được kết quả tương ứng duy nhất.
Để cập nhật currentScreen
, bạn cần lặp lại danh sách rallyTabRowScreens
để tìm tuyến đường trùng khớp, sau đó trả về RallyDestination
tương ứng. Kotlin cung cấp một hàm .find()
hữu ích cho việc đó:
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...
@Composable
fun RallyApp() {
RallyTheme {
val navController = rememberNavController()
val currentBackStack by navController.currentBackStackEntryAsState()
val currentDestination = currentBackStack?.destination
// Change the variable to this and use Overview as a backup screen if this returns null
val currentScreen = rallyTabRowScreens.find { it.route == currentDestination?.route } ?: Overview
// ...
}
}
Vì currentScreen
đã được chuyển đến RallyTabRow
, nên bạn có thể chạy ứng dụng này và xác minh giao diện người dùng thanh thẻ hiện đang được cập nhật tương ứng.
6. Trích xuất các thành phần kết hợp màn hình từ RallyDestinations
Cho đến nay, để đơn giản, chúng tôi đã sử dụng thuộc tính screen
từ giao diện RallyDestination
và các đối tượng màn hình mở rộng từ giao diện này, để thêm giao diện người dùng có thể kết hợp trong NavHost (RallyActivity.kt
):
import com.example.compose.rally.ui.overview.OverviewScreen
// ...
NavHost(
navController = navController,
startDestination = Overview.route,
modifier = Modifier.padding(innerPadding)
) {
composable(route = Overview.route) {
Overview.screen()
}
// ...
}
Tuy nhiên, các bước sau trong lớp học lập trình này (chẳng hạn như sự kiện nhấp chuột) sẽ yêu cầu chuyển trực tiếp thông tin bổ sung đến màn hình có thể kết hợp. Trong môi trường phát hành, chắc chắn sẽ có nhiều dữ liệu hơn cần được truyền.
Để đạt được mục tiêu này, bạn nên thêm các thành phần kết hợp trực tiếp vào biểu đồ điều hướng NavHost
và trích xuất các thành phần đó từ RallyDestination
. Sau đó, RallyDestination
và các đối tượng màn hình sẽ chỉ chứa thông tin dành riêng cho việc điều hướng, như icon
và route
, và sẽ được tách riêng khỏi mọi nội dung liên quan đến giao diện người dùng trong Compose.
Mở RallyDestinations.kt
. Trích xuất thành phần kết hợp của mỗi màn hình từ tham số screen
của đối tượng RallyDestination
, và vào các hàm composable
tương ứng trong NavHost
, thay thế lệnh gọi .screen()
trước đó, như sau:
import com.example.compose.rally.ui.accounts.AccountsScreen
import com.example.compose.rally.ui.bills.BillsScreen
import com.example.compose.rally.ui.overview.OverviewScreen
// ...
NavHost(
navController = navController,
startDestination = Overview.route,
modifier = Modifier.padding(innerPadding)
) {
composable(route = Overview.route) {
OverviewScreen()
}
composable(route = Accounts.route) {
AccountsScreen()
}
composable(route = Bills.route) {
BillsScreen()
}
}
Tại thời điểm này, bạn có thể yên tâm để xoá tham số screen
khỏi RallyDestination
và các đối tượng của tham số đó:
interface RallyDestination {
val icon: ImageVector
val route: String
}
/**
* Rally app navigation destinations
*/
object Overview : RallyDestination {
override val icon = Icons.Filled.PieChart
override val route = "overview"
}
// ...
Chạy lại ứng dụng và xác minh mọi thứ vẫn hoạt động như trước đây. Giờ khi đã hoàn tất bước này, bạn có thể thiết lập các sự kiện nhấp chuột bên trong màn hình có thể kết hợp.
Triển khai sự kiện nhấp vào màn hình Overview
Hiện tại, mọi sự kiện nhấp chuột trong OverviewScreen
đều bị bỏ qua. Điều này có nghĩa là các nút Tài khoản và hóa đơn "SEE ALL" (XEM TẤT CẢ) có thể nhấp được, nhưng trên thực tế, bạn không được đưa đến bất kỳ nơi nào. Mục tiêu của bước này là bật thao tác di chuyển cho những sự kiện nhấp chuột này.
Thành phần kết hợp OverviewScreen
có thể chấp nhận một số hàm làm lệnh gọi lại để đặt làm sự kiện nhấp. Trong trường hợp này, phải có các thao tác điều hướng đưa bạn đến AccountsScreen
hoặc BillsScreen
. Hãy truyền các lệnh gọi lại điều hướng này đến onClickSeeAllAccounts
và onClickSeeAllBills
để điều hướng đến các đích đến liên quan.
Mở RallyActivity.kt
, tìm OverviewScreen
trong NavHost
và truyền navController.navigateSingleTopTo(...)
vào cả hai lệnh gọi lại điều hướng với các tuyến đường tương ứng:
OverviewScreen(
onClickSeeAllAccounts = {
navController.navigateSingleTopTo(Accounts.route)
},
onClickSeeAllBills = {
navController.navigateSingleTopTo(Bills.route)
}
)
navController
hiện sẽ có đủ thông tin, chẳng hạn như tuyến đường của điểm đến chính xác,
để chuyển đến đích đến phù hợp bằng một lượt nhấp vào nút. Nếu xem xét cách triển khai OverviewScreen
, bạn sẽ thấy các lệnh gọi lại này đang được đặt thành các tham số onClick
tương ứng:
@Composable
fun OverviewScreen(...) {
// ...
AccountsCard(
onClickSeeAll = onClickSeeAllAccounts,
onAccountClick = onAccountClick
)
// ...
BillsCard(
onClickSeeAll = onClickSeeAllBills
)
}
Như đã đề cập trước đó, hãy giữ navController
ở cấp cao nhất trong hệ phân cấp điều hướng, đồng thời di chuyển lên cấp của thành phần kết hợp App
(thay vì truyền trực tiếp vào đó, ví dụ: OverviewScreen)
, giúp bạn dễ dàng xem trước, sử dụng lại và kiểm thử thành phần kết hợp OverviewScreen
một cách riêng biệt mà không cần phải dựa vào thực thể navController
thực tế hoặc mô phỏng. Việc chuyển lệnh gọi lại thay vào đó cũng cho phép thay đổi nhanh sự kiện nhấp chuột của bạn!
7. Điều hướng đến SingleAccountScreen có đối số
Hãy thêm một số chức năng mới vào màn hình Accounts
và Overview
! Hiện tại, những màn hình này hiển thị một danh sách một số loại tài khoản – "Kiểm tra", "Tiết kiệm tiền mua nhà", v.v.
Tuy nhiên, việc nhấp vào các loại tài khoản này vẫn chưa có tác dụng nào. Hãy khắc phục vấn đề này. Khi nhấn vào từng loại tài khoản, chúng ta muốn hiển thị một màn hình mới có thông tin chi tiết đầy đủ về tài khoản. Để làm như vậy, chúng tôi cần cung cấp thêm thông tin cho navController
về loại tài khoản chính xác đang nhấp vào. Bạn có thể thực hiện việc này qua các đối số.
Đối số là một công cụ rất mạnh mẽ, cho phép định tuyến động bằng cách truyền một hoặc nhiều đối số đến một tuyến đường. Cho phép hiển thị nhiều thông tin dựa trên các đối số khác nhau được cung cấp.
Trong RallyApp
, hãy thêm một đích đến mới là SingleAccountScreen
. Đích đến này sẽ xử lý việc hiển thị các tài khoản cá nhân này trong biểu đồ bằng cách thêm một hàm composable
mới vào NavHost:
hiện có
import com.example.compose.rally.ui.accounts.SingleAccountScreen
// ...
NavHost(
navController = navController,
startDestination = Overview.route,
modifier = Modifier.padding(innerPadding)
) {
...
composable(route = SingleAccount.route) {
SingleAccountScreen()
}
}
Thiết lập đích đến SingleAccountScreen
Khi bạn đáp vào SingleAccountScreen
, điểm đến này cần có thêm thông tin để biết chính xác loại tài khoản mà địa điểm này sẽ hiển thị khi mở cửa. Chúng ta có thể sử dụng đối số để truyền loại thông tin này. Bạn cần chỉ định về việc tuyến đường của nó cũng cần có đối số {account_type}
. Nếu xem RallyDestination
và đối tượng SingleAccount
của đối tượng này, bạn sẽ thấy đối số này đã được định nghĩa để bạn sử dụng dưới dạng Chuỗi accountTypeArg
.
Để truyền đối số cùng với tuyến đường của bạn khi điều hướng, bạn cần nối các đối số đó với nhau, theo mẫu: "route/{argument}"
. Trong trường hợp của bạn, lệnh sẽ có dạng như sau: "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
. Hãy nhớ ký hiệu $ được dùng để thoát biến:
import androidx.navigation.NavType
import androidx.navigation.compose.navArgument
// ...
composable(
route =
"${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
) {
SingleAccountScreen()
}
Việc này sẽ đảm bảo khi một hành động được kích hoạt để điều hướng đến SingleAccountScreen
, đối số accountTypeArg
cũng phải được truyền, nếu không thì quá trình điều hướng sẽ không thành công. Hãy coi đây là một chữ ký hoặc một hợp đồng cần phải được theo dõi bởi các đích đến khác muốn điều hướng đến SingleAccountScreen
.
Bước thứ hai là thiết lập để composable
này nhận biết các đối số. Bạn thực hiện điều này bằng cách xác định tham số arguments
. Bạn có thể xác định bao nhiêu đối số tùy thích, vì theo mặc định, hàm composable
chấp nhận danh sách các đối số. Trong trường hợp của bạn, bạn chỉ cần thêm một chế độ cài đặt có tên là accountTypeArg
, sau đó thêm một số dữ liệu an toàn khác bằng cách chỉ định mã này là loại String
. Nếu bạn không đặt rõ một kiểu, thì kiểu đó sẽ được suy ra từ giá trị mặc định của đối số này:
import androidx.navigation.NavType
import androidx.navigation.compose.navArgument
// ...
composable(
route =
"${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
arguments = listOf(
navArgument(SingleAccount.accountTypeArg) { type = NavType.StringType }
)
) {
SingleAccountScreen()
}
Mã này sẽ hoạt động hoàn hảo và bạn có thể chọn giữ lại mã như thế này. Tuy nhiên, vì mọi thông tin cụ thể về đích đến đều nằm trong RallyDestinations.kt
và các đối tượng của nó, nên chúng ta hãy tiếp tục áp dụng cách thức tương tự (như đã thực hiện ở trên đối với Overview
, Accounts,
và Bills
), đồng thời di chuyển các đối số này vào SingleAccount:
object SingleAccount : RallyDestination {
// ...
override val route = "single_account"
const val accountTypeArg = "account_type"
val arguments = listOf(
navArgument(accountTypeArg) { type = NavType.StringType }
)
}
Thay thế các đối số trước đó bằng SingleAccount.arguments
hiện quay trở lại composable
tương ứng trên NavHost. Việc này cũng đảm bảo là chúng ta sẽ giữ cho NavHost
sạch và dễ đọc nhất có thể:
composable(
route = "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
arguments = SingleAccount.arguments
) {
SingleAccountScreen()
}
Sau khi xác định được tuyến hoàn chỉnh với các đối số cho SingleAccountScreen
, bạn hãy đảm bảo rằng accountTypeArg
này được tiếp tục truyền đến thành phần kết hợp SingleAccountScreen
để thành phần này biết được đâu là loại tài khoản cần hiển thị chính xác. Nếu xem xét cách triển khai SingleAccountScreen
, bạn sẽ thấy thiết bị đã được thiết lập và đang chờ chấp nhận tham số accountType
:
fun SingleAccountScreen(
accountType: String? = UserData.accounts.first().name
) {
// ...
}
Cho đến thời điểm hiện tại:
- Bạn phải chắc chắn việc xác định tuyến đường để yêu cầu đối số, làm tín hiệu cho các đích đến trước đó
- Bạn phải đảm bảo
composable
biết cần phải chấp nhận các đối số
Bước cuối cùng là thực sự truy xuất đối số đã truyền theo cách nào đó.
Trong Navigation Navigation, mỗi hàm có khả năng kết hợp NavHost
đều có quyền truy cập vào NavBackStackEntry
hiện tại – một lớp chứa thông tin về tuyến đường hiện tại và truyền các đối số của một mục nhập trong ngăn xếp lui. Bạn có thể sử dụng tính năng này để lấy danh sách arguments
bắt buộc từ navBackStackEntry
, sau đó tìm kiếm và truy xuất chính xác đối số cần thiết để chuyển đối số đó xuống màn hình kết hợp.
Trong trường hợp này, bạn sẽ yêu cầuaccountTypeArg
từnavBackStackEntry
. Sau đó, bạn cần truyền tiếp tham số đó xuống tham số accountType
của SingleAccountScreen'
.
Bạn cũng có thể cung cấp một giá trị mặc định cho đối số làm phần giữ chỗ trong trường hợp đối số đó chưa được cung cấp, đồng thời giúp mã của bạn an toàn hơn bằng cách bao gồm trường hợp đặc biệt (edge case) này.
Mã của bạn bây giờ sẽ có dạng như sau:
NavHost(...) {
// ...
composable(
route =
"${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
arguments = SingleAccount.arguments
) { navBackStackEntry ->
// Retrieve the passed argument
val accountType =
navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)
// Pass accountType to SingleAccountScreen
SingleAccountScreen(accountType)
}
}
SingleAccountScreen
của bạn hiện đã có thông tin cần thiết để hiển thị đúng loại tài khoản khi bạn chuyển đến đó. Nếu xem xét cách triển khai SingleAccountScreen,
, bạn có thể thấy tính năng này đã thực hiện việc so khớp accountType
được chuyển với nguồn UserData
để tìm nạp thông tin chi tiết tương ứng về tài khoản.
Hãy thực hiện thêm một nhiệm vụ tối ưu hoá nhỏ, đồng thời di chuyển tuyến "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
vào RallyDestinations.kt
và đối tượng SingleAccount
của tuyến này:
object SingleAccount : RallyDestination {
// ...
override val route = "single_account"
const val accountTypeArg = "account_type"
val routeWithArgs = "${route}/{${accountTypeArg}}"
val arguments = listOf(
navArgument(accountTypeArg) { type = NavType.StringType }
)
}
Và một lần nữa, hãy thay thế mã này trong NavHost composable:
tương ứng
// ...
composable(
route = SingleAccount.routeWithArgs,
arguments = SingleAccount.arguments
) {...}
Thiết lập đích đến cho Tài khoản và Tổng quan
Sau khi bạn đã định nghĩa được tuyến SingleAccountScreen
và đối số mà tuyến này yêu cầu, đồng thời chấp nhận thực hiện điều hướng thành công đến SingleAccountScreen
, bạn cần đảm bảo đối số accountTypeArg
đó đang được truyền từ đích đến trước (nghĩa là một đích đến bắt đầu bất kỳ của bạn).
Như bạn thấy, đối tượng này có 2 phía – đích đến bắt đầu cung cấp và truyền một đối số, còn đích đến kết thúc tiếp nhận và sử dụng đối số đó để hiển thị thông tin chính xác. Bạn cần xác định cả hai.
Ví dụ như khi bạn đang ở đích Accounts
và nhấn vào loại tài khoản "Đang kiểm tra", đích Tài khoản cần chuyển một Chuỗi "Đang kiểm tra" làm đối số, nối vào tuyến đường Chuỗi "single_account", để mở thành công SingleAccountScreen
tương ứng. Tuyến đường của chuỗi sẽ có dạng như sau: "single_account/Checking"
Bạn sẽ sử dụng cùng một tuyến đường này với đối số được truyền khi sử dụng navController.navigateSingleTopTo(...),
như sau:
navController.navigateSingleTopTo("${SingleAccount.route}/$accountType")
.
Chuyển lệnh gọi lại thao tác điều hướng này đến tham số onAccountClick
của OverviewScreen
và AccountsScreen
. Vui lòng lưu ý các tham số này được xác định trước là onAccountClick: (String) -> Unit
, trong đó Chuỗi là dữ liệu đầu vào. Tức là khi người dùng nhấn vào một loại tài khoản cụ thể trong Overview
và Account
, loại tài khoản đó là Chuỗi sẽ có sẵn cho bạn, cũng như có thể dễ dàng được chuyển dưới dạng đối số điều hướng:
OverviewScreen(
// ...
onAccountClick = { accountType ->
navController
.navigateSingleTopTo("${SingleAccount.route}/$accountType")
}
)
// ...
AccountsScreen(
// ...
onAccountClick = { accountType ->
navController
.navigateSingleTopTo("${SingleAccount.route}/$accountType")
}
)
Để mọi thứ dễ đọc, bạn có thể trích xuất hành động điều hướng này thành một trình trợ giúp riêng, hàm mở rộng:
import androidx.navigation.NavHostController
// ...
OverviewScreen(
// ...
onAccountClick = { accountType ->
navController.navigateToSingleAccount(accountType)
}
)
// ...
AccountsScreen(
// ...
onAccountClick = { accountType ->
navController.navigateToSingleAccount(accountType)
}
)
// ...
private fun NavHostController.navigateToSingleAccount(accountType: String) {
this.navigateSingleTopTo("${SingleAccount.route}/$accountType")
}
Khi chạy ứng dụng tại thời điểm này, bạn có thể nhấp vào từng loại tài khoản và sẽ được chuyển đến SingleAccountScreen
tương ứng hiển thị dữ liệu cho tài khoản được chọn.
8. Bật chế độ hỗ trợ liên kết sâu
Ngoài việc thêm các đối số, bạn cũng có thể thêm đường liên kết sâu để liên kết một URL, hành động và/hoặc loại mime cụ thể với một thành phần kết hợp. Trên Android, đường liên kết sâu là đường đưa bạn đến thẳng một đích đến cụ thể trong ứng dụng. Navigation Compose hỗ trợ đường liên kết sâu ngầm ẩn. Khi đường liên kết sâu ngầm ẩn được gọi (ví dụ như khi người dùng nhấp vào một đường liên kết), Android có thể mở ứng dụng tại đích đến tương ứng.
Trong phần này, bạn sẽ thêm một đường liên kết sâu mới để chuyển đến thành phần kết hợp SingleAccountScreen
với loại tài khoản tương ứng, đồng thời cho phép đường liên kết sâu này hiển thị với các ứng dụng bên ngoài. Để làm mới bộ nhớ, tuyến đường cho thành phần kết hợp này là "single_account/{account_type}"
, đây cũng là những gì bạn sẽ sử dụng cho liên kết sâu, với một số thay đổi nhỏ liên quan đến liên kết sâu.
Vì theo mặc định, tính năng hiển thị đường liên kết sâu đến các ứng dụng bên ngoài chưa được bật, nên bạn cũng phải thêm các phần tử <intent-filter>
vào tệp manifest.xml
của ứng dụng này. Do đó, đây sẽ là bước đầu tiên của bạn.
Bắt đầu bằng cách thêm đường liên kết sâu vào AndroidManifest.xml
của ứng dụng. Bạn cần tạo một bộ lọc ý định mới qua <intent-filter>
bên trong <activity>
, với thao tác VIEW
và các danh mục BROWSABLE
và DEFAULT
.
Sau đó, bên trong bộ lọc, bạn cần thẻ data
để thêm scheme
(rally
– tên của ứng dụng) và host
(single_account
– định tuyến tới thành phần kết hợp) để xác định đường liên kết sâu chính xác. Thao tác này sẽ cung cấp cho bạn rally://single_account
dưới dạng URL liên kết sâu.
Vui lòng lưu ý là bạn không cần khai báo đối số account_type
trong AndroidManifest
. Mã này sẽ được thêm vào sau trong hàm có khả năng kết hợp NavHost
.
<activity
android:name=".RallyActivity"
android:windowSoftInputMode="adjustResize"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="rally" android:host="single_account" />
</intent-filter>
</activity>
Kích hoạt và xác minh đường liên kết sâu
Giờ bạn đã có thể phản hồi ý định đến từ bên trong RallyActivity
.
Thành phần kết hợp SingleAccountScreen
đã chấp nhận các đối số, nhưng giờ đây cũng cần chấp nhận đường liên kết sâu mới tạo để khởi chạy đích đến này khi đường liên kết sâu của nó được kích hoạt.
Bên trong hàm kết hợp của SingleAccountScreen
, hãy thêm một tham số deepLinks
nữa. Tương tự như arguments,
, thuộc tính này cũng chấp nhận danh sách navDeepLink
, vì bạn có thể xác định nhiều đường liên kết sâu dẫn đến cùng một đích đến. Truyền uriPattern
phù hợp với một thuộc tính được xác định trongintent-filter
trong tệp kê khai - rally://singleaccount
, nhưng lần này bạn cũng sẽ thêm đối số accountTypeArg
của nó:
import androidx.navigation.navDeepLink
// ...
composable(
route = SingleAccount.routeWithArgs,
// ...
deepLinks = listOf(navDeepLink {
uriPattern = "rally://${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
})
)
Bạn biết các thao tác tiếp theo chứ? Di chuyển danh sách này vào RallyDestinations SingleAccount:
object SingleAccount : RallyDestination {
// ...
val arguments = listOf(
navArgument(accountTypeArg) { type = NavType.StringType }
)
val deepLinks = listOf(
navDeepLink { uriPattern = "rally://$route/{$accountTypeArg}"}
)
}
Và một lần nữa, hãy thay thế mã này trong thành phần kết hợp NavHost
tương ứng
// ...
composable(
route = SingleAccount.routeWithArgs,
arguments = SingleAccount.arguments,
deepLinks = SingleAccount.deepLinks
) {...}
Kiểm thử liên kết sâu bằng adb
Ứng dụng và SingleAccountScreen
hiện đã sẵn sàng để xử lý các đường liên kết sâu. Để kiểm tra xem ứng dụng có hoạt động đúng cách hay không, hãy cài đặt làm mới Rally trên một trình mô phỏng hoặc thiết bị đã kết nối, mở dòng lệnh rồi thực thi lệnh sau để mô phỏng việc chạy đường liên kết sâu:
adb shell am start -d "rally://single_account/Checking" -a android.intent.action.VIEW
Thao tác này sẽ đưa bạn đến thẳng tài khoản "Đang kiểm tra". Tuy nhiên, bạn cũng có thể xác minh việc tài khoản đó hoạt động đúng cách đối với tất cả các loại tài khoản khác.
9. Trích xuất NavHost thành RallyNavHost
Bạn hiện đã hoàn tất thành phần kết hợp NavHost
. Tuy nhiên, để hàm này có thể kiểm thử được, đồng thời giữ cho RallyActivity
sạch hơn, bạn có thể trích xuất NavHost
hiện tại và các hàm trợ giúp (chẳng hạn như navigateToSingleAccount
) từ thành phần kết hợp RallyApp
thành hàm có khả năng kết hợp và đặt tên là RallyNavHost
.
RallyApp
là thành phần kết hợp duy nhất sẽ hoạt động trực tiếp với navController
. Như đã đề cập trước đó, mọi màn hình kết hợp khác sẽ chỉ nhận lệnh gọi lại điều hướng, không phải chính navController
.
Do đó, RallyNavHost
mới sẽ chấp nhận navController
và modifier
làm tham số từ RallyApp
:
@Composable
fun RallyNavHost(
navController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = Overview.route,
modifier = modifier
) {
composable(route = Overview.route) {
OverviewScreen(
onClickSeeAllAccounts = {
navController.navigateSingleTopTo(Accounts.route)
},
onClickSeeAllBills = {
navController.navigateSingleTopTo(Bills.route)
},
onAccountClick = { accountType ->
navController.navigateToSingleAccount(accountType)
}
)
}
composable(route = Accounts.route) {
AccountsScreen(
onAccountClick = { accountType ->
navController.navigateToSingleAccount(accountType)
}
)
}
composable(route = Bills.route) {
BillsScreen()
}
composable(
route = SingleAccount.routeWithArgs,
arguments = SingleAccount.arguments,
deepLinks = SingleAccount.deepLinks
) { navBackStackEntry ->
val accountType =
navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)
SingleAccountScreen(accountType)
}
}
}
fun NavHostController.navigateSingleTopTo(route: String) =
this.navigate(route) { launchSingleTop = true }
private fun NavHostController.navigateToSingleAccount(accountType: String) {
this.navigateSingleTopTo("${SingleAccount.route}/$accountType")
}
Giờ thì hãy thêm RallyNavHost
mới vào RallyApp
và chạy lại ứng dụng để xác minh mọi thứ hoạt động như trước:
fun RallyApp() {
RallyTheme {
...
Scaffold(
...
) { innerPadding ->
RallyNavHost(
navController = navController,
modifier = Modifier.padding(innerPadding)
)
}
}
}
10. Kiểm thử tính năng Compose Navigation
Từ đầu lớp học lập trình này, bạn phải đảm bảo không truyền navController
trực tiếp vào bất kỳ thành phần kết hợp nào (ngoài ứng dụng cấp cao) và chuyể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ả các thành phần kết hợp đều có thể thử nghiệm riêng lẻ, vì chúng không yêu cầu thực thể navController
trong thử nghiệm.
Bạn phải luôn kiểm tra để đảm bảo toàn bộ cơ chế Điều hướng Compose hoạt động như dự kiến trong ứng dụng của mình bằng cách kiểm thử RallyNavHost
, và các thao tác điều hướng được chuyển đến nội dung kết hợp. Đây sẽ là những mục tiêu chính của phần này. Để kiểm thử riêng từng hàm có khả năng kết hợp, vui lòng tham khảo lớp học lập trình Kiểm thử trong Jetpack Compose.
Để bắt đầu kiểm thử, trước tiên, chúng ta cần thêm các phần phụ thuộc kiểm thử cần thiết. Vì vậy, hãy quay lại tệp bản dựng của ứng dụng tại app/build.gradle
. Trong mục phần phụ thuộc, hãy thêm phần phụ thuộc navigation-testing
:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$rootProject.composeNavigationVersion"
// ...
}
Chuẩn bị lớp NavigationTest
Bạn có thể kiểm thử RallyNavHost
riêng biệt với Activity
.
Vì chương trình kiểm thử này sẽ vẫn chạy trên thiết bị Android, bạn cần tạo thư mục kiểm thử /app/src/androidTest/java/com/example/compose/rally
rồi tạo một lớp kiểm thử tệp kiểm thử mới và đặt tên cho lớp đó là NavigationTest
.
Trước tiên, để sử dụng các API kiểm thử Compose cũng như kiểm trả và kiểm soát các thành phần kết hợp và ứng dụng bằng Compose, hãy thêm một quy tắc kiểm thử Compose:
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
}
Viết kiểm thử đầu tiên
Tạo một hàm kiểm thử rallyNavHost
công khai và chú thích hàm @Test
đó. Trong hàm này, trước tiên bạn cần thiết lập nội dung Compose mà bạn muốn kiểm thử. Bạn sẽ thực hiện việc này bằng cách sử dụng setContent
của composeTestRule
. Hàm này sẽ lấy một tham số có thể kết hợp làm nội dung, đồng thời cho phép bạn viết mã Compose và thêm các thành phần kết hợp trong môi trường thử nghiệm, giống như khi bạn đang sử dụng một ứng dụng môi trường sản xuất thông thường.
Bên trong setContent,
, bạn có thể thiết lập đối tượng kiểm thử hiện tại, RallyNavHost
và truyền một thực thể của thực thể navController
mới tới đối tượng đó. Cấu phần phần mềm kiểm thử Navigation cung cấp một TestNavHostController
hữu ích để sử dụng. Vì thế hãy thêm bước này:
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.testing.TestNavHostController
import org.junit.Assert.fail
import org.junit.Test
// ...
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Test
fun rallyNavHost() {
composeTestRule.setContent {
// Creates a TestNavHostController
navController =
TestNavHostController(LocalContext.current)
// Sets a ComposeNavigator to the navController so it can navigate through composables
navController.navigatorProvider.addNavigator(
ComposeNavigator()
)
RallyNavHost(navController = navController)
}
fail()
}
}
Nếu bạn sao chép mã ở trên, lệnh gọi fail()
sẽ đảm bảo kiểm thử không đạt cho đến khi đưa ra một xác nhận (assertion) thực sự. Lệnh này như một lời nhắc để hoàn tất quá trình kiểm thử.
Để xác minh rằng thành phần kết hợp màn hình được hiển thị là chính xác, bạn có thể sử dụng contentDescription
để xác nhận rằng thành phần đó được hiển thị. Trong lớp học lập trình này, trước đây bạn đã thiết lập contentDescription
cho tài khoản và đích đến Tổng quan để có thể sử dụng chúng cho quá trình xác minh thử nghiệm.
Với lần xác minh đầu tiên, bạn cần kiểm tra để đảm bảo màn hình Tổng quan được hiển thị làm đích đến đầu tiên khi RallyNavHost
được khởi chạy lần đầu. Bạn cũng nên đổi tên bài kiểm thử để phản ánh điều đó – hãy gọi hàm rallyNavHost_verifyOverviewStartDestination
. Bạn có thể thực hiện việc này bằng cách thay thế lệnh gọi fail()
với lệnh bên dưới:
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
// ...
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Test
fun rallyNavHost_verifyOverviewStartDestination() {
composeTestRule.setContent {
navController =
TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(
ComposeNavigator()
)
RallyNavHost(navController = navController)
}
composeTestRule
.onNodeWithContentDescription("Overview Screen")
.assertIsDisplayed()
}
}
Chạy lại bài kiểm thử và xác minh bài kiểm thử đó đạt.
Vì bạn cần thiết lập RallyNavHost
theo cách tương tự cho từng kiểm thử sắp tới, bạn có thể trích xuất khởi chạy của hàm này vào hàm @Before
có chú thích, để tránh việc lặp lại không cần thiết và giữ cho kiểm thử ngắn gọn hơn:
import org.junit.Before
// ...
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Before
fun setupRallyNavHost() {
composeTestRule.setContent {
navController =
TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(
ComposeNavigator()
)
RallyNavHost(navController = navController)
}
}
@Test
fun rallyNavHost_verifyOverviewStartDestination() {
composeTestRule
.onNodeWithContentDescription("Overview Screen")
.assertIsDisplayed()
}
}
Điều hướng trong quá trình kiểm thử
Bạn có thể kiểm thử việc 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 các thành phầ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 đường dự kiến với tuyến đường hiện tại.
Kiểm thử thông qua các lượt nhấp vào giao diện người dùng và contentDescription trên màn hình
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. Văn bản tiếp theo có thể xác minh việc khi ở màn hình Tổng quan, thao tác nhấp vào nút "SEE ALL" (XEM TẤT CẢ) trong tiểu mục Tài khoản sẽ đưa bạn đến đích Tài khoản:
Bạn sẽ sử dụng lại contentDescription
được đặt trên nút cụ thể này trong thành phần kết hợp OverviewScreenCard
, mô phỏng một lần nhấp vào nút đó qua performClick()
và xác minh rằng đích đến Tài khoản được hiển thị sau đó:
import androidx.compose.ui.test.performClick
// ...
@Test
fun rallyNavHost_clickAllAccount_navigatesToAccounts() {
composeTestRule
.onNodeWithContentDescription("All Accounts")
.performClick()
composeTestRule
.onNodeWithContentDescription("Accounts Screen")
.assertIsDisplayed()
}
Bạn có thể làm theo mẫu này để kiểm tra tất cả các thao tác nhấp còn lại trong ứng dụng.
Kiểm thử thông qua so sánh các lộ trình và lượt nhấp trên giao diện người dùng
Bạn cũng có thể sử dụng navController
để kiểm tra xác nhận 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. Để thực hiện việc này, hãy nhấp vào giao diện người dùng giống như trong phần trước, sau đó dùng navController.currentBackStackEntry?.destination?.route
để so sánh tuyến hiện tại với tuyến mà bạn mong đợi.
Bạn nên thực hiện thêm một bước nữa, là trước tiên, hãy di chuyển đến mục con Bills trên màn hình Overview (Tổng quan), nếu không thì quá trình kiểm thử sẽ không thành công vì không thể tìm thấy nút có contentDescription
"All Bills" (Tất cả hoá đơn):
import androidx.compose.ui.test.performScrollTo
import org.junit.Assert.assertEquals
// ...
@Test
fun rallyNavHost_clickAllBills_navigateToBills() {
composeTestRule.onNodeWithContentDescription("All Bills")
.performScrollTo()
.performClick()
val route = navController.currentBackStackEntry?.destination?.route
assertEquals(route, "bills")
}
Khi làm theo các mẫu này, bạn có thể hoàn tất lớp kiểm thử bằng cách đưa vào mọi tuyến đường điều hướng, đích đến và hành động nhấp bổ sung. Chạy toàn bộ tập hợp kiểm thử ngay để xác minh chúng đã đạt.
11. Xin chúc mừng
Xin chúc mừng, bạn đã hoàn tất thành công lớp học lập trình này! Bạn có thể tìm thấy mã giải pháp tại đây và so sánh với mã giải pháp của mình.
Bạn đã thêm thành phần điều hướng Jetpack Compose vào ứng dụng Rally, đồng thời hiểu rõ các khái niệm chính của ứng dụng. Bạn đã tìm hiểu cách thiết lập biểu đồ điều hướng cho các đích đến có thể kết hợp, xác định các hành động và tuyến điều hướng, truyền thông tin bổ sung đến các tuyến thông qua đối số, thiết lập đường liên kết sâu và kiểm thử các chức năng điều hướng.
Để tìm hiểu về các chủ đề và thông tin khác, chẳng hạn như tích hợp thanh điều hướng dưới cùng, điều hướng nhiều mô-đun và biểu đồ lồng nhau, bạn có thể xem kho lưu trữ dành cho ứng dụng Now in Android trên GitHub và xem cách triển khai tại đó.
Nội dung tiếp theo là gì?
Vui lòng xem các tài liệu này để tiếp tục lộ trình học tập trong Jetpack Compose :
Thông tin khác về Jetpack Navigation (Điều hướng Jetpack):