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 khả năng 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 của Material 3.

Trước khi bắt đầu, bạn cần hiểu rõ ý nghĩa của tính 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, chọn các kiểu điều hướng tương ứng, 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 phần 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à hoạt động của 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). Ứng dụng này cho bạn biết 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 tiếp cận hoạt động cùng nhau để mang lại trải nghiệm tối ưu cho người dùng.

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

  • Cách thiết kế ứng dụng để nhắm đến mọi kích thước cửa sổ bằng Jetpack Compose.
  • Cách nhắm đến ứng dụng của bạn cho nhiều thiết bị gập.
  • Cách sử dụng nhiều loại thành phần điều hướng để tăng khả năng tiếp cận và hỗ trợ 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ó

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 nhiều loại thiết bị và kích thước cửa sổ.

Trình mô phỏng có thể thay đổi kích thước với các lựa chọn về điện thoại, thiết bị ở trạng thái mở, 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 thành 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 email tương tác có tên là Reply (Trả lời), sử dụng các phương pháp hay nhất cho thiết kế có thể thích ứng, các thành phần điều hướng Material khác nhau và cách sử dụng không gian màn hình tối ưu.

Nhiều thiết bị hỗ trợ giới thiệu 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 đoạn mã trong nhánh main 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ứ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ử các loại thiết bị khác nhau, 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 ra không gian đã cho thay vì tận dụng không gian màn hình hoặc mang lại khả năng tiếp cận tiện dụng.

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

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

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

3. Làm cho ứng dụng có khả năng thích ứng

Phần này giới thiệu ý nghĩa của việc điều chỉnh ứng dụng và những thành phần mà Material 3 cung cấp để giúp bạn thực hiện việc này dễ dàng hơn. Khoá học 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 cỡ 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 những kiến thức cơ bản về kích thước cửa sổ, tư thế gập và các loại lựa chọn điều hướng. Sau đó, bạn có thể sử dụng các API này trong ứng dụng để giúp ứng dụng thích ứng tốt hơn.

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

Các 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 ra 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 (thu gọn, trung bình và mở rộng), được gọi là các lớp kích thước cửa sổ. Đây là một tập hợp các điểm ngắt khung hiển thị 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. Không gian này 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 cho 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 nhỏ 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ự phổ biến của việc cuộn màn hình theo chiều dọc, 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.

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 nữa mà ứ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; bản lề cũng có thể tách biệt, tức là có 2 màn hình thực tế riêng biệt khi thiết bị mở ra.

Các tư thế của thiết bị có 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 trong khi bản lề đang mở một phần, dẫn đến các tư thế vật lý khác nhau dựa trên hướng gấp: tư thế trên mặt bàn (gấp ngang, xuất hiện ở bên phải trong hình ảnh ở trên) và tư thế trang sách (gấp 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 cần được 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 thực hiện đồng bộ hoá Gradle:

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

Giờ đây, trong mọi phạm vi có khả năng kết hợp, bạn có thể dùng currentWindowAdaptiveInfo() để lấy một đố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ể dùng thử tính năng này ngay bây giờ trong phần 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 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, các lớp kích thước cửa sổ sẽ xuất hiện trên nội dung ứng dụng. Bạn có thể thoải mái 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 của ứng dụng và không cần thiết cho các bước tiếp theo.

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

Giờ đây, bạn sẽ điều chỉnh 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ười dùng dễ dàng sử dụng ứng dụng của bạn hơn.

Khi người dùng cầm điện thoại, các ngón tay của họ thường ở dưới cùng màn hình. Khi người dùng cầm một thiết bị có thể gập lại hoặc máy tính bảng đang mở, các ngón tay của họ thường ở gần các cạnh. Người dùng có thể điều hướng hoặc bắt đầu tương tác với một ứ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 thiết kế ứng dụng và quyết định vị trí đặt các phần tử giao diện người dùng có thể tương tác trong bố cục, hãy cân nhắc những tác động về mặt công thái học của các khu vực 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ể chạm tới 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 ở xa nơi người dùng cầm thiết bị?

Thanh điều hướng là thứ đầu tiên mà người dùng tương tác và chứa các hành động có tầm quan trọng cao 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 thanh điều hướng ở những vị trí 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 hoạt độ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 thường cầm thiết bị sao cho ngón cái có thể dễ dàng chạm vào tất cả các điểm chạm của thanh điều hướng dưới cùng. Hãy sử dụng thành phần này bất cứ khi nào bạn có một thiết bị có kích thước nhỏ gọn hoặc một 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 thường đặt dọc theo cạnh của thiết bị. Bạn cũng có thể kết hợp thanh điều hướng với ngăn điều hướng để hiện 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 cho các thẻ điều hướng và dễ dàng truy cập khi bạn đang sử dụng máy tính bảng hoặc thiết bị lớn hơn. Có hai loại ngăn điều hướng: ngăn điều hướng mẫu và ngăn điều hướng cố định.

Ngăn điều hướng 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 từ 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. Đôi khi, bạn có thể kết hợp thành phần này với một thanh điều hướng.

Ngăn điều hướng phương thức có các mục

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

Bạn có thể dùng ngăn điều hướng cố định cho chế độ đ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 thành phần điều hướng động

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

Hiện tại, ứng dụng luôn hiển thị một NavigationBar bên dưới nội dung trên màn hình, bất kể trạng thái của thiết bị. Thay vào đó, bạn có thể 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 để nhận 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 đó thực hiện đồ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 composable ReplyNavigationWrapper() trong ReplyApp.kt rồi thay thế Column và nội dung của hàm đó 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ố đến ReplyNavigationWrapperUI().

Chạy ứng dụng trên trình mô phỏng và 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 thanh điều hướng và ngược 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 ngăn điều hướng cố định. NavigationSuiteScaffold có hỗ trợ việc hiển thị một ngăn kéo cố định, mặc dù ngăn kéo 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()
    }
}

Đầu tiên, mã này lấy kích thước cửa sổ và chuyển đổi kích thước đó thành đơn vị DP bằng cách sử dụ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 cửa sổ tối thiểu là 1200.dp, thì chiều rộng này sẽ sử dụng NavigationSuiteType.NavigationDrawer. Nếu không, hệ thống sẽ quay lại tính toán theo mặc định.

Khi bạn chạy lại ứng dụng trên trình mô phỏng có thể thay đổi kích thước và thử các loại khác nhau, 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ở một thiết bị có thể gập lại, 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 những thay đổi về khả năng thích ứng đối với các kích thước thiết bị khác nhau.

Xin chúc mừng! Bạn đã tìm hiểu về nhiều loại thành phần đ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ẽ khám phá cách tận dụng mọi khoảng trống còn lại trên màn hình thay vì kéo dài cùng một mục trong danh sách từ cạnh này sang cạnh khác.

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ị ở trạng thái mở hay máy tính bảng lớn, màn hình sẽ được kéo giãn để lấp đầy khoảng trống còn lại. Bạn cần đảm bảo có thể tận dụng không gian màn hình đó để hiện thêm thông tin, chẳng hạn như đối với ứng dụng này, hãy hiện email và chuỗi cho người dùng trên cùng một trang.

Material 3 xác định 3 bố cục chuẩn, mỗi bố cục có cấu hình cho các lớp kích thước cửa sổ thu 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ó trong Compose dưới dạng ListDetailPaneScaffold.

  1. Nhận thành phần này bằng cách thêm các phần phụ thuộc sau đây và thực hiện đồ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 composable 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ế phương thức triển khai này bằng ListDetailPaneScaffold bằng cách chèn đoạn mã sau. Vì đây là một API thử nghiệm, nên bạn cũng sẽ thêm chú giải @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())
        }
    )
}

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

ListDetailPaneScaffold sẽ cho thấy hai ngăn khi lớp kích thước cửa sổ theo chiều rộng được mở rộng. Nếu không, nó sẽ hiện 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 2 tham số: chỉ thị giàn giáo và giá trị giàn giáo. Để có được hành vi mặc định, mã này sử dụng chỉ thị khung và giá trị khung 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() dự kiến sẽ có một đối số email, vì vậy, hiện tại mã này sử dụng email đầu tiên trong 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. Như vậy trông đã đẹp hơn rất nhiều so với trước đây!

Bây giờ, hãy giải quyết 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ẽ xuất hiện trong ngăn chi tiết cùng với tất cả các thư trả lời. Hiện tại, ứng dụng không theo dõi email nào được chọn và việ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 giữ 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à điều gì sẽ 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. Những thông tin nào sẽ xuất hiện 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 cùng một 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 sẵn nội dung cho 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 khi bạn nhấn vào một mục trong danh sách, nội dung của ngăn chi tiết sẽ được cập nhật.

Điều này hoạt động 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 ở chế độ dọc và lưu ý rằng chỉ có ngăn danh sách xuất hiện ngay cả sau khi bạn nhấn vào một mục. Đó là vì 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 vào ReplyListPane:

ReplyApp.kt

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

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

Ứ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 hay hai ngăn hiển thị.

  1. Hỗ trợ thao tác điều hướng 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 toàn bộ trạng thái của ListDetailPaneScaffold, liệu có thể điều hướng quay lại hay không và cần làm gì trong tất cả các trường hợp này. Mã này tạo ra 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 hàm lambda sẽ gọi navigateBack(). Ngoài ra, để quá trình chuyển đổi giữa các ngăn diễn ra mượt mà hơn, mỗi ngăn sẽ được bao bọc trong một thành phần kết hợp AnimatedPane().

Chạy lại ứng dụng trên một 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ở 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 hoạt để 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, cho thấy cả hai ngăn cạnh nhau hoặc chuyển động mượt mà giữa chúng.

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

Xin chúc mừng! Bạn đã điều chỉnh thành công ứng dụng của mình cho phù hợp với mọi loại trạng thái và kích thước thiết bị. Hãy 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 có khả nă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ị, đồng thời cập nhật giao diện người dùng, chế độ đ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 giúp 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

  • Các mẫu Compose là một tập hợp gồm 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