1. Giới thiệu
Trong lớp học lập trình trước, bạn đã bắt đầu chuyển đổi ứng dụng Reply (Trả lời) để thích ứng bằng cách sử dụng các lớp kích thước cửa sổ và triển khai chức năng điều hướng động. Các tính năng này là nền tảng quan trọng và là bước đầu tiên để xây dựng ứng dụng phù hợp với mọi kích thước màn hình. Nếu đã bỏ lỡ lớp học lập trình Tạo ứng dụng thích ứng bằng tính năng điều hướng động thì bạn nên quay lại và bắt đầu từ đó.
Trong lớp học lập trình này, bạn sẽ xây dựng dựa trên một khái niệm đã học để triển khai thêm bố cục thích ứng trong ứng dụng. Bố cục thích ứng mà bạn sẽ triển khai là một phần của bố cục chuẩn - một tập hợp các mẫu thường dùng cho màn hình lớn. Bạn cũng sẽ tìm hiểu thêm về các kỹ thuật kiểm thử và công cụ để có thể nhanh chóng xây dựng các ứng dụng mạnh mẽ.
Điều kiện tiên quyết
- Hoàn thành lớp học lập trình Tạo ứng dụng thích ứng bằng tính năng điều hướng động
- Làm quen với lập trình Kotlin, bao gồm các lớp, hàm và điều kiện
- Làm quen với các lớp
ViewModel
- Làm quen với các hàm
Composable
- Trải nghiệm xây dựng bố cục bằng Jetpack Compose
- Trải nghiệm chạy các ứng dụng trên một thiết bị hoặc trình mô phỏng
- Trải nghiệm khi sử dụng
WindowSizeClass
API
Kiến thức bạn sẽ học được
- Cách tạo bố cục thích ứng cho mẫu chế độ xem danh sách bằng Jetpack Compose
- Cách tạo bản xem trước cho nhiều kích thước màn hình
- Cách kiểm tra mã cho nhiều kích thước màn hình
Sản phẩm bạn sẽ tạo ra
- Bạn sẽ tiếp tục cập nhật ứng dụng Reply (Trả lời) để thích ứng với mọi kích thước màn hình.
Ứng dụng sau cùng sẽ có giao diện như sau:
Bạn cần có
- Một máy tính có quyền truy cập Internet, trình duyệt web và Android Studio
- Quyền truy cập vào GitHub
Tải mã khởi đầu xuống
Để bắt đầu, hãy tải mã khởi đầu xuống:
Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho mã:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git $ cd basic-android-kotlin-compose-training-reply-app $ git checkout nav-update
Bạn có thể duyệt tìm mã khởi đầu trong kho lưu trữ GitHub Reply
.
2. Bản xem trước cho nhiều kích thước màn hình
Tạo bản xem trước cho nhiều kích thước màn hình
Trong lớp học lập trình Tạo ứng dụng thích ứng bằng tính năng điều hướng động, bạn đã tìm hiểu cách sử dụng thành phần kết hợp xem trước để giúp quá trình phát triển của mình. Đối với ứng dụng thích ứng, phương pháp hay nhất là tạo nhiều bản xem trước để hiển thị ứng dụng trên nhiều kích thước màn hình. Với nhiều bản xem trước, bạn có thể thấy các thay đổi trên tất cả kích thước màn hình cùng lúc. Ngoài ra, bản xem trước còn là tài liệu để các nhà phát triển khác xem mã của bạn thấy rằng ứng dụng tương thích với nhiều kích thước màn hình.
Trước đây, bạn chỉ có một bản xem trước duy nhất hỗ trợ màn hình thu gọn. Sắp tới, bạn sẽ thêm các bản xem trước khác.
Để thêm bản xem trước cho màn hình trung bình và màn hình mở rộng, hãy hoàn tất các bước sau:
- Thêm một bản xem trước cho màn hình trung bình bằng cách thiết lập một giá trị
widthDp
trung bình trong tham số chú thíchPreview
rồi chỉ định giá trịWindowWidthSizeClass.Medium
làm tham số cho thành phần kết hợpReplyApp
.
MainActivity.kt
...
@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppMediumPreview() {
ReplyTheme {
Surface {
ReplyApp(windowSize = WindowWidthSizeClass.Medium)
}
}
}
...
- Thêm một bản xem trước khác cho màn hình mở rộng bằng cách thiết lập một giá trị
widthDp
lớn trong tham số chú thíchPreview
rồi chỉ định giá trịWindowWidthSizeClass.Expanded
làm tham số cho thành phần kết hợpReplyApp
.
MainActivity.kt
...
@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppExpandedPreview() {
ReplyTheme {
Surface {
ReplyApp(windowSize = WindowWidthSizeClass.Expanded)
}
}
}
...
- Tạo bản xem trước để xem những nội dung sau:
3. Triển khai bố cục nội dung thích ứng
Giới thiệu về khung hiển thị danh sách-chi tiết
Bạn có thể thấy rằng trên màn hình mở rộng, nội dung bị kéo giãn và không tận dụng được không gian màn hình hiện có.
Bạn có thể áp dụng một trong các bố cục chuẩn để cải thiện bố cục này. Bố cục chuẩn là cấu trúc màn hình lớn đóng vai trò là điểm bắt đầu cho hoạt động thiết kế và triển khai. Bạn có thể sử dụng ba bố cục có sẵn để định hướng cách sắp xếp các thành phần phổ biến trong một ứng dụng, chế độ xem danh sách, bảng điều khiển hỗ trợ và nguồn cấp dữ liệu. Mỗi bố cục xem xét một số thành phần và trường hợp sử dụng phổ biến để đáp ứng kỳ vọng và nhu cầu của người dùng về khả năng thích ứng của ứng dụng đối với nhiều kích thước màn hình và điểm ngắt.
Đối với ứng dụng Reply (Trả lời), hãy triển khai chế độ xem chi tiết danh sách, vì chế độ này phù hợp nhất để duyệt qua nội dung và xem nhanh các thông tin chi tiết. Bạn sẽ dùng bố cục khung hiển thị danh sách-chi tiết để tạo một ngăn khác bên cạnh màn hình danh sách email để hiển thị thông tin chi tiết của các email đó. Bố cục này tạo điều kiện để bạn sử dụng màn hình có sẵn để hiện thêm thông tin cho người dùng cũng như giúp ứng dụng hoạt động hiệu quả hơn.
Triển khai khung hiển thị danh sách-chi tiết
Để triển khai khung hiển thị danh sách-chi tiết cho màn hình mở rộng, hãy hoàn tất các bước sau:
- Để thể hiện nhiều loại bố cục nội dung, trên
WindowStateUtils.kt
, hãy tạo một lớpEnum
mới cho các loại nội dung. Dùng giá trịLIST_AND_DETAIL
khi màn hình mở rộng đang được sử dụng, nếu không, hãy dùngLIST_ONLY
.
WindowStateUtils.kt
...
enum class ReplyContentType {
LIST_ONLY, LIST_AND_DETAIL
}
...
- Khai báo biến
contentType
trênReplyApp.kt
rồi chỉ địnhcontentType
phù hợp với nhiều kích thước cửa sổ để giúp xác định lựa chọn phù hợp về loại nội dung, tuỳ theo kích thước màn hình.
ReplyApp.kt
...
import com.example.reply.ui.utils.ReplyContentType
...
val navigationType: ReplyNavigationType
val contentType: ReplyContentType
when (windowSize) {
WindowWidthSizeClass.Compact -> {
...
contentType = ReplyContentType.LIST_ONLY
}
WindowWidthSizeClass.Medium -> {
...
contentType = ReplyContentType.LIST_ONLY
}
WindowWidthSizeClass.Expanded -> {
...
contentType = ReplyContentType.LIST_AND_DETAIL
}
else -> {
...
contentType = ReplyContentType.LIST_ONLY
}
}
...
Tiếp theo, bạn có thể dùng giá trị contentType
để tạo nhánh khác cho các bố cục trong thành phần kết hợp ReplyAppContent
.
- Trong
ReplyHomeScreen.kt
, hãy thêmcontentType
làm tham số vào thành phần kết hợpReplyHomeScreen
.
ReplyHomeScreen.kt
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
contentType: ReplyContentType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit,
onEmailCardPressed: (Email) -> Unit,
onDetailScreenBackPressed: () -> Unit,
modifier: Modifier = Modifier
) {
...
- Truyền giá trị
contentType
đến thành phần kết hợpReplyHomeScreen
.
ReplyApp.kt
...
ReplyHomeScreen(
navigationType = navigationType,
contentType = contentType,
replyUiState = replyUiState,
onTabPressed = { mailboxType: MailboxType ->
viewModel.updateCurrentMailbox(mailboxType = mailboxType)
viewModel.resetHomeScreenStates()
},
onEmailCardPressed = { email: Email ->
viewModel.updateDetailsScreenStates(
email = email
)
},
onDetailScreenBackPressed = {
viewModel.resetHomeScreenStates()
},
modifier = modifier
)
...
- Thêm
contentType
làm tham số cho thành phần kết hợpReplyAppContent
.
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
contentType: ReplyContentType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
...
- Truyền giá trị
contentType
đến hai thành phần kết hợpReplyAppContent
.
ReplyHomeScreen.kt
...
ReplyAppContent(
navigationType = navigationType,
contentType = contentType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
} else {
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
navigationType = navigationType,
contentType = contentType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
isFullScreen = true,
onBackButtonClicked = onDetailScreenBackPressed,
modifier = modifier
)
}
}
...
Hãy hiện danh sách đầy đủ và màn hình chi tiết khi contentType
là LIST_AND_DETAIL
hoặc hiện danh sách chỉ có nội dung email khi contentType
là LIST_ONLY
.
- Trong
ReplyHomeScreen.kt
, hãy thêm câu lệnhif/else
trên thành phần kết hợpReplyAppContent
để hiện thành phần kết hợpReplyListAndDetailContent
khi giá trịcontentType
làLIST_AND_DETAIL
, cũng như hiện thành phần kết hợpReplyListOnlyContent
trên nhánhelse
.
ReplyHomeScreen.kt
...
Column(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.inverseOnSurface)
) {
if (contentType == ReplyContentType.LIST_AND_DETAIL) {
ReplyListAndDetailContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
)
} else {
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
.padding(
horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
)
)
}
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
}
...
- Xoá điều kiện
replyUiState.isShowingHomepage
để hiện ngăn điều hướng cố định vì người dùng không cần chuyển sang chế độ xem chi tiết nếu họ đang dùng chế độ xem mở rộng.
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList,
modifier = Modifier
.wrapContentWidth()
.fillMaxHeight()
.background(MaterialTheme.colorScheme.inverseOnSurface)
.padding(dimensionResource(R.dimen.drawer_padding_content))
)
}
}
) {
...
- Chạy ứng dụng ở chế độ máy tính bảng để xem màn hình dưới đây:
Cải thiện phần tử trên giao diện người dùng cho khung hiển thị danh sách-chi tiết
Hiện tại, ứng dụng hiển thị một ngăn chi tiết trên màn hình chính cho các màn hình mở rộng.
Tuy nhiên, màn hình chứa các phần tử không liên quan (chẳng hạn như nút quay lại, đề mục chủ đề và khoảng đệm bổ sung) vì được thiết kế dưới dạng màn hình chi tiết độc lập. Bạn có thể cải thiện màn hình này ở bước tiếp theo bằng một thao tác điều chỉnh đơn giản.
Để cải thiện màn hình chi tiết ở chế độ xem mở rộng, hãy hoàn tất các bước sau:
- Trong
ReplyDetailsScreen.kt
, hãy thêm một biếnisFullScreen
làm tham sốBoolean
cho thành phần kết hợpReplyDetailsScreen
.
Thao tác thêm biến này giúp bạn phân biệt thành phần kết hợp đó khi sử dụng dưới dạng độc lập, cũng như khi sử dụng bên trong màn hình chính.
ReplyDetailsScreen.kt
...
@Composable
fun ReplyDetailsScreen(
replyUiState: ReplyUiState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
isFullScreen: Boolean = false
) {
...
- Bên trong thành phần kết hợp
ReplyDetailsScreen
, hãy gói thành phần kết hợpReplyDetailsScreenTopBar
bằng một câu lệnhif
để nó chỉ xuất hiện khi ứng dụng ở chế độ toàn màn hình.
ReplyDetailsScreen.kt
...
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.inverseOnSurface)
.padding(top = dimensionResource(R.dimen.detail_card_list_padding_top))
) {
item {
if (isFullScreen) {
ReplyDetailsScreenTopBar(
onBackPressed,
replyUiState,
Modifier
.fillMaxWidth()
.padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
)
)
}
...
Giờ thì bạn đã có thể thêm khoảng đệm vào. Khoảng đệm cần có cho thành phần kết hợp ReplyEmailDetailsCard
còn tuỳ thuộc vào việc bạn có dùng khoảng đệm đó dưới dạng toàn màn hình hay không. Sẽ có thêm khoảng đệm trên các thành phần kết hợp khác khi bạn dùng ReplyEmailDetailsCard
với các thành phần kết hợp khác trên màn hình mở rộng.
- Truyền giá trị
isFullScreen
đến thành phần kết hợpReplyEmailDetailsCard
. Truyền một đối tượng sửa đổi có khoảng đệm ngang làR.dimen.detail_card_outer_padding_horizontal
nếu màn hình ở chế độ toàn màn hình, nếu không, hãy truyền một đối tượng sửa đổi có khoảng đệm cuối làR.dimen.detail_card_outer_padding_horizontal
.
ReplyDetailsScreen.kt
...
item {
if (isFullScreen) {
ReplyDetailsScreenTopBar(
onBackPressed,
replyUiState,
Modifier
.fillMaxWidth()
.padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
)
)
}
ReplyEmailDetailsCard(
email = replyUiState.currentSelectedEmail,
mailboxType = replyUiState.currentMailbox,
isFullScreen = isFullScreen,
modifier = if (isFullScreen) {
Modifier.padding(horizontal = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
} else {
Modifier.padding(end = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
}
)
}
...
- Thêm giá trị
isFullScreen
làm tham số cho thành phần kết hợpReplyEmailDetailsCard
.
ReplyDetailsScreen.kt
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
email: Email,
mailboxType: MailboxType,
modifier: Modifier = Modifier,
isFullScreen: Boolean = false
) {
...
- Bên trong thành phần kết hợp
ReplyEmailDetailsCard
, hãy chỉ cho thấy văn bản tiêu đề của email khi ứng dụng không ở chế độ toàn màn hình, vì bố cục toàn màn hình vốn đã hiển thị chủ đề email dưới dạng tiêu đề. Nếu ở chế độ toàn màn hình, hãy thêm một dấu cách có chiều caoR.dimen.detail_content_padding_top
.
ReplyDetailsScreen.kt
...
Column(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.detail_card_inner_padding))
) {
DetailsScreenHeader(
email,
Modifier.fillMaxWidth()
)
if (isFullScreen) {
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.detail_content_padding_top)))
} else {
Text(
text = stringResource(email.subject),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.padding(
top = dimensionResource(R.dimen.detail_content_padding_top),
bottom = dimensionResource(R.dimen.detail_expanded_subject_body_spacing)
),
)
}
Text(
text = stringResource(email.body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
DetailsScreenButtonBar(mailboxType, displayToast)
}
...
- Trong
ReplyHomeScreen.kt
, bên trong thành phần kết hợpReplyHomeScreen
, hãy truyền giá trịtrue
cho tham sốisFullScreen
khi tạo thành phần kết hợpReplyDetailsScreen
dưới dạng độc lập.
ReplyHomeScreen.kt
...
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
isFullScreen = true,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
...
- Chạy ứng dụng ở chế độ máy tính bảng và xem bố cục dưới đây:
Điều chỉnh nút quay lại cho khung hiển thị danh sách-chi tiết
Với màn hình mở rộng, bạn không cần phải điều hướng đến ReplyDetailsScreen
. Thay vào đó, bạn muốn ứng dụng đóng khi người dùng chọn nút quay lại. Theo đó, chúng ta nên điều chỉnh trình xử lý quay lại.
Hãy sửa đổi trình xử lý quay lại bằng cách truyền hàm activity.finish()
làm tham số onBackPressed
của thành phần kết hợp ReplyDetailsScreen
bên trong thành phần kết hợp ReplyListAndDetailContent
.
ReplyHomeContent.kt
...
import android.app.Activity
import androidx.compose.ui.platform.LocalContext
...
val activity = LocalContext.current as Activity
ReplyDetailsScreen(
replyUiState = replyUiState,
modifier = Modifier.weight(1f),
onBackPressed = { activity.finish() }
)
...
4. Xác minh nhiều kích thước màn hình
Nguyên tắc về chất lượng đối với ứng dụng trên màn hình lớn
Để tạo ra trải nghiệm phù hợp và nhất quán cho người dùng Android, điều quan trọng là bạn phải xây dựng và kiểm thử ứng dụng theo tiêu chuẩn chất lượng tốt. Bạn có thể tham khảo Nguyên tắc về chất lượng cốt lõi của ứng dụng để xác định cách cải thiện chất lượng ứng dụng của mình.
Để tạo ra ứng dụng có chất lượng tốt phù hợp với mọi hệ số hình dạng, hãy xem Nguyên tắc về chất lượng đối với ứng dụng trên màn hình lớn. Ứng dụng của bạn cũng phải đáp ứng các yêu cầu với tiêu chuẩn Tier 3 – Sẵn sàng để sử dụng cho màn hình lớn.
Kiểm thử ứng dụng theo cách thủ công để đảm bảo khả năng đáp ứng màn hình lớn
Nguyên tắc về chất lượng của ứng dụng đưa ra các đề xuất và quy trình kiểm thử thiết bị để kiểm tra chất lượng ứng dụng. Hãy xem ví dụ kiểm thử liên quan đến ứng dụng Reply (Trả lời).
Nguyên tắc nêu trên về chất lượng của ứng dụng đòi hỏi ứng dụng phải giữ lại hoặc khôi phục trạng thái sau khi thay đổi cấu hình. Nguyên tắc này cũng đưa ra hướng dẫn về cách kiểm thử ứng dụng, như minh hoạ trong hình sau đây:
Để kiểm thử tính liên tục của cấu hình trong ứng dụng Reply (Trả lời) theo cách thủ công, hãy hoàn thành các bước sau:
- Chạy ứng dụng Reply (Trả lời) trên một thiết bị có kích thước màn hình trung bình, hoặc nếu bạn đang sử dụng trình mô phỏng có thể thay đổi kích thước thì hãy dùng chế độ đang mở trên thiết bị mô phỏng có thể gập lại.
- Nhớ đặt tuỳ chọn Auto rotate (Tự động xoay) trên trình mô phỏng thành on (bật).
- Cuộn danh sách email xuống.
- Nhấp vào một thẻ email. Ví dụ: mở email của Ali.
- Xoay thiết bị để kiểm tra xem email đã chọn có còn nhất quán với email được chọn ở hướng dọc hay không. Trong ví dụ dưới đây, màn hình vẫn hiện một email của Ali.
- Xoay lại theo hướng dọc để kiểm tra xem ứng dụng có còn hiển thị email đó hay không.
5. Thêm kiểm thử tự động cho ứng dụng thích ứng
Định cấu hình kiểm thử cho màn hình có kích thước thu gọn
Trong lớp học lập trình Kiểm thử ứng dụng Cupcake, bạn đã tìm hiểu cách tạo các bài kiểm thử giao diện người dùng. Bây giờ, hãy tìm hiểu cách tạo các bài kiểm thử cụ thể cho nhiều kích thước màn hình.
Trong ứng dụng Reply (Trả lời), bạn dùng các thành phần điều hướng riêng tuỳ theo kích thước màn hình. Ví dụ: bạn sẽ thấy một ngăn điều hướng cố định khi người dùng nhìn thấy màn hình mở rộng. Bạn nên tạo các bài kiểm thử để xác minh sự tồn tại của nhiều thành phần điều hướng, chẳng hạn như thanh điều hướng dưới cùng, dải điều hướng và ngăn điều hướng phù hợp với nhiều kích thước màn hình.
Để tạo kiểm thử nhằm xác minh xem thành phần điều hướng dưới cùng có trong màn hình thu gọn hay không, hãy hoàn tất các bước sau đây:
- Trong thư mục kiểm thử này, hãy tạo một lớp Kotlin mới có tên là
ReplyAppTest.kt
. - Trong lớp
ReplyAppTest
, hãy tạo một quy tắc kiểm thử bằng cách dùngcreateAndroidComposeRule
và truyềnComponentActivity
dưới dạng loại tham số.ComponentActivity
được dùng để truy cập vào một hoạt động trống thay vìMainActivity
.
ReplyAppTest.kt
...
class ReplyAppTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
...
Để phân biệt các thành phần điều hướng trong màn hình, hãy thêm testTag
vào thành phần kết hợp ReplyBottomNavigationBar
.
- Xác định tài nguyên chuỗi cho thanh điều hướng dưới cùng.
strings.xml
...
<resources>
...
<string name="navigation_bottom">Navigation Bottom</string>
...
</resources>
- Thêm tên chuỗi làm đối số
testTag
cho phương thứctestTag
củaModifier
trong thành phần kết hợpReplyBottomNavigationBar
.
ReplyHomeScreen.kt
...
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
...
modifier = Modifier
.fillMaxWidth()
.testTag(bottomNavigationContentDescription)
)
...
- Trong lớp
ReplyAppTest
, hãy tạo một hàm kiểm thử để kiểm thử màn hình có kích thước thu gọn. Thiết lập nội dung củacomposeTestRule
bằng thành phần kết hợpReplyApp
rồi truyềnWindowWidthSizeClass.Compact
làm đối sốwindowSize
.
ReplyAppTest.kt
...
@Test
fun compactDevice_verifyUsingBottomNavigation() {
// Set up compact window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact
)
}
}
- Dùng thẻ kiểm thử để xác nhận rằng phần tử điều hướng dưới cùng có tồn tại. Gọi hàm mở rộng
onNodeWithTagForStringId
trêncomposeTestRule
rồi truyền chuỗi điều hướng dưới cùng, sau đó gọi phương thứcassertExists()
.
ReplyAppTest.kt
...
@Test
fun compactDevice_verifyUsingBottomNavigation() {
// Set up compact window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact
)
}
// Bottom navigation is displayed
composeTestRule.onNodeWithTagForStringId(
R.string.navigation_bottom
).assertExists()
}
- Chạy kiểm thử và xác minh kiểm thử đó đạt.
Định cấu hình kiểm thử cho các kích thước màn hình trung bình và mở rộng
Giờ đây, khi đã tạo thành công một bài kiểm thử cho màn hình thu gọn, hãy cùng tạo bài kiểm thử tương ứng cho các màn hình trung bình và màn hình mở rộng.
Để tạo các bài kiểm thử nhằm xác minh một dải điều hướng và ngăn điều hướng cố định có trong màn hình trung bình và màn hình mở rộng hay không, hãy hoàn thành các bước sau:
- Xác định tài nguyên chuỗi cho Dải điều hướng để dùng làm thẻ kiểm thử sau này.
strings.xml
...
<resources>
...
<string name="navigation_rail">Navigation Rail</string>
...
</resources>
- Truyền chuỗi dưới dạng thẻ kiểm thử thông qua
Modifier
trong thành phần kết hợpPermanentNavigationDrawer
.
ReplyHomeScreen.kt
...
val navigationDrawerContentDescription = stringResource(R.string.navigation_drawer)
PermanentNavigationDrawer(
...
modifier = Modifier.testTag(navigationDrawerContentDescription)
)
...
- Truyền chuỗi dưới dạng thẻ kiểm thử thông qua
Modifier
trong thành phần kết hợpReplyNavigationRail
.
ReplyHomeScreen.kt
...
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
...
modifier = Modifier
.testTag(navigationRailContentDescription)
)
...
- Thêm kiểm thử để xác minh là một thành phần dải điều hướng có trong màn hình trung bình.
ReplyAppTest.kt
...
@Test
fun mediumDevice_verifyUsingNavigationRail() {
// Set up medium window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Medium
)
}
// Navigation rail is displayed
composeTestRule.onNodeWithTagForStringId(
R.string.navigation_rail
).assertExists()
}
- Thêm kiểm thử để xác minh là một thành phần ngăn điều hướng có trong màn hình mở rộng.
ReplyAppTest.kt
...
@Test
fun expandedDevice_verifyUsingNavigationDrawer() {
// Set up expanded window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Expanded
)
}
// Navigation drawer is displayed
composeTestRule.onNodeWithTagForStringId(
R.string.navigation_drawer
).assertExists()
}
- Hãy sử dụng trình mô phỏng máy tính bảng hoặc trình mô phỏng có thể thay đổi kích thước trong chế độ Máy tính bảng để chạy kiểm thử.
- Chạy mọi kiểm thử và xác minh các bài kiểm thử đó đạt.
Kiểm thử thay đổi về cấu hình trong màn hình thu gọn
Thay đổi cấu hình là trường hợp thường xảy ra trong vòng đời của ứng dụng. Ví dụ: tình trạng thay đổi cấu hình xảy ra khi bạn thay đổi hướng từ dọc sang ngang. Khi thay đổi cấu hình, điều quan trọng là bạn phải kiểm thử để đảm bảo là ứng dụng vẫn giữ nguyên trạng thái hoạt động. Tiếp theo, bạn sẽ tạo các bài kiểm thử mô phỏng sự thay đổi về cấu hình để kiểm tra xem ứng dụng có giữ lại trạng thái trên một màn hình nhỏ hơn hay không.
Cách kiểm thử thay đổi về cấu hình trong màn hình thu gọn:
- Trong thư mục kiểm thử này, hãy tạo một lớp Kotlin mới có tên là
ReplyAppStateRestorationTest.kt
. - Trong lớp
ReplyAppStateRestorationTest
, hãy tạo một quy tắc kiểm thử bằng cách dùngcreateAndroidComposeRule
và truyềnComponentActivity
dưới dạng tham số loại.
ReplyAppStateRestorationTest.kt
...
class ReplyAppStateRestorationTest {
/**
* Note: To access to an empty activity, the code uses ComponentActivity instead of
* MainActivity.
*/
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
}
...
- Tạo một hàm kiểm thử để xác minh là email vẫn được chọn trong màn hình thu gọn sau khi thay đổi cấu hình.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
}
...
Để kiểm thử một thay đổi về cấu hình, bạn cần sử dụng StateRestorationTester
.
- Thiết lập
stateRestorationTester
bằng cách truyềncomposeTestRule
làm đối số choStateRestorationTester
. - Sử dụng
setContent()
với thành phần kết hợpReplyApp
rồi truyềnWindowWidthSizeClass.Compact
dưới dạng đối sốwindowSize
.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
}
...
- Xác minh rằng email thứ ba xuất hiện trong ứng dụng. Dùng phương thức
assertIsDisplayed()
trêncomposeTestRule
để tìm văn bản trong email thứ ba.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
}
...
- Chuyển đến màn hình chi tiết của email bằng cách nhấp vào tiêu đề email. Hãy sử dụng phương thức
performClick()
để điều hướng.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
// Open detailed page
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
}
...
- Xác minh rằng email thứ ba xuất hiện trên màn hình chi tiết. Kiểm tra xem có nút quay lại hay không để xác nhận rằng ứng dụng đang ở màn hình chi tiết, đồng thời xác minh rằng văn bản trong email thứ ba đã xuất hiện.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
...
// Open detailed page
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
// Verify that it shows the detailed screen for the correct email
composeTestRule.onNodeWithContentDescriptionForStringId(
R.string.navigation_back
).assertExists()
composeTestRule.onNodeWithText(
}
...
- Mô phỏng một thay đổi về cấu hình bằng
stateRestorationTester.emulateSavedInstanceStateRestore()
.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
...
// Verify that it shows the detailed screen for the correct email
composeTestRule.onNodeWithContentDescriptionForStringId(
R.string.navigation_back
).assertExists()
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertExists()
// Simulate a config change
stateRestorationTester.emulateSavedInstanceStateRestore()
}
...
- Xác minh lại rằng email thứ ba xuất hiện trên màn hình chi tiết. Kiểm tra xem có nút quay lại hay không để xác nhận rằng ứng dụng đang ở màn hình chi tiết, đồng thời xác minh rằng văn bản trong email thứ ba đã xuất hiện.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
// Open detailed page
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
// Verify that it shows the detailed screen for the correct email
composeTestRule.onNodeWithContentDescriptionForStringId(
R.string.navigation_back
).assertExists()
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertExists()
// Simulate a config change
stateRestorationTester.emulateSavedInstanceStateRestore()
// Verify that it still shows the detailed screen for the same email
composeTestRule.onNodeWithContentDescriptionForStringId(
R.string.navigation_back
).assertExists()
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertExists()
}
...
- Chạy kiểm thử bằng trình mô phỏng điện thoại hoặc trình mô phỏng có thể thay đổi kích thước ở chế độ Điện thoại.
- Xác minh kiểm thử đã đạt.
Kiểm thử thay đổi về cấu hình trong màn hình mở rộng
Để kiểm thử thay đổi về cấu hình trong màn hình mở rộng bằng cách mô phỏng một thay đổi về cấu hình và truyền WindowWidthSizeClass phù hợp, hãy hoàn tất các bước sau đây:
- Tạo một hàm kiểm thử để xác minh là email vẫn được chọn trong màn hình chi tiết sau khi thay đổi cấu hình.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
}
...
Để kiểm thử một thay đổi về cấu hình, bạn cần sử dụng StateRestorationTester
.
- Thiết lập
stateRestorationTester
bằng cách truyềncomposeTestRule
làm đối số choStateRestorationTester
. - Sử dụng
setContent()
với thành phần kết hợpReplyApp
rồi truyềnWindowWidthSizeClass.Expanded
dưới dạng đối sốwindowSize
.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
}
...
- Xác minh rằng email thứ ba xuất hiện trong ứng dụng. Dùng phương thức
assertIsDisplayed()
trêncomposeTestRule
để tìm văn bản trong email thứ ba.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
}
...
- Chọn email thứ ba trên màn hình chi tiết. Sử dụng phương thức
performClick()
để chọn email đó.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
// Select third email
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
...
}
...
- Xác minh rằng màn hình chi tiết cho thấy email thứ ba bằng cách dùng
testTag
trên màn hình chi tiết và tìm văn bản của email trên màn hình con. Cách này giúp đảm bảo bạn có thể tìm thấy văn bản trong phần thông tin chi tiết chứ không phải trong danh sách email.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...
// Select third email
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
// Verify that third email is displayed on the details screen
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
.assertAny(hasAnyDescendant(hasText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
)
...
}
...
- Mô phỏng một thay đổi về cấu hình bằng
stateRestorationTester.emulateSavedInstanceStateRestore()
.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...
// Verify that third email is displayed on the details screen
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
.assertAny(hasAnyDescendant(hasText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
)
// Simulate a config change
stateRestorationTester.emulateSavedInstanceStateRestore()
...
}
...
- Xác minh lại rằng email thứ ba xuất hiện trên màn hình chi tiết sau khi thay đổi cấu hình.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
// Given third email is displayed
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
).assertIsDisplayed()
// Select third email
composeTestRule.onNodeWithText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
).performClick()
// Verify that third email is displayed on the details screen
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
.assertAny(hasAnyDescendant(hasText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
)
// Simulate a config change
stateRestorationTester.emulateSavedInstanceStateRestore()
// Verify that third email is still displayed on the details screen
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
.assertAny(hasAnyDescendant(hasText(
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
)
}
...
- Chạy kiểm thử bằng trình mô phỏng máy tính bảng hoặc trình mô phỏng có thể thay đổi kích thước ở chế độ Máy tính bảng.
- Xác minh kiểm thử đã đạt.
Sử dụng chú thích để nhóm các bài kiểm thử cho các kích thước màn hình khác nhau
Từ các bài kiểm thử trước đó, bạn có thể nhận thấy một số lượt kiểm thử không đạt khi chạy trên các thiết bị có kích thước màn hình không tương thích. Tuy bạn có thể chạy từng lượt kiểm thử một bằng cách sử dụng một thiết bị thích hợp, nhưng có thể phương pháp này không điều chỉnh được theo tỷ lệ phù hợp với nhiều trường hợp kiểm thử.
Để giải quyết vấn đề này, bạn có thể tạo các chú thích để biểu thị kích thước màn hình mà bài kiểm thử có thể chạy và định cấu hình bài kiểm thử được chú thích cho các thiết bị thích hợp.
Để chạy kiểm thử dựa trên kích thước màn hình, hãy hoàn tất các bước sau:
- Trong thư mục kiểm thử này, hãy tạo
TestAnnotations.kt
chứa 3 lớp chú thích làTestCompactWidth
,TestMediumWidth
,TestExpandedWidth
.
TestAnnotations.kt
...
annotation class TestCompactWidth
annotation class TestMediumWidth
annotation class TestExpandedWidth
...
- Sử dụng chú thích trong hàm kiểm thử cho màn hình thu gọn bằng cách đặt chú thích
TestCompactWidth
sau chú thích kiểm thử cho màn hình thu gọn trongReplyAppTest
vàReplyAppStateRestorationTest
.
ReplyAppTest.kt
...
@Test
@TestCompactWidth
fun compactDevice_verifyUsingBottomNavigation() {
...
ReplyAppStateRestorationTest.kt
...
@Test
@TestCompactWidth
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
...
- Dùng chú thích trong hàm kiểm thử cho màn hình trung bình bằng cách đặt chú thích
TestMediumWidth
sau chú thích kiểm thử cho màn hình trung bình trongReplyAppTest
.
ReplyAppTest.kt
...
@Test
@TestMediumWidth
fun mediumDevice_verifyUsingNavigationRail() {
...
- Sử dụng chú thích trong hàm kiểm thử cho màn hình mở rộng bằng cách đặt chú thích
TestExpandedWidth
sau chú thích kiểm thử cho màn hình mở rộng trongReplyAppTest
vàReplyAppStateRestorationTest
.
ReplyAppTest.kt
...
@Test
@TestExpandedWidth
fun expandedDevice_verifyUsingNavigationDrawer() {
...
ReplyAppStateRestorationTest.kt
...
@Test
@TestExpandedWidth
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...
Để đảm bảo thành công, hãy định cấu hình lần kiểm thử để chỉ chạy các kiểm thử có chú giải TestCompactWidth
.
- Trong Android Studio, hãy chọn Run > Edit Configurations… (Chạy > Chỉnh sửa cấu hình…)
- Đổi tên kiểm thử thành Compact Test (Kiểm thử màn hình thu gọn) rồi chọn chạy kiểm thử All in Package (Tất cả trong gói).
- Nhấp vào biểu tượng dấu ba chấm (...) phía bên phải trường Instrumentation arguments (Đối số đo lường).
- Nhấp vào nút dấu cộng (
+
) và thêm các tham số bổ sung: annotation (chú giải) có giá trị com.example.reply.test.TestCompactWidth.
- Chạy các kiểm thử bằng một trình mô phỏng màn hình thu gọn.
- Kiểm tra để đảm bảo là chỉ chạy các kiểm thử màn hình thu gọn.
- Lặp lại các bước cho màn hình trung bình và màn hình mở rộng.
6. Lấy mã giải pháp
Để tải mã cho lớp học lập trình đã kết thúc, hãy sử dụng lệnh git sau:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git
Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip, giải nén và mở tệp trong Android Studio.
Nếu bạn muốn xem mã giải pháp, hãy xem mã đó trên GitHub.
7. Kết luận
Xin chúc mừng! Bạn đã làm cho ứng dụng Reply (Trả lời) thích ứng với mọi kích thước màn hình bằng cách triển khai bố cục thích ứng. Bạn cũng đã học cách tăng tốc độ phát triển bằng cách xem trước và duy trì chất lượng ứng dụng bằng nhiều phương pháp kiểm thử.
Đừng quên chia sẻ thành quả của mình trên mạng xã hội với #AndroidBasics!