Tạo ứng dụng bằng bố cục thích ứng

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:

  1. 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ích Preview rồi chỉ định giá trị WindowWidthSizeClass.Medium làm tham số cho thành phần kết hợp ReplyApp.

MainActivity.kt

...
@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppMediumPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(windowSize = WindowWidthSizeClass.Medium)
        }
    }
}
...
  1. 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ích Preview rồi chỉ định giá trị WindowWidthSizeClass.Expanded làm tham số cho thành phần kết hợp ReplyApp.

MainActivity.kt

...
@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppExpandedPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(windowSize = WindowWidthSizeClass.Expanded)
        }
    }
}
...
  1. Tạo bản xem trước để xem những nội dung sau:

5577b1d0fe306e33.png

f624e771b76bbc2.png

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ó.

56cfa13ef31d0b59.png

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:

  1. Để thể hiện nhiều loại bố cục nội dung, trên WindowStateUtils.kt, hãy tạo một lớp Enum 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ùng LIST_ONLY.

WindowStateUtils.kt

...
enum class ReplyContentType {
    LIST_ONLY, LIST_AND_DETAIL
}
...
  1. Khai báo biến contentType trên ReplyApp.kt rồi chỉ định contentType 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.

  1. Trong ReplyHomeScreen.kt, hãy thêm contentType làm tham số vào thành phần kết hợp ReplyHomeScreen.

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
) {
...
  1. Truyền giá trị contentType đến thành phần kết hợp ReplyHomeScreen.

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
    )

...
  1. Thêm contentType làm tham số cho thành phần kết hợp ReplyAppContent.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    contentType: ReplyContentType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
...
  1. Truyền giá trị contentType đến hai thành phần kết hợp ReplyAppContent.

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 contentTypeLIST_AND_DETAIL hoặc hiện danh sách chỉ có nội dung email khi contentTypeLIST_ONLY.

  1. Trong ReplyHomeScreen.kt, hãy thêm câu lệnh if/else trên thành phần kết hợp ReplyAppContent để hiện thành phần kết hợp ReplyListAndDetailContent khi giá trị contentTypeLIST_AND_DETAIL, cũng như hiện thành phần kết hợp ReplyListOnlyContent trên nhánh else.

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
                )
            }
        }
...
  1. 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))
                    )
                }
            }
        ) {

...
  1. Chạy ứng dụng ở chế độ máy tính bảng để xem màn hình dưới đây:

fe811a212feefea5.png

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.

e7c540e41fe1c3d.png

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:

  1. Trong ReplyDetailsScreen.kt, hãy thêm một biến isFullScreen làm tham số Boolean cho thành phần kết hợp ReplyDetailsScreen.

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
) {
...
  1. Bên trong thành phần kết hợp ReplyDetailsScreen, hãy gói thành phần kết hợp ReplyDetailsScreenTopBar bằng một câu lệnh if để 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.

  1. Truyền giá trị isFullScreen đến thành phần kết hợp ReplyEmailDetailsCard. 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))
                }
            )
        }
...
  1. Thêm giá trị isFullScreen làm tham số cho thành phần kết hợp ReplyEmailDetailsCard.

ReplyDetailsScreen.kt

...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
    email: Email,
    mailboxType: MailboxType,
    modifier: Modifier = Modifier,
    isFullScreen: Boolean = false
) {
...
  1. 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 cao R.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)
}

...
  1. Trong ReplyHomeScreen.kt, bên trong thành phần kết hợp ReplyHomeScreen, hãy truyền giá trị true cho tham số isFullScreen khi tạo thành phần kết hợp ReplyDetailsScreen dưới dạng độc lập.

ReplyHomeScreen.kt

...
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                isFullScreen = true,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
...
  1. Chạy ứng dụng ở chế độ máy tính bảng và xem bố cục dưới đây:

833b3986a71a0b67.png

Đ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).

Nội dung mô tả chất lượng của ứng dụng liên quan đến cấu hình và tính liên tục trên màn hình lớn.

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:

Các bước kiểm thử chất lượng của ứng dụng liên quan đến cấu hình và tính liên tục trên màn hình lớn.

Để 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:

  1. 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.
  2. Nhớ đặt tuỳ chọn Auto rotate (Tự động xoay) trên trình mô phỏng thành on (bật).

5a1c3a4cb4fc0192.png

  1. Cuộn danh sách email xuống.

7ce0887b5b38a1f0.png

  1. Nhấp vào một thẻ email. Ví dụ: mở email của Ali.

16d7ca9c17206bf8.png

  1. 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.

d078601f2cc50341.png

  1. 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.

16d7ca9c17206bf8.png

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:

  1. 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.
  2. Trong lớp ReplyAppTest, hãy tạo một quy tắc kiểm thử bằng cách dùng createAndroidComposeRule và truyền ComponentActivity 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.

  1. 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>
  1. Thêm tên chuỗi làm đối số testTag cho phương thức testTag của Modifier trong thành phần kết hợp ReplyBottomNavigationBar.

ReplyHomeScreen.kt

...
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
    ...
    modifier = Modifier
        .fillMaxWidth()
        .testTag(bottomNavigationContentDescription)
)
...
  1. 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ủa composeTestRule bằng thành phần kết hợp ReplyApp rồi truyền WindowWidthSizeClass.Compact làm đối số windowSize.

ReplyAppTest.kt

...
    @Test
    fun compactDevice_verifyUsingBottomNavigation() {
        // Set up compact window
        composeTestRule.setContent {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact
            )
        }
    }
  1. 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ên composeTestRule rồi truyền chuỗi điều hướng dưới cùng, sau đó gọi phương thức assertExists().

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()
    }
  1. 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:

  1. 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>
  1. Truyền chuỗi dưới dạng thẻ kiểm thử thông qua Modifier trong thành phần kết hợp PermanentNavigationDrawer.

ReplyHomeScreen.kt

...
    val navigationDrawerContentDescription = stringResource(R.string.navigation_drawer)
        PermanentNavigationDrawer(
...
modifier = Modifier.testTag(navigationDrawerContentDescription)
)
...
  1. Truyền chuỗi dưới dạng thẻ kiểm thử thông qua Modifier trong thành phần kết hợp ReplyNavigationRail.

ReplyHomeScreen.kt

...
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
    ...
    modifier = Modifier
        .testTag(navigationRailContentDescription)
)
...
  1. 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()
}
  1. 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()
}
  1. 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ử.
  2. 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:

  1. 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.
  2. Trong lớp ReplyAppStateRestorationTest, hãy tạo một quy tắc kiểm thử bằng cách dùng createAndroidComposeRule và truyền ComponentActivity 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>()
}
...
  1. 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.

  1. Thiết lập stateRestorationTester bằng cách truyền composeTestRule làm đối số cho StateRestorationTester.
  2. Sử dụng setContent() với thành phần kết hợp ReplyApp rồi truyền WindowWidthSizeClass.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) }

}
...
  1. Xác minh rằng email thứ ba xuất hiện trong ứng dụng. Dùng phương thức assertIsDisplayed() trên composeTestRule để 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()
}
...
  1. 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()
}
...
  1. 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(
}
...
  1. 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()
}
...
  1. 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()
}

...
  1. 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.
  2. 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:

  1. 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.

  1. Thiết lập stateRestorationTester bằng cách truyền composeTestRule làm đối số cho StateRestorationTester.
  2. Sử dụng setContent() với thành phần kết hợp ReplyApp rồi truyền WindowWidthSizeClass.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) }
}
...
  1. Xác minh rằng email thứ ba xuất hiện trong ứng dụng. Dùng phương thức assertIsDisplayed() trên composeTestRule để 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()
}
...
  1. 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()
    ...
}

...
  1. 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)))
        )
...
}

...
  1. 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()
    ...
}
...
  1. 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)))
        )
}
...
  1. 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.
  2. 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:

  1. 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
...
  1. 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 trong ReplyAppTestReplyAppStateRestorationTest.

ReplyAppTest.kt

...
    @Test
    @TestCompactWidth
    fun compactDevice_verifyUsingBottomNavigation() {
...

ReplyAppStateRestorationTest.kt

...
    @Test
    @TestCompactWidth
    fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {

...
  1. 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 trong ReplyAppTest.

ReplyAppTest.kt

...
    @Test
    @TestMediumWidth
    fun mediumDevice_verifyUsingNavigationRail() {
...
  1. 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 trong ReplyAppTestReplyAppStateRestorationTest.

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.

  1. Trong Android Studio, hãy chọn Run > Edit Configurations… (Chạy > Chỉnh sửa cấu hình…) 7be537f5faa1a61a.png
  2. Đổ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).

f70b74bc2e6674f1.png

  1. 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).
  2. 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.

cf1ef9b80a1df8aa.png

  1. Chạy các kiểm thử bằng một trình mô phỏng màn hình thu gọn.
  2. Kiểm tra để đảm bảo là chỉ chạy các kiểm thử màn hình thu gọn.

204ed40031f8615a.png

  1. 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!

Tìm hiểu thêm