Tạo ứng dụng thích ứng bằng tính năng điều hướng động

1. Giới thiệu

Một trong những lợi thế tuyệt vời của việc phát triển ứng dụng trên nền tảng Android là cơ hội lớn để tiếp cận người dùng dưới nhiều hình thức, chẳng hạn như thiết bị đeo, thiết bị có thể gập lại, máy tính bảng, máy tính và thậm chí cả TV. Khi sử dụng một ứng dụng, người dùng của bạn có thể muốn dùng cùng một ứng dụng đó trên các thiết bị có màn hình lớn để tận dụng không gian tăng thêm. Người dùng Android ngày càng sử dụng ứng dụng trên nhiều thiết bị có kích thước màn hình khác nhau và mong đợi trải nghiệm người dùng chất lượng cao trên tất cả các thiết bị.

Tới nay, bạn đã học được cách tạo ứng dụng chủ yếu cho thiết bị di động. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách chuyển đổi ứng dụng để thích ứng với các kích thước màn hình khác. Bạn sẽ sử dụng các mẫu bố cục điều hướng thích ứng đẹp mắt và dùng được cho cả thiết bị di động lẫn thiết bị có màn hình lớn, chẳng hạn như thiết bị có thể gập lại, máy tính bảng và máy tính.

Điều kiện tiên quyết

  • 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ách sử dụng lớp ViewModel
  • Làm quen với cách tạo hàm Composables
  • Trải nghiệm bố cục xây dựng 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

Kiến thức bạn sẽ học được

  • Cách tạo điều hướng giữa các màn hình mà không cần Biểu đồ điều hướng cho các ứng dụng đơn giản
  • Cách dùng Jetpack Compose để tạo bố cục điều hướng thích ứng
  • Cách tạo trình xử lý quay lại tuỳ chỉnh

Sản phẩm bạn sẽ tạo ra

  • Bạn sẽ triển khai tính năng điều hướng động trong ứng dụng Reply (Trả lời) hiện có để làm cho bố cục của ứng dụng thích ứng với mọi kích thước màn hình

Sản phẩm hoàn thiện sẽ giống như hình dưới đây:

​​ Hình minh hoạ ứng dụng Reply (Trả lời) ở cuối lớp học lập trình này với ngăn điều hướng hiển thị ở bên trái. Ngăn điều hướng liệt kê 4 thẻ mà người dùng có thể chọn là "Hộp thư đến", "Đã gửi", "Thư nháp" và "Thư rác". Một danh sách email mẫu sẽ hiển thị phía bên phải ngăn điều hướng.

Những gì bạn cần

  • 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

2. Tổng quan về ứng dụng

Giới thiệu về ứng dụng Reply (Trả lời)

Reply là một ứng dụng nhiều màn hình giống với một chương trình email khách (ứng dụng khách dùng để gởi/quản lý email).

Ứng dụng Reply (Trả lời) sẽ hiển thị ở chế độ điện thoại. Chế độ này hiển thị một danh sách các email mẫu để người dùng đọc. Ở cuối màn hình là 4 biểu tượng đại diện cho hộp thư đến, đã gửi, thư nháp và thư rác.

Ứng dụng chứa 4 danh mục. Những danh mục này sẽ hiển thị theo các thẻ khác nhau, cụ thể là: hộp thư đến, thư đã gửi, thư nháp và thư rác.

Tải mã khởi động xuống

Trong Android Studio, hãy mở thư mục basic-android-kotlin-compose-training-reply-app.

  1. Chuyển đến trang kho lưu trữ GitHub được cung cấp cho dự án.
  2. Xác minh rằng tên chi nhánh khớp với tên chi nhánh được chỉ định trong lớp học lập trình. Ví dụ: trong ảnh chụp màn hình sau đây, tên nhánh là main (chính).

1e4c0d2c081a8fd2.png

  1. Trên trang GitHub cho dự án này, hãy nhấp vào nút Code (Mã nguồn), một cửa sổ bật lên sẽ hiện ra.

1debcf330fd04c7b.png

  1. Trong cửa sổ bật lên, nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
  2. Xác định vị trí của tệp trên máy tính (có thể trong thư mục Tải xuống (Tệp đã tải xuống)).
  3. Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.

Mở dự án trong Android Studio

  1. Khởi động Android Studio.
  2. Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open (Mở).

d8e9dbdeafe9038a.png

Lưu ý: Nếu Android Studio đã mở sẵn thì hãy chuyển sang chọn tuỳ chọn File (Tệp) > Open (Mở) trên trình đơn.

8d1fda7396afe8e5.png

  1. Trong trình duyệt tệp, hãy chuyển đến vị trí của thư mục dự án chưa giải nén (có thể nằm trong thư mục Downloads (Tệp đã tải xuống)).
  2. Nhấp đúp vào thư mục dự án đó.
  3. Chờ Android Studio mở dự án.
  4. Nhấp vào nút Run (Chạy) 8de56cba7583251f.png để tạo và chạy ứng dụng. Đảm bảo rằng ứng dụng được xây dựng như dự kiến.

3. Hướng dẫn từng bước về mã khởi động

Các thư mục quan trọng trong ứng dụng Reply (Trả lời)

Thư mục tệp Ứng dụng Reply (Trả lời) hiển thị hai thư mục con được mở rộng là "dữ liệu" và "giao diện người dùng". Trong thư mục giao diện người dùng, MainActivity.kt được chọn. MainActivity.kt xuất hiện ở cuối danh sách nội dung.

Lớp dữ liệu và giao diện người dùng của dự án ứng dụng Reply (Trả lời) được phân tách thành các thư mục khác nhau. ReplyViewModel, ReplyUiState và các thành phần kết hợp khác nằm trong thư mục ui. Các lớp dataenum xác định lớp dữ liệu và lớp trình cung cấp dữ liệu nằm trong thư mục data.

Khởi tạo dữ liệu trong ứng dụng Reply (Trả lời)

Ứng dụng Reply (Trả lời) được khởi tạo bằng dữ liệu thông qua phương thức initilizeUIState() trong ReplyViewModel, phương thức này được thực thi trong hàm init.

ReplyViewModel.kt

...
   init {
        initializeUIState()
    }

   private fun initializeUIState() {
        var mailboxes: Map<MailboxType, List<Email>> =
            LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
        _uiState.value =
            ReplyUiState(
                mailboxes = mailboxes,
                currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                    ?: LocalEmailsDataProvider.defaultEmail
            )
    }
...

Thành phần kết hợp cấp màn hình

Cũng giống như các ứng dụng khác, ứng dụng Reply (Trả lời) sử dụng thành phần kết hợp ReplyApp làm thành phần kết hợp chính nơi bạn khai báo viewModeluiState. Nhiều hàm viewModel cũng được truyền dưới dạng đối số lambda cho thành phần kết hợp (composable) ReplyHomeScreen.

ReplyApp.kt

...
@Composable
fun ReplyApp(modifier: Modifier = Modifier) {
    val viewModel: ReplyViewModel = viewModel()
    val replyUiState = viewModel.uiState.collectAsState().value

    ReplyHomeScreen(
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )
}

Các thành phần kết hợp khác

  • ReplyHomeScreen.kt: chứa các thành phần kết hợp màn hình dành cho màn hình chính, bao gồm cả phần tử điều hướng.
  • ReplyHomeContent.kt: chứa các thành phần kết hợp xác định chi tiết hơn thành phần kết hợp trên màn hình chính.
  • ReplyDetailsScreen.kt: chứa các thành phần kết hợp màn hình và thành phần kết hợp nhỏ hơn cho màn hình chi tiết.

Vui lòng xem kỹ từng tệp để hiểu rõ hơn về các thành phần kết hợp trước khi chuyển sang phần tiếp theo của lớp học lập trình.

4. Thay đổi màn hình mà không cần biểu đồ điều hướng

Trong lộ trình trước đó, bạn đã học cách sử dụng lớp NavHostController để điều hướng từ màn hình này sang màn hình khác. Với Compose, bạn cũng có thể thay đổi màn hình bằng những câu lệnh có điều kiện đơn giản nhờ sử dụng trạng thái có thể thay đổi trong thời gian chạy. Điều này đặc biệt hữu ích trong các ứng dụng nhỏ như Reply (Trả lời), nơi bạn chỉ muốn chuyển đổi giữa 2 màn hình.

Thay đổi màn hình bằng cách thay đổi trạng thái

Trong Compose, màn hình được kết hợp lại khi có thay đổi về trạng thái. Bạn có thể thay đổi màn hình bằng cách sử dụng các điều kiện đơn giản để phản hồi các thay đổi về trạng thái.

Bạn sẽ sử dụng các điều kiện để hiển thị nội dung trên màn hình chính khi người dùng đang ở màn hình chính và hiển thị nội dung trên màn hình chi tiết khi người dùng không ở màn hình chính.

Sửa đổi ứng dụng Reply (Trả lời) để cho phép màn hình thay đổi khi trạng thái thay đổi bằng cách hoàn thành các bước sau:

  1. Mở mã khởi động trong Android Studio.
  2. Ở thành phần kết hợp ReplyHomeScreen trong ReplyHomeScreen.kt, hãy gói thành phần kết hợp ReplyAppContent bằng câu lệnh if khi thuộc tính isShowingHomepage của đối tượng replyUiStatetrue.

ReplyHomeScreen.kt

@Composable
fun ReplyHomeScreen(
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Int) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {

...
    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
            onTabPressed = onTabPressed,
            onEmailCardPressed = onEmailCardPressed,
            navigationItemContentList = navigationItemContentList,
            modifier = modifier

        )
    }
}

Bây giờ, bạn phải tính đến trường hợp người dùng không ở màn hình chính thông qua việc hiển thị màn hình chi tiết.

  1. Thêm một nhánh else có thành phần kết hợp ReplyDetailsScreen trong phần nội dung của nhánh đó. Thêm replyUIState, onDetailScreenBackPressedmodifier làm đối số cho thành phần kết hợp ReplyDetailsScreen.

ReplyHomeScreen.kt

@Composable
fun ReplyHomeScreen(
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Int) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {

...

    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
            onTabPressed = onTabPressed,
            onEmailCardPressed = onEmailCardPressed,
            navigationItemContentList = navigationItemContentList,
            modifier = modifier

        )
    } else {
        ReplyDetailsScreen(
            replyUiState = replyUiState,
            onBackPressed = onDetailScreenBackPressed,
            modifier = modifier
        )
    }
}

replyUiState là một đối tượng trạng thái. Như vậy, khi có thay đổi về thuộc tính isShowingHomepage của đối tượng replyUiState, thành phần kết hợp ReplyHomeScreen sẽ được kết hợp lại và câu lệnh if/else sẽ được đánh giá lại trong thời gian chạy. Phương pháp này hỗ trợ điều hướng giữa các màn hình mà không cần sử dụng lớp NavHostController.

Hình minh hoạ hoạt ảnh của ứng dụng Reply (Trả lời) trên trình mô phỏng điện thoại cho thấy màn hình thay đổi từ Màn hình chính sang trang chi tiết. Màn hình chính hiển thị một danh sách các email có 4 biểu tượng cho thư ở dưới cùng (hộp thư đến, đã gửi, thư nháp và thư rác). Trang chi tiết hiển thị toàn bộ văn bản của một email mẫu, với các nút "Trả lời" và "Trả lời tất cả" bên dưới.

Tạo trình xử lý quay lại tuỳ chỉnh

Một lợi thế của việc sử dụng thành phần kết hợp NavHost để chuyển đổi giữa các màn hình là hướng của màn hình trước được lưu trong ngăn xếp lui. Những màn hình đã lưu này cho phép nút quay lại của hệ thống dễ dàng chuyển về màn hình trước đó khi được gọi. Vì ứng dụng Reply (Trả lời) không sử dụng NavHost nên bạn phải thêm mã này để xử lý nút quay lại theo cách thủ công. Bạn sẽ thực hiện việc này ở bước tiếp theo.

Hoàn thành các bước sau để tạo một trình xử lý quay lại tuỳ chỉnh trong ứng dụng Reply (Trả lời):

  1. Trên dòng đầu tiên của thành phần kết hợp ReplyDetailsScreen, hãy thêm một thành phần kết hợp BackHandler.
  2. Gọi hàm onBackPressed() trong phần nội dung của thành phần kết hợp BackHandler.

ReplyDetailsScreen.kt

...
import androidx.activity.compose.BackHandler
...
@Composable
fun ReplyDetailsScreen(
    replyUiState: ReplyUiState,
    modifier: Modifier = Modifier,
    onBackPressed: () -> Unit = {},
) {
    BackHandler {
        onBackPressed()
    }
...

5. Chạy ứng dụng trên các thiết bị có màn hình lớn

Kiểm tra ứng dụng bằng trình mô phỏng có thể thay đổi kích thước

Để tạo ra các ứng dụng hữu ích, nhà phát triển cần hiểu được trải nghiệm của người dùng dưới nhiều dạng thức. Do đó, bạn phải kiểm thử ứng dụng trên nhiều hệ số hình dạng ngay từ đầu quá trình phát triển.

Bạn có thể sử dụng nhiều trình mô phỏng ở nhiều kích thước màn hình để đạt được mục tiêu này. Tuy nhiên, việc này có thể rườm rà, nhất là khi bạn đang xây dựng cho nhiều kích thước màn hình cùng lúc. Bạn có thể cũng cần kiểm thử cách một ứng dụng đang phản hồi các thay đổi về kích thước màn hình, chẳng hạn như các thay đổi về hướng, về kích thước cửa sổ trên máy tính và về trạng thái gập trên thiết bị có thể gập lại.

Android Studio sẽ giúp bạn kiểm thử các trường hợp này bằng việc giới thiệu về trình mô phỏng có thể thay đổi kích thước.

Hoàn thành các bước sau để thiết lập trình mô phỏng có thể thay đổi kích thước:

  1. Đảm bảo là bạn đang chạy Android Studio Chipmunk | 2021.2.1 trở lên.
  2. Trong Android Studio, hãy chọn Tools (Công cụ) > Device Manager (Trình quản lý thiết bị).

Trình đơn Công cụ hiển thị một danh sách các tuỳ chọn. Trình quản lý thiết bị xuất hiện ở giữa danh sách được chọn.

  1. Trong Trình quản lý thiết bị, hãy nhấp vào mục Tạo thiết bị. Thanh công cụ của trình quản lý thiết bị hiển thị hai tuỳ chọn trình đơn là "Virtual" (Thiết bị ảo) và "Virtual" (Thiết bị thực). Có một nút Create Device (Tạo thiết bị) bên dưới các tuỳ chọn này.
  2. Chọn danh mục Điện thoại và thiết bị Có thể thay đổi kích thước (Thử nghiệm).
  3. Nhấp vào Next (Tiếp theo).

Cửa sổ Trình quản lý thiết bị hiển thị lời nhắc chọn định nghĩa cho thiết bị. Danh sách các lựa chọn sẽ xuất hiện cùng với một trường tìm kiếm ở phía trên. Danh mục "Điện thoại" được chọn và tên định nghĩa của thiết bị "Có thể thay đổi kích thước (Thử nghiệm)" được chọn.

  1. Chọn API cấp 33.
  2. Nhấp vào Next (Tiếp theo).

Cửa sổ Virtual Device Configuration (Cấu hình thiết bị ảo) sẽ hiển thị lời nhắc chọn một hình ảnh hệ thống. API Tiramisu được chọn.

  1. Đặt tên cho Thiết bị Android ảo mới.
  2. Nhấp vào Finish (Hoàn tất).

Màn hình Cấu hình ảo trong Android Virtural Device (AVD) (Thiết bị ảo Android) sẽ hiển thị. Màn hình cấu hình bao gồm trường văn bản để nhập tên AVD. Dưới trường tên là danh sách các tuỳ chọn thiết bị, bao gồm định nghĩa thiết bị (Thử nghiệm có thể thay đổi kích thước), hình ảnh hệ thống (Tiramisu) và hướng với hướng dọc được chọn theo mặc định. Các nút đọc "Thay đổi" xuất hiện ở bên phải phần định nghĩa thiết bị và thông tin ảnh hệ thống, còn tuỳ chọn Ngang ở bên phải tuỳ chọn hướng Dọc đã chọn. Có 4 nút ở góc dưới bên phải: Cancel (Huỷ), Previous (Trước), Next (Tiếp theo) (nút này có màu xám và không thể chọn được), và Finish (Kết thúc).

Chạy ứng dụng trên trình mô phỏng màn hình lớn

Bạn hiện đã thiết lập trình mô phỏng có thể đổi kích thước, hãy xem ứng dụng trông như thế nào trên màn hình lớn.

  1. Chạy ứng dụng trên trình mô phỏng có thể đổi kích thước.
  2. Chọn Máy tính bảng cho chế độ hiển thị.

Trình mô phỏng có thể đổi kích thước hiển thị ứng dụng Reply (Trả lời) trên màn hình điện thoại. Ứng dụng này hiển thị một danh sách các thư có 4 biểu tượng ở cuối màn hình cho Hộp thư đến, Thư đã gửi, Thư nháp và Thư rác.

  1. Kiểm tra ứng dụng ở chế độ Máy tính bảng khi nằm ngang.

Trình mô phỏng có thể thay đổi kích thước hiển thị ứng dụng Reply (Trả lời) trong màn hình máy tính bảng, với phần thân được kéo dài ra. Các biểu tượng hiển thị ở cuối màn hình cho hộp thư đến, đã gửi, thư nháp và thư rác.

Lưu ý rằng màn hình máy tính bảng được kéo dài theo chiều ngang. Mặc dù hướng này hoạt động đúng chức năng, nhưng nó có thể không phải là hướng sử dụng tốt nhất trong không gian màn hình lớn. Chúng ta sẽ giải quyết vấn đề đó ở bước tiếp theo.

Thiết kế cho màn hình lớn

Cảm nhận đầu tiên của bạn khi xem ứng dụng này trên máy tính bảng là ứng dụng được thiết kế không bắt mắt và kém hấp dẫn. Bạn đã đúng: bố cục này không được thiết kế để sử dụng cho màn hình lớn.

Khi thiết kế bố cục cho màn hình lớn (chẳng hạn như máy tính bảng và thiết bị có thể gập lại), bạn phải cân nhắc đến sự thoải mái và hiệu quả cho người dùng cũng như khoảng cách giữa các ngón tay của người dùng với màn hình. Với thiết bị di động, ngón tay của người dùng có thể dễ dàng di chuyển trên phần lớn màn hình; vị trí của các phần tử tương tác (chẳng hạn như nút và phần tử điều hướng) đều không quan trọng bằng. Tuy nhiên, đối với màn hình lớn, việc thành phần tương tác quan trọng nằm ở giữa màn hình có thể khiến người dùng khó tiếp cận với các thành phần này.

Như bạn thấy trong ứng dụng Reply (Trả lời), việc thiết kế màn hình lớn không chỉ đơn giản là kéo giãn hoặc phóng to các thành phần trên giao diện người dùng để vừa với màn hình. Đây là cơ hội để bạn sử dụng không gian tăng thêm nhằm tạo ra trải nghiệm khác biệt cho người dùng. Ví dụ: bạn có thể thêm một bố cục khác trên cùng một màn hình để tránh việc phải chuyển đến màn hình khác hoặc làm được nhiều việc cùng lúc.

Ứng dụng Reply (Trả lời) hiển thị màn hình chi tiết trên trang chủ cùng với ngăn điều hướng và danh sách email. Một email mẫu hiển thị ở bên phải của danh sách email. Nút Trả lời và Trả lời tất cả sẽ hiển thị bên dưới email mẫu.

Thiết kế này có thể làm tăng hiệu suất của người dùng và thúc đẩy họ tương tác nhiều hơn. Nhưng trước khi triển khai thiết kế này, trước tiên, bạn phải tìm hiểu cách tạo các bố cục tuỳ theo kích thước màn hình.

6. Tạo bố cục thích ứng với các kích thước màn hình khác nhau

Điểm ngắt là gì?

Bạn có thể thắc mắc về cách hiển thị nhiều bố cục cho cùng một ứng dụng. Câu trả lời ngắn gọn là sử dụng nhiều điều kiện ở các trạng thái khác nhau, giống như cách bạn đã làm ở đầu lớp học lập trình này.

Để tạo một ứng dụng thích ứng, bạn cần có bố cục thay đổi dựa trên kích thước màn hình. Điểm đo lường mà bố cục thay đổi được gọi là điểm ngắt. Material Design đã tạo một phạm vi điểm ngắt mở rộng trên hầu hết các màn hình Android.

Bảng hiển thị phạm vi điểm ngắt (tính bằng dp) cho các loại thiết bị và cách thiết lập khác nhau. 0 đến 599 dp dành cho thiết bị cầm tay ở chế độ dọc, điện thoại di động ở chế độ ngang, kích thước cửa sổ nhỏ gọn, 4 cột và 8 lề tối thiểu. 600 đến 839 dp dành cho máy tính bảng nhỏ có thể gập lại ở chế độ dọc hoặc ngang, lớp kích thước cửa sổ trung bình, 12 cột và 12 lề tối thiểu. 840 dp trở lên dành cho loại máy tính bảng lớn ở chế độ dọc hoặc ngang, lớp kích thước cửa sổ mở rộng, 12 cột và lề tối thiểu là 32. Bảng ghi chú cho biết lề cũng như rãnh mang tính linh hoạt và không cần phải có kích thước bằng nhau, đồng thời điện thoại ở chế độ ngang được xem là một trường hợp ngoại lệ khi vẫn phải nằm trong phạm vi điểm ngắt từ 0 đến 599 dp.

Ví dụ: bảng phạm vi điểm ngắt này cho biết rằng nếu ứng dụng của bạn đang chạy trên một thiết bị có kích thước màn hình nhỏ hơn 600 dp, thì bạn nên hiển thị bố cục dành cho thiết bị di động.

Sử dụng các lớp kích thước cửa sổ

API WindowSizeClass được giới thiệu cho Compose giúp việc triển khai các điểm ngắt của Material Design trở nên đơn giản hơn.

Các lớp (class) kích thước cửa sổ giới thiệu 3 danh mục kích thước: Nhỏ gọn, Trung bình và Mở rộng, cho cả chiều rộng và chiều cao.

Biểu đồ thể hiện các lớp kích thước cửa sổ theo chiều rộng. Biểu đồ thể hiện các lớp kích thước cửa sổ theo chiều cao.

Hoàn thành các bước sau để triển khai API WindowSizeClass trong ứng dụng Reply (Trả lời):

  1. Thêm phần phụ thuộc material3-window-size-class vào tệp build.gradle của mô-đun.

build.gradle

...
dependencies {
...
"androidx.compose.material3:material3-window-size-class:$material3_version"
...
  1. Nhấp vào Đồng bộ hoá ngay để đồng bộ hoá gradle sau khi thêm phần phụ thuộc.

Nút Đồng bộ hoá ngay (Sync Now) sẽ hiển thị bên dưới các thẻ để chọn những tệp .kt và .gradle khác nhau. Ở bên phải nút Đồng bộ hoá ngay (Sync Now) là một nút khác có nội dung Bỏ qua các thay đổi này (Ignore these changes).

Với tệp build.grade đã cập nhật, bạn hiện có thể tạo một biến để lưu trữ kích thước của cửa sổ ứng dụng tại một thời điểm bất kỳ.

  1. Ở hàm onCreate() trong tệp MainActivity.kt, hãy chỉ định phương thức calculateWindowSizeClass có ngữ cảnh this được truyền trong tham số cho một biến có tên windowSize.
  2. Nhập gói calculateWindowSizeClass phù hợp.

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

...

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ReplyTheme {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp()

...
  1. Hãy chú ý phần gạch chân màu đỏ dưới cú pháp calculateWindowSizeClass cho thấy biểu tượng bóng đèn màu đỏ. Nhấp vào bóng đèn màu đỏ ở bên trái của biến windowSize rồi chọn Opt in for 'ExperimentalMaterial3WindowSizeClassApi' on 'onCreate' (Chọn sử dụng cho "ExperimentalMaterial3WindowSizeClassApi" trên "onCreate") để tạo chú thích phía trên phương thức onCreate().

Trong mã này, dòng "val windowSize = calculateWindowSizeClass(this)" được chọn, với biểu tượng bóng đèn bên phải hiển thị ở bên trái dòng mã. Trong bóng đèn đã chọn, bạn sẽ thấy một danh sách các tuỳ chọn để xử lý lỗi, trong đó tuỳ chọn "Chọn sử dụng 'ExperimentalMaterial3WindowSizeClassApi' trên 'onCreate' được chọn.

Bạn có thể sử dụng biến WindowWidthSizeClass trong MainActivity.kt để xác định bố cục sẽ hiển thị trong các thành phần kết hợp khác nhau. Hãy chuẩn bị thành phần kết hợp ReplyApp để nhận giá trị này.

  1. Trong tệp ReplyApp.kt, hãy chỉnh sửa thành phần kết hợp ReplyApp để chấp nhận WindowWidthSizeClass dưới dạng tham số và nhập gói phù hợp.

ReplyApp.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
...
  1. Chuyển biến windowSize vào thành phần ReplyApp trong phương thức onCreate() của tệp MainActivity.kt.

MainActivity.kt

...
         setContent {
            ReplyTheme {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp(
                    windowSize = windowSize.widthSizeClass
                )
...

Bạn cũng cần cập nhật bản xem trước của ứng dụng cho tham số windowSize.

  1. Truyền WindowWidthSizeClass.Compact dưới dạng tham số windowSize tới thành phần kết hợp ReplyApp để xem trước thành phần và nhập gói thích hợp.

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Preview(showBackground = true)
@Composable
fun ReplyAppPreview() {
    ReplyTheme {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Compact,
        )
    }
}
  1. Để thay đổi bố cục ứng dụng dựa trên kích thước màn hình, hãy thêm câu lệnh when vào thành phần kết hợp ReplyApp dựa trên giá trị WindowWidthSizeClass.

ReplyApp.kt

...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
    val viewModel: ReplyViewModel = viewModel()
    val replyUiState = viewModel.uiState.collectAsState().value

    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
        }
        WindowWidthSizeClass.Medium -> {
        }
        WindowWidthSizeClass.Expanded -> {
        }
        else -> {
        }
    }
...

Đến đây, bạn đã thiết lập một nền tảng để sử dụng các giá trị WindowSizeClass nhằm thay đổi bố cục trong ứng dụng. Bước tiếp theo là xác định cách bạn muốn ứng dụng hiển thị trên nhiều kích thước màn hình.

7. Triển khai bố cục điều hướng thích ứng

Triển khai điều hướng giao diện người dùng thích ứng

Thanh điều hướng dưới cùng hiện đang được sử dụng cho tất cả các kích thước màn hình.

Thanh điều hướng dưới cùng cho ứng dụng Reply (Trả lời).

Như đã thảo luận trước đó, phần tử điều hướng này không lý tưởng mấy vì người dùng có thể khó tiếp cận các phần tử điều hướng thiết yếu này trên màn hình lớn hơn. May mắn là đã có các mẫu đề xuất liên quan đến nhiều phần tử điều hướng cho các lớp kích thước cửa sổ khác nhau trong phần điều hướng cho giao diện người dùng thích ứng. Đối với ứng dụng Reply (Trả lời), bạn có thể triển khai các phần tử sau:

Một bảng liệt kê các lớp kích thước cửa sổ và một vài mục hiển thị. Chiều rộng thu gọn hiển thị một thanh điều hướng ở dưới cùng. Chiều rộng trung bình sẽ hiển thị thanh điều hướng. Chiều rộng mở rộng sẽ hiển thị một ngăn điều hướng cố định với cạnh ở trên cùng.

Dải điều hướng là một thành phần điều hướng khác theo thiết kế Material Design, cho phép các tuỳ chọn điều hướng nhỏ gọn dành cho các đích đến chính tiếp cận được từ cạnh ứng dụng.

Dải điều hướng mẫu trong ứng dụng Reply (Trả lời) hiển thị 4 biểu tượng theo chiều dọc là hộp thư đến, đã gửi, thư nháp và thư rác.

Tương tự như vậy, một ngăn điều hướng cố định/vĩnh viễn được tạo bằng thiết kế Material Design là một tuỳ chọn khác để cung cấp khả năng truy cập mang tính nhỏ gọn (ergonomic) cho màn hình lớn hơn.

Ngăn điều hướng cố định trong ứng dụng Reply (Trả lời) liệt kê 4 thẻ theo chiều dọc có biểu tượng và tên của thẻ là Hộp thư đến, Đã gửi, Thư nháp và Thư rác.

Triển khai ngăn điều hướng

Để tạo ngăn điều hướng cho các màn hình mở rộng, bạn có thể sử dụng tham số navigationType. Hãy làm theo các bước sau đây:

  1. Để thể hiện các loại phần tử điều hướng khác nhau, hãy tạo một tệp WindowStateUtils.kt mới trong một gói utils mới thuộc thư mục ui.
  2. Thêm một lớp Enum để đại diện cho các loại thành phần điều hướng khác nhau.

WindowStateUtils.kt

package com.example.reply.ui.utils

enum class ReplyNavigationType {
    BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}

Để triển khai thành công ngăn điều hướng, bạn cần xác định loại điều hướng dựa trên kích thước cửa sổ của ứng dụng.

  1. Trong thành phần kết hợp ReplyApp, hãy tạo một biến navigationType và gán giá trị ReplyNavigationType thích hợp cho biến đó theo kích thước màn hình trong câu lệnh when.

ReplyApp.kt

...
import com.example.reply.ui.utils.ReplyNavigationType
...
    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
            navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
        }
        WindowWidthSizeClass.Medium -> {
            navigationType = ReplyNavigationType.NAVIGATION_RAIL
        }
        WindowWidthSizeClass.Expanded -> {
            navigationType = ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        }
        else -> {
            navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
        }
    }
...

Bạn có thể dùng giá trị navigationType trong thành phần kết hợp ReplyHomeScreen. Bạn có thể chuẩn bị cho việc này bằng cách đặt giá trị này làm tham số cho thành phần kết hợp.

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

ReplyHomeScreen.kt

...
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
)

...

  1. Truyền navigationType vào thành phần kết hợp ReplyHomeScreen.

ReplyApp.kt

...
   ReplyHomeScreen(
        navigationType = navigationType,
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )
...

Tiếp theo, bạn có thể tạo một nhánh để hiển thị nội dung ứng dụng bằng ngăn điều hướng khi người dùng mở ứng dụng trên màn hình mở rộng và hiển thị màn hình chính.

  1. Trong phần thân (nội dung) của thành phần kết hợp ReplyHomeScreen, hãy thêm câu lệnh if cho điều kiện navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepage.

ReplyHomeScreen.kt

import androidx.compose.material3.PermanentNavigationDrawer
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {
...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
    }

    if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                replyUiState = replyUiState,
...
  1. Để tạo ngăn vĩnh viễn, hãy tạo thành phần kết hợp PermanentNavigationDrawer trong nội dung của câu lệnh if và thêm thành phần kết hợp NavigationDrawerContent làm dữ liệu đầu vào cho tham số drawerContent.
  2. Thêm thành phần kết hợp ReplyAppContent làm đối số lambda cuối cùng của PermanentNavigationDrawer.

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                NavigationDrawerContent(
                    selectedDestination = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier

            )
        }
    }

...
  1. Thêm một nhánh else sử dụng phần thân (nội dung) của thành phần kết hợp trước để duy trì việc phân nhánh trước đó cho các màn hình không được mở rộng.

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                NavigationDrawerContent(
                    selectedDestination = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier

            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
    }
}
...
  1. Thêm chú thích thử nghiệm vào thành phần kết hợp ReplyHomeScreen. Bạn cần làm điều này vì API PermanentNavigationDrawer vẫn đang trong quá trình thử nghiệm.

ReplyHomeScreen.kt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {
...
  1. Chạy ứng dụng ở chế độ Máy tính bảng. Bạn sẽ thấy màn hình sau:

Ứng dụng Reply (Trả lời) hiển thị ở chế độ máy tính bảng với ngăn điều hướng ở bên trái màn hình và danh sách email ở bên phải.

Triển khai dải điều hướng

Tương tự như cách triển khai ngăn điều hướng, bạn cần dùng tham số navigationType để chuyển đổi giữa các thành phần điều hướng.

Trước tiên, hãy thêm dải điều hướng cho các màn hình với kích cỡ trung bình.

  1. Bắt đầu với việc chuẩn bị cho thành phần kết hợp ReplyAppContent bằng cách thêm navigationType làm tham số.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
...
  1. Chuyển giá trị navigationType vào cả hai thành phần kết hợp ReplyAppContent.

ReplyHomeScreen.kt

...
            ReplyAppContent(
                navigationType = navigationType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                navigationType = navigationType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
...

Tiếp theo, hãy thêm tính năng phân nhánh để cho phép ứng dụng hiển thị dải điều hướng trong một số trường hợp.

  1. Trong dòng đầu tiên của nội dung thành phần kết hợp ReplyAppContent, hãy gói thành phần kết hợp ReplyNavigationRail xung quanh thành phần kết hợp AnimatedVisibility và đặt tham số visibility thành true nếu giá trị ReplyNavigationTypeNavigationRail.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()            .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList

            )
        }

}
...
  1. Để căn chỉnh chính xác các thành phần kết hợp, hãy gói cả thành phần kết hợp AnimatedVisibilityColumn có ở nội dung ReplyAppContent trong thành phần kết hợp Row.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier.fillMaxSize()) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()            .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList

            )
        }
    }
}
...

Cuối cùng, hãy đảm bảo thanh điều hướng ở dưới cùng sẽ hiển thị trong một số trường hợp.

  1. Sau thành phần kết hợp ReplyListOnlyContent, hãy gói thành phần kết hợp ReplyBottomNavigationBar bằng một thành phần kết hợp AnimatedVisibility.
  2. Đặt tham số visible khi giá trị ReplyNavigationTypeBOTTOM_NAVIGATION.

ReplyHomeScreen.kt

...
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
                ReplyBottomNavigationBar(
                    currentTab = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
...
  1. Chạy ứng dụng ở chế độ Mở ra và gập lại. Bạn sẽ thấy màn hình sau:

Ứng dụng Reply (Trả lời) hiển thị trên một thiết bị có thể gập lại, bên trái màn hình là dải điều hướng còn bên phải là danh sách email.

8. Lấy mã giải pháp

Để tải xuống mã cho lớp học lập trình đã kết thúc, bạn có thể sử dụng lệnh git này:

$ 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

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ở trong Android Studio.

Nếu bạn muốn xem mã giải pháp, hãy xem mã đó trên GitHub.

9. Kết luận

Xin chúc mừng! Bạn đã tiến gần hơn một bước để tạo ứng dụng Reply (Trả lời) thích ứng cho mọi kích thước màn hình bằng cách triển khai bố cục điều hướng thích ứng. Bạn đã nâng cao trải nghiệm người dùng bằng cách sử dụng nhiều hệ số hình dạng trong Android. Trong lớp học lập trình tiếp theo, bạn sẽ cải thiện hơn nữa kỹ năng làm việc với các ứng dụng thích ứng bằng cách triển khai bố cục, thử nghiệm và xem trước nội dung thích ứng.

Đừng quên chia sẻ công việc của bạn trên mạng xã hội với #AndroidBasics!

Tìm hiểu thêm