Tạo ứng dụng thích ứng bằng Jetpack Compose

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách xây dựng ứng dụng có khả năng thích ứng dành cho điện thoại, máy tính bảng và thiết bị có thể gập lại, cũng như cách các ứng dụng này tăng cường phạm vi tiếp cận bằng Jetpack Compose. Bạn cũng sẽ tìm hiểu các phương pháp hay nhất để sử dụng thành phần và giao diện Material 3.

Trước khi tìm hiểu sâu hơn, bạn cần hiểu rõ ý nghĩa của khả năng thích ứng.

Khả năng thích ứng

Giao diện người dùng của ứng dụng phải thích ứng để phù hợp với nhiều kích thước cửa sổ, hướng và kiểu dáng. Bố cục thích ứng thay đổi dựa trên không gian màn hình hiện có. Những thay đổi này bao gồm từ những điều chỉnh bố cục đơn giản cho đến việc lấp đầy không gian, cũng như việc thay đổi hoàn toàn bố cục để tận dụng thêm chỗ trống.

Để tìm hiểu thêm, hãy xem bài viết Thiết kế thích ứng.

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng và xem xét khả năng thích ứng khi dùng Jetpack Compose. Bạn sẽ tạo một ứng dụng có tên là Reply (Trả lời) để hướng dẫn cách triển khai khả năng thích ứng cho mọi loại màn hình, cũng như cách khả năng thích ứng và khả năng kết nối phối hợp với nhau để mang đến cho người dùng trải nghiệm tối ưu.

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

  • Cách thiết kế ứng dụng để nhắm đến tất cả kích thước cửa sổ bằng Jetpack Compose.
  • Cách nhắm mục tiêu ứng dụng cho nhiều thiết bị có thể gập lại.
  • Cách sử dụng các loại thao tác điều hướng để tăng khả năng tiếp cận và khả năng tiếp cận.
  • Cách sử dụng các thành phần Material 3 để mang lại trải nghiệm tốt nhất cho mọi kích thước cửa sổ.

Bạn cần có

  • Phiên bản ổn định mới nhất của Android Studio.
  • Thiết bị ảo có thể đổi kích thước trên Android 13.
  • Có kiến thức về Kotlin.
  • Hiểu biết cơ bản về Compose (chẳng hạn như chú thích @Composable).
  • Quen thuộc cơ bản với bố cục Compose (ví dụ: RowColumn).
  • Hiểu biết cơ bản về đối tượng sửa đổi (ví dụ: Modifier.padding()).

Bạn sẽ sử dụng Trình mô phỏng có thể đổi kích thước cho lớp học lập trình này. Trình mô phỏng này cho phép bạn chuyển đổi giữa các loại thiết bị và kích thước cửa sổ.

Trình mô phỏng có thể đổi kích thước với các tuỳ chọn điện thoại, mở ra, máy tính bảng và máy tính.

Nếu bạn chưa hiểu rõ về Compose, hãy cân nhắc tham gia lớp học lập trình cơ bản về Jetpack Compose trước khi hoàn tất lớp học lập trình này.

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

  • Một ứng dụng ứng dụng email tương tác có tên Reply (Trả lời), sử dụng các phương pháp hay nhất cho thiết kế thích ứng, các thao tác điều hướng Material khác nhau và sử dụng không gian màn hình tối ưu.

Giới thiệu chức năng hỗ trợ nhiều thiết bị mà bạn sẽ đạt được trong lớp học lập trình này

2. Bắt đầu thiết lập

Để lấy mã cho lớp học lập trình này, hãy sao chép kho lưu trữ GitHub từ dòng lệnh:

git clone https://github.com/android/codelab-android-compose.git
cd codelab-android-compose/AdaptiveUiCodelab

Ngoài ra, bạn có thể tải kho lưu trữ ở dạng định dạng tệp ZIP:

Bạn nên bắt đầu bằng mã trong nhánh main (chính) và làm theo hướng dẫn từng bước của lớp học lập trình theo tốc độ của riêng bạn.

Mở dự án trong Android Studio

  1. Trên cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy chọn c01826594f360d94.pngOpen an Existing Project (Mở một dự án hiện có).
  2. Chọn thư mục <Download Location>/AdaptiveUiCodelab (nhớ chọn thư mục AdaptiveUiCodelab có chứa build.gradle).
  3. Khi Android Studio đã nhập dự án, hãy kiểm tra để chắc chắn rằng bạn có thể chạy nhánh main.

Tìm hiểu mã khởi đầu

Mã nhánh main (chính) chứa gói ui. Bạn sẽ làm việc với các tệp sau trong gói đó:

  • MainActivity.kt – Hoạt động chạy đầu tiên khi bạn khởi chạy ứng dụng.
  • ReplyApp.kt – Chứa các thành phần kết hợp giao diện người dùng trên màn hình chính.
  • ReplyHomeViewModel.kt – Cung cấp dữ liệu và trạng thái giao diện người dùng cho nội dung ứng dụng.
  • ReplyListContent.kt – Chứa các thành phần kết hợp để cung cấp danh sách và màn hình chi tiết.

Nếu bạn chạy ứng dụng này trên một trình mô phỏng có thể đổi kích thước và thử nhiều loại thiết bị, chẳng hạn như điện thoại hoặc máy tính bảng, thì giao diện người dùng sẽ chỉ mở rộng đến không gian đã cho thay vì tận dụng không gian màn hình hoặc cung cấp khả năng tiếp cận phù hợp với công thái học.

Màn hình ban đầu trên điện thoại

Chế độ xem kéo giãn ban đầu trên máy tính bảng

Bạn sẽ cập nhật ứng dụng để tận dụng không gian màn hình, tăng khả năng hữu dụng và cải thiện trải nghiệm tổng thể của người dùng.

3. Tạo ứng dụng có khả năng thích ứng

Phần này giới thiệu ý nghĩa của việc làm cho ứng dụng có thể thích ứng và các thành phần mà Material 3 cung cấp để giúp bạn dễ dàng thực hiện việc đó. Tài liệu này cũng đề cập đến các loại màn hình và trạng thái mà bạn sẽ nhắm đến, bao gồm điện thoại, máy tính bảng, máy tính bảng lớn và thiết bị có thể gập lại.

Bạn sẽ bắt đầu bằng cách tìm hiểu các kiến thức cơ bản về kích thước cửa sổ, tư thế gập và các loại tuỳ chọn điều hướng. Sau đó, bạn có thể sử dụng các API này trong ứng dụng để ứng dụng thích ứng hơn.

Kích thước cửa sổ

Thiết bị Android có đủ kiểu dáng và kích thước, từ điện thoại đến thiết bị có thể gập lại, máy tính bảng và thiết bị ChromeOS. Để hỗ trợ nhiều kích thước cửa sổ nhất có thể, giao diện người dùng của bạn cần phải thích ứng và đáp ứng. Để giúp bạn tìm ngưỡng phù hợp để thay đổi giao diện người dùng của ứng dụng, chúng tôi đã xác định các giá trị điểm ngắt giúp phân loại thiết bị thành các lớp kích thước được xác định trước (nhỏ gọn, trung bình và mở rộng), được gọi là lớp kích thước cửa sổ. Đây là một tập hợp các điểm ngắt khung nhìn có ý kiến giúp bạn thiết kế, phát triển và thử nghiệm các bố cục ứng dụng thích ứng và thích ứng.

Các danh mục này được chọn để tạo sự cân bằng giữa tính đơn giản và linh hoạt trong bố cục nhằm tối ưu hoá ứng dụng trong các trường hợp riêng biệt. Lớp kích thước cửa sổ luôn được xác định theo không gian màn hình có sẵn cho ứng dụng, có thể không phải là toàn bộ màn hình thực tế để thực hiện đa nhiệm hoặc các phân đoạn khác.

WindowWidthSizeClass cho chiều rộng thu gọn, trung bình và mở rộng.

WindowHeightSizeClass cho chiều cao thu gọn, trung bình và mở rộng.

Cả chiều rộng và chiều cao đều được phân loại riêng biệt, vì vậy tại bất kỳ thời điểm nào, ứng dụng của bạn cũng có hai lớp kích thước cửa sổ — một cho chiều rộng và một cho chiều cao. Chiều rộng có sẵn thường quan trọng hơn chiều cao có sẵn do sự cuộn theo chiều dọc đang hiển thị; vì vậy, trong trường hợp này, bạn cũng sẽ sử dụng các lớp kích thước chiều rộng.

Các trạng thái gập

Thiết bị có thể gập lại mang đến nhiều tình huống hơn để ứng dụng của bạn có thể thích ứng do kích thước đa dạng và sự hiện diện của bản lề. Bản lề có thể che khuất một phần màn hình, khiến khu vực đó không phù hợp để hiển thị nội dung; chúng cũng có thể bị tách riêng, nghĩa là có hai màn hình thực riêng biệt khi thiết bị được mở ra.

Tư thế gập lại, mở 180 độ và mở một nửa

Ngoài ra, người dùng có thể nhìn vào màn hình bên trong khi bản lề mở một phần, dẫn đến các tư thế thực tế khác nhau dựa trên hướng gập: tư thế trên mặt bàn (gập theo chiều ngang, hiển thị ở bên phải trong hình ảnh trên) và tư thế sách (gập theo chiều dọc).

Đọc thêm về các tư thế gập và bản lề.

Tất cả những điều này đều là những điều cần cân nhắc khi triển khai bố cục thích ứng hỗ trợ thiết bị có thể gập lại.

Nhận thông tin thích ứng

Thư viện Material3 adaptive cung cấp quyền truy cập thuận tiện vào thông tin về cửa sổ mà ứng dụng của bạn đang chạy.

  1. Thêm các mục nhập cho cấu phần phần mềm này và phiên bản của cấu phần phần mềm đó vào tệp danh mục phiên bản:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. Trong tệp bản dựng của mô-đun ứng dụng, hãy thêm phần phụ thuộc thư viện mới rồi đồng bộ hoá Gradle:

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

Giờ đây, trong bất kỳ phạm vi có khả năng kết hợp nào, bạn đều có thể sử dụng currentWindowAdaptiveInfo() để lấy đối tượng WindowAdaptiveInfo chứa thông tin như lớp kích thước cửa sổ hiện tại và liệu thiết bị có ở tư thế có thể gập lại như tư thế trên mặt bàn hay không.

Bạn có thể thử ngay trong MainActivity.

  1. Trong onCreate() bên trong khối ReplyTheme, hãy lấy thông tin thích ứng của cửa sổ và hiển thị các lớp kích thước trong thành phần kết hợp Text. Bạn có thể thêm phần tử này sau phần tử ReplyApp():

MainActivity.kt

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

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )

            val adaptiveInfo = currentWindowAdaptiveInfo()
            val sizeClassText =
                "${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
                "${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
            Text(
                text = sizeClassText,
                color = Color.Magenta,
                modifier = Modifier.padding(
                    WindowInsets.safeDrawing.asPaddingValues()
                )
            )
        }
    }
}

Khi chạy ứng dụng ngay, bạn sẽ thấy các lớp kích thước cửa sổ được in trên nội dung ứng dụng. Bạn có thể khám phá những thông tin khác được cung cấp trong thông tin thích ứng của cửa sổ. Sau đó, bạn có thể xoá Text này vì nó bao gồm nội dung ứng dụng và không cần thiết cho các bước tiếp theo.

4. Điều hướng động

Bây giờ, bạn sẽ điều chỉnh cách điều hướng của ứng dụng khi trạng thái và kích thước thiết bị thay đổi để giúp ứng dụng dễ sử dụng hơn.

Khi người dùng cầm điện thoại, các ngón tay của họ thường ở cuối màn hình. Khi người dùng cầm thiết bị có thể gập lại hoặc máy tính bảng đã mở, các ngón tay của họ thường ở gần các cạnh. Người dùng của bạn phải có thể điều hướng hoặc bắt đầu tương tác với ứng dụng mà không cần phải đặt tay ở vị trí quá khó hoặc thay đổi vị trí đặt tay.

Khi bạn thiết kế ứng dụng và quyết định vị trí đặt các phần tử tương tác trên giao diện người dùng trong bố cục, hãy xem xét tác động của các vùng khác nhau trên màn hình.

  • Những khu vực nào dễ dàng tiếp cận khi cầm thiết bị?
  • Những khu vực nào chỉ có thể tiếp cận bằng cách duỗi ngón tay, điều này có thể gây bất tiện?
  • Những khu vực nào khó tiếp cận hoặc cách xa vị trí người dùng cầm thiết bị?

Điều hướng là yếu tố đầu tiên mà người dùng tương tác và chứa các hành động quan trọng liên quan đến hành trình quan trọng của người dùng. Vì vậy, bạn nên đặt điều hướng ở những khu vực dễ tiếp cận nhất. Thư viện thích ứng Material cung cấp một số thành phần giúp bạn triển khai tính năng điều hướng, tuỳ thuộc vào lớp kích thước cửa sổ của thiết bị.

Thanh điều hướng dưới cùng

Thanh điều hướng dưới cùng rất phù hợp với các kích thước nhỏ gọn, vì chúng ta giữ thiết bị ở vị trí mà ngón cái có thể dễ dàng chạm đến tất cả các điểm chạm điều hướng dưới cùng. Sử dụng chế độ này bất cứ khi nào bạn có thiết bị có kích thước nhỏ gọn hoặc thiết bị có thể gập lại ở trạng thái gập nhỏ gọn.

Thanh điều hướng dưới cùng có các mục

Đối với kích thước cửa sổ có chiều rộng trung bình, thanh điều hướng là lựa chọn lý tưởng để dễ dàng tiếp cận vì ngón tay cái của chúng ta tự nhiên nằm dọc theo cạnh thiết bị. Bạn cũng có thể kết hợp dải điều hướng với ngăn điều hướng để hiển thị thêm thông tin.

Dải điều hướng có các mục

Ngăn điều hướng giúp bạn dễ dàng xem thông tin chi tiết về các thẻ điều hướng, cũng như dễ dàng truy cập được khi bạn đang sử dụng máy tính bảng hoặc các thiết bị lớn hơn. Có hai loại ngăn điều hướng: ngăn điều hướng theo phương thức và ngăn điều hướng cố định.

Ngăn điều hướng theo phương thức phương thức

Bạn có thể sử dụng ngăn điều hướng phương thức cho điện thoại và máy tính bảng có kích thước nhỏ đến trung bình vì ngăn này có thể mở rộng hoặc ẩn dưới dạng lớp phủ trên nội dung. Dải điều hướng đôi khi có thể được kết hợp với dải điều hướng.

Ngăn điều hướng mô-đun có các mục

Ngăn điều hướng cố định

Bạn có thể sử dụng ngăn điều hướng cố định để điều hướng cố định trên máy tính bảng lớn, Chromebook và máy tính.

Ngăn điều hướng cố định có các mục

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

Giờ đây, bạn sẽ chuyển đổi giữa các loại thao tác điều hướng khi trạng thái và kích thước thiết bị thay đổi.

Hiện tại, ứng dụng luôn hiển thị NavigationBar bên dưới nội dung màn hình bất kể trạng thái thiết bị. Thay vào đó, bạn có thể sử dụng thành phần NavigationSuiteScaffold của Material để tự động chuyển đổi giữa các thành phần điều hướng dựa trên thông tin như lớp kích thước cửa sổ hiện tại.

  1. Thêm phần phụ thuộc Gradle để lấy thành phần này bằng cách cập nhật danh mục phiên bản và tập lệnh bản dựng của ứng dụng, sau đó đồng bộ hoá Gradle:

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0"

[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.navigation.suite)
}
  1. Tìm hàm có khả năng kết hợp ReplyNavigationWrapper() trong ReplyApp.kt rồi thay thế Column cùng nội dung trong đó bằng NavigationSuiteScaffold:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    NavigationSuiteScaffold(
        navigationSuiteItems = {
            ReplyDestination.entries.forEach {
                item(
                    selected = it == selectedDestination,
                    onClick = { /*TODO update selection*/ },
                    icon = {
                        Icon(
                            imageVector = it.icon,
                            contentDescription = stringResource(it.labelRes)
                        )
                    },
                    label = {
                        Text(text = stringResource(it.labelRes))
                    },
                )
            }
        }
    ) {
        content()
    }
}

Đối số navigationSuiteItems là một khối cho phép bạn thêm các mục bằng hàm item(), tương tự như việc thêm các mục trong LazyColumn. Bên trong trailing lambda, mã này gọi content() được truyền dưới dạng đối số cho ReplyNavigationWrapperUI().

Chạy ứng dụng trên trình mô phỏng rồi thử thay đổi kích thước giữa điện thoại, thiết bị có thể gập lại và máy tính bảng. Bạn sẽ thấy thanh điều hướng thay đổi thành dải điều hướng và quay lại.

Trên các cửa sổ rất rộng, chẳng hạn như trên máy tính bảng ở chế độ ngang, bạn có thể muốn hiển thị ngăn điều hướng cố định. NavigationSuiteScaffold hỗ trợ hiển thị ngăn cố định, mặc dù ngăn này không xuất hiện trong bất kỳ giá trị WindowWidthSizeClass nào hiện tại. Tuy nhiên, bạn có thể thực hiện việc này bằng một thay đổi nhỏ.

  1. Thêm mã sau ngay trước lệnh gọi đến NavigationSuiteScaffold:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    val windowSize = with(LocalDensity.current) {
        currentWindowSize().toSize().toDpSize()
    }
    val layoutType = if (windowSize.width >= 1200.dp) {
        NavigationSuiteType.NavigationDrawer
    } else {
        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
            currentWindowAdaptiveInfo()
        )
    }

    NavigationSuiteScaffold(
        layoutType = layoutType,
        ...
    ) {
        content()
    }
}

Trước tiên, mã này sẽ lấy kích thước cửa sổ và chuyển đổi kích thước đó thành đơn vị DP bằng currentWindowSize()LocalDensity.current, sau đó so sánh chiều rộng cửa sổ để quyết định loại bố cục của giao diện người dùng điều hướng. Nếu chiều rộng tối thiểu của cửa sổ là 1200.dp, thì cửa sổ sẽ sử dụng NavigationSuiteType.NavigationDrawer. Nếu không, hàm này sẽ quay lại phương thức tính mặc định.

Khi bạn chạy lại ứng dụng trên trình mô phỏng có thể đổi kích thước và thử nhiều loại, hãy lưu ý rằng bất cứ khi nào cấu hình màn hình thay đổi hoặc bạn mở ra một thiết bị gập, thành phần điều hướng sẽ thay đổi thành loại phù hợp với kích thước đó.

Cho thấy các thay đổi về khả năng thích ứng cho nhiều kích thước thiết bị.

Xin chúc mừng! Bạn đã tìm hiểu về các loại thao tác điều hướng để hỗ trợ nhiều loại kích thước và trạng thái cửa sổ!

Trong phần tiếp theo, bạn sẽ tìm hiểu cách tận dụng mọi khu vực màn hình còn lại thay vì kéo giãn cùng một mục danh sách từ cạnh này sang cạnh kia.

5. Sử dụng không gian màn hình

Bất kể bạn đang chạy ứng dụng trên máy tính bảng nhỏ, thiết bị chưa mở ra hay máy tính bảng lớn, màn hình sẽ được kéo giãn để lấp đầy không gian còn lại. Bạn muốn đảm bảo có thể tận dụng không gian màn hình đó để hiển thị thêm thông tin, chẳng hạn như cho ứng dụng này, hiển thị email và chuỗi thư cho người dùng trên cùng một trang.

Material 3 xác định 3 bố cục chuẩn, trong đó mỗi bố cục có cấu hình dành cho các lớp kích thước cửa sổ nhỏ gọn, trung bình và mở rộng. Bố cục chuẩn Danh sách chi tiết rất phù hợp với trường hợp sử dụng này và có sẵn trong Compose dưới dạng ListDetailPaneScaffold.

  1. Tải thành phần này bằng cách thêm các phần phụ thuộc sau và đồng bộ hoá Gradle:

gradle/libs.versions.toml

[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.layout)
    implementation(libs.androidx.material3.adaptive.navigation)
}
  1. Tìm hàm có khả năng kết hợp ReplyAppContent() trong ReplyApp.kt. Hàm này hiện chỉ hiển thị ngăn danh sách bằng cách gọi ReplyListPane(). Thay thế cách triển khai này bằng ListDetailPaneScaffold bằng cách chèn mã sau. Vì đây là một API thử nghiệm, nên bạn cũng sẽ thêm chú thích @OptIn vào hàm ReplyAppContent():

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            ReplyListPane(replyHomeUIState, onEmailClick)
        },
        detailPane = {
            ReplyDetailPane(replyHomeUIState.emails.first())
        }
    )
}

Trước tiên, mã này sẽ tạo một trình điều hướng bằng cách sử dụng rememberListDetailPaneNavigator(). Trình điều hướng cung cấp một số quyền kiểm soát đối với ngăn nào được hiển thị và nội dung nào sẽ được trình bày trong ngăn đó. Nội dung này sẽ được hiển thị sau.

ListDetailPaneScaffold sẽ hiển thị 2 ngăn khi lớp kích thước chiều rộng cửa sổ được mở rộng. Nếu không, trình tạo sẽ hiển thị một ngăn hoặc ngăn còn lại dựa trên các giá trị được cung cấp cho hai tham số: lệnh tạo khung và giá trị tạo khung. Để có hành vi mặc định, mã này sử dụng lệnh scaffold và giá trị scaffold do trình điều hướng cung cấp.

Các tham số bắt buộc còn lại là lambda có thể kết hợp cho các ngăn. ReplyListPane()ReplyDetailPane() (có trong ReplyListContent.kt) được dùng để điền vào vai trò của ngăn danh sách và ngăn chi tiết tương ứng. ReplyDetailPane() yêu cầu đối số email, nên hiện tại, mã này sử dụng email đầu tiên từ danh sách email trong ReplyHomeUIState.

Chạy ứng dụng và chuyển chế độ xem trình mô phỏng sang thiết bị có thể gập lại hoặc máy tính bảng (bạn cũng có thể phải thay đổi hướng) để xem bố cục hai ngăn. Giao diện này đã trông đẹp hơn nhiều so với trước!

Bây giờ, hãy xử lý một số hành vi mong muốn của màn hình này. Khi người dùng nhấn vào một email trong ngăn danh sách, email đó sẽ hiển thị trong ngăn chi tiết cùng với tất cả thư trả lời. Hiện tại, ứng dụng không theo dõi email nào đã được chọn và thao tác nhấn vào một mục sẽ không có tác dụng. Nơi tốt nhất để lưu trữ thông tin này là cùng với phần còn lại của trạng thái giao diện người dùng trong ReplyHomeUIState.

  1. Mở ReplyHomeViewModel.kt rồi tìm lớp dữ liệu ReplyHomeUIState. Thêm một thuộc tính cho email đã chọn, với giá trị mặc định là null:

ReplyHomeViewModel.kt

data class ReplyHomeUIState(
    val emails : List<Email> = emptyList(),
    val selectedEmail: Email? = null,
    val loading: Boolean = false,
    val error: String? = null
)
  1. Trong cùng một tệp, ReplyHomeViewModel có một hàm setSelectedEmail() được gọi khi người dùng nhấn vào một mục trong danh sách. Sửa đổi hàm này để sao chép trạng thái giao diện người dùng và ghi lại email đã chọn:

ReplyHomeViewModel.kt

fun setSelectedEmail(email: Email) {
    _uiState.update {
        it.copy(selectedEmail = email)
    }
}

Một điều cần cân nhắc là những gì xảy ra trước khi người dùng nhấn vào bất kỳ mục nào và email đã chọn là null. Nội dung nào sẽ hiển thị trong ngăn chi tiết? Có nhiều cách để xử lý trường hợp này, chẳng hạn như hiển thị mục đầu tiên trong danh sách theo mặc định.

  1. Trong chính tệp đó, hãy sửa đổi hàm observeEmails(). Khi danh sách email được tải, nếu trạng thái giao diện người dùng trước đó không có email nào được chọn, hãy đặt trạng thái đó thành mục đầu tiên:

ReplyHomeViewModel.kt

private fun observeEmails() {
    viewModelScope.launch {
        emailsRepository.getAllEmails()
            .catch { ex ->
                _uiState.value = ReplyHomeUIState(error = ex.message)
            }
            .collect { emails ->
                val currentSelection = _uiState.value.selectedEmail
                _uiState.value = ReplyHomeUIState(
                    emails = emails,
                    selectedEmail = currentSelection ?: emails.first()
                )
            }
    }
}
  1. Quay lại ReplyApp.kt và sử dụng email đã chọn (nếu có) để điền nội dung vào ngăn chi tiết:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    detailPane = {
        if (replyHomeUIState.selectedEmail != null) {
            ReplyDetailPane(replyHomeUIState.selectedEmail)
        }
    }
)

Chạy lại ứng dụng và chuyển trình mô phỏng sang kích thước máy tính bảng. Bạn sẽ thấy rằng thao tác nhấn vào một mục trong danh sách sẽ cập nhật nội dung của ngăn chi tiết.

Thao tác này hiệu quả khi cả hai ngăn đều hiển thị, nhưng khi cửa sổ chỉ có chỗ để hiển thị một ngăn, thì có vẻ như không có gì xảy ra khi bạn nhấn vào một mục. Hãy thử chuyển chế độ xem trình mô phỏng sang điện thoại hoặc thiết bị có thể gập lại theo chiều dọc và nhận thấy rằng chỉ có ngăn danh sách là hiển thị ngay cả sau khi nhấn vào một mục. Đó là do mặc dù email đã chọn được cập nhật, nhưng ListDetailPaneScaffold vẫn giữ tiêu điểm trên ngăn danh sách trong các cấu hình này.

  1. Để khắc phục vấn đề đó, hãy chèn mã sau làm lambda được truyền đến ReplyListPane:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    listPane = {
        ReplyListPane(
            replyHomeUIState = replyHomeUIState,
            onEmailClick = { email ->
                onEmailClick(email)
                navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
            }
        )
    },
    // ...
)

Hàm lambda này sử dụng trình điều hướng được tạo trước đó để thêm hành vi bổ sung khi người dùng nhấp vào một mục. Hàm này sẽ gọi lambda ban đầu được truyền vào hàm này, sau đó cũng gọi navigator.navigateTo() để chỉ định ngăn nào sẽ hiển thị. Mỗi ngăn trong ngăn xếp có một vai trò liên kết với nó và đối với ngăn chi tiết, đó là ListDetailPaneScaffoldRole.Detail. Trên các cửa sổ nhỏ hơn, điều này sẽ tạo ra giao diện rằng ứng dụng đã điều hướng tiến.

Ứng dụng cũng cần xử lý những gì xảy ra khi người dùng nhấn nút quay lại từ ngăn chi tiết. Hành vi này sẽ khác nhau tuỳ thuộc vào việc có một ngăn hay hai ngăn hiển thị.

  1. Hỗ trợ thao tác quay lại bằng cách thêm mã sau.

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    BackHandler(navigator.canNavigateBack()) {
        navigator.navigateBack()
    }

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            AnimatedPane {
                ReplyListPane(
                    replyHomeUIState = replyHomeUIState,
                    onEmailClick = { email ->
                        onEmailClick(email)
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
                    }
                )
            }
        },
        detailPane = {
            AnimatedPane {
                if (replyHomeUIState.selectedEmail != null) {
                    ReplyDetailPane(replyHomeUIState.selectedEmail)
                }
            }
        }
    )
}

Trình điều hướng biết trạng thái đầy đủ của ListDetailPaneScaffold, liệu có thể điều hướng quay lại hay không và việc cần làm trong tất cả các trường hợp này. Mã này tạo một BackHandler được bật bất cứ khi nào trình điều hướng có thể quay lại và bên trong lệnh gọi lambda navigateBack(). Ngoài ra, để quá trình chuyển đổi giữa các ngăn diễn ra suôn sẻ hơn, mỗi ngăn được gói trong một thành phần kết hợp AnimatedPane().

Chạy lại ứng dụng trên trình mô phỏng có thể đổi kích thước cho tất cả các loại thiết bị và lưu ý rằng bất cứ khi nào cấu hình màn hình thay đổi hoặc bạn mở ra một thiết bị có thể gập lại, nội dung điều hướng và màn hình sẽ thay đổi linh động để phản hồi các thay đổi về trạng thái thiết bị. Ngoài ra, hãy thử nhấn vào email trong ngăn danh sách và xem bố cục hoạt động như thế nào trên các màn hình khác nhau, hiển thị cả hai ngăn cạnh nhau hoặc tạo ảnh động giữa các ngăn một cách mượt mà.

Cho thấy các thay đổi về khả năng thích ứng cho nhiều kích thước thiết bị.

Xin chúc mừng! Bạn đã thành công trong việc điều chỉnh ứng dụng cho phù hợp với mọi loại trạng thái và kích thước thiết bị. Hãy tiếp tục và thử chạy ứng dụng trên thiết bị có thể gập lại, máy tính bảng hoặc các thiết bị di động khác.

6. Xin chúc mừng

Xin chúc mừng! Bạn đã hoàn tất thành công lớp học lập trình này và tìm hiểu cách tạo ứng dụng thích ứng bằng Jetpack Compose.

Bạn đã tìm hiểu cách kiểm tra kích thước và trạng thái gập của thiết bị, cũng như cách cập nhật giao diện người dùng, chức năng điều hướng và các chức năng khác của ứng dụng cho phù hợp. Bạn cũng đã tìm hiểu cách khả năng thích ứng cải thiện khả năng tiếp cận và nâng cao trải nghiệm người dùng.

Tiếp theo là gì?

Hãy tham khảo các lớp học lập trình khác trên Lộ trình học tập về Compose.

Ứng dụng mẫu

  • Mẫu Compose là một tập hợp nhiều ứng dụng kết hợp các phương pháp hay nhất được giải thích trong các lớp học lập trình.

Tài liệu tham khảo