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 tạo ứng dụng thích ứng 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 ứ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 và tuỳ chỉnh giao diện thành phần Material 3.

Trước khi tìm hiểu kỹ hơn, quan trọng là bạn phải hiểu được khái niệm "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 để thích ứng với nhiều kích thước cửa sổ, hướng và hệ số hình dạng. Bố cục thích ứng thay đổi dựa trên không gian màn hình có sẵn. Những thay đổi này bao gồm từ những điều chỉnh bố cục đơn giản để lấp đầy không gian, chọn kiểu điều hướng tương ứng cho đến thay đổi hoàn toàn bố cục để tận dụng thêm không gian.

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

Bạn sẽ dùng Trình mô phỏng có thể thay đổi kích thước cho lớp học lập trình này để 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ể thay đổi kích thước với các tuỳ chọn điện thoại, màn hình 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 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 email khách có tính tương tác áp dụng các phương pháp hay nhất về thiết kế thích ứng, nhiều thao tác điều hướng trong Material và mức sử dụng không gian màn hình tối ưu.

Giới thiệu nhiều loại 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 và làm theo hướng dẫn từng bước của lớp học lập trình theo tiến độ phù hợp với 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 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 tại điểm truy cập nơi bạn khởi động ứng dụng của mình.
  • ReplyApp.kt – Chứa các thành phần kết hợp giao diện người dùng 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.

Trước tiên, bạn sẽ tập trung vào MainActivity.kt.

MainActivity.kt

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

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

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ị (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 nhất định thay vì tận dụng không gian màn hình hoặc mang đến khả năng tiếp cận 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 màn hình để 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. Làm cho ứng dụng dễ điều chỉnh

Phần này giới thiệu ý nghĩa của việc tăng khả năng thích ứng cho ứng dụng cũng như những thành phần mà Material 3 cung cấp để giúp bạn làm việc đó dễ dàng hơn. Danh mục này cũng bao gồm 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 nguyên tắ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 của mình để giúp ứ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 cho đến 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 có tính thích ứng và thích ứng. Nhằm giúp bạn tìm ra ngưỡng thích 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ố định giúp bạn thiết kế, phát triển và kiểm thử 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 riêng để cân bằng giữa tính đơn giản của bố cục và tính linh hoạt trong việc 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 nhỏ 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ũng có hai lớp kích thước cửa sổ — một cho chiều rộng và một lớp 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 cuộn dọc phổ biến. 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 đưa ra nhiều tình huống hơn mà ứng dụng của bạn có thể thích ứng do kích thước đa dạng của chú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ị phân tách, nghĩa là có hai màn hình thực riêng biệt khi thiết bị được mở ra.

Các tư thế có thể gập lại, mở bằng phẳng 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 trên) và tư thế sách (gập theo chiều dọc).

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

Tất cả đều là những điều cần xem xét 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 adaptive Material3 cho phép bạ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 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-beta01"

[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 bất kỳ phạm vi thành phần kết hợp nào, bạn có thể 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ó đang ở 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 vào 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(20.dp)
            )
        }
    }
}

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 nội dung khác được cung cấp trong cửa sổ thông tin thích ứng. Sau đó, bạn có thể xoá Text này vì nó che khuất nội dung ứng dụng và bạn sẽ không cần thực hiện các bước tiếp theo nữa.

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

Bây giờ, bạn sẽ điều chỉnh cách điều hướng trong ứng dụng khi trạng thái thiết bị và kích thước thay đổi để cải thiện phạm vi tiếp cận.

Khả năng tiếp cận là khả năng điều hướng hoặc bắt đầu tương tác với ứng dụng mà không yêu cầu vị trí tay phải hoặc thay đổi vị trí của bàn tay. Khi người dùng cầm điện thoại, các ngón tay của họ thường nằm ở cuối 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 đã mở, các ngón tay của họ thường ở gần các cạnh. 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.

  • Bạn có thể chạm vào khu vực nào khi cầm thiết bị?
  • 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ể bất tiện?
  • Khu vực nào khó tiếp cận hoặc ở xa nơi người dùng cầm thiết bị?

Thành phần Điều hướng là thứ đầu tiên người dùng tương tác. Thành phần này chứa các hành động có tầm quan trọng cao liên quan đến hành trình trọng yếu của người dùng. Vì vậy, bạn nên đặt thành phần này ở những vị trí dễ tiếp cận nhất. 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. Hãy 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, dải điều hướng là lý tưởng để bạn có thể tiếp cận vì ngón cái của chúng ta rơi tự nhiên 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. Hiện có 2 loại ngăn điều hướng: ngăn điều hướng kiểu mẫu và ngăn điều hướng cố định.

Ngăn điều hướng mô-đun

Bạn có thể sử dụng ngăn điều hướng ở chế độ cho điện thoại và máy tính bảng có kích thước từ nhỏ gọn đế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 để bàn.

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

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

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

Hiện tại, ứng dụng luôn hiển thị NavigationBar bên dưới nội dung trên 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 để tải 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-beta01"

[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 cách sử dụng hàm item(), tương tự như cách 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ị một ngăn cố định, mặc dù ngăn này không hiển thị trong bất kỳ giá trị WindowWidthSizeClass hiện tại nào. Tuy nhiên, bạn có thể thực hiện điều này với 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 lấy kích thước cửa sổ rồi chuyển đổi 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 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, phép tính sẽ trở lại cách 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ử 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 khi bạn mở 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 những 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 kiểu đ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 bất kỳ khu vực màn hình còn lại nào thay vì kéo dài cùng một mục 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ị được mở 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 rằng mình 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ư cho ứng dụng này, hiển thị email và chuỗi tin nhắn 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 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 List Detail (Chi tiết danh sách) là bố cục hoàn hảo cho 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 đây và đồng bộ hoá Gradle:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[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à 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())
        }
    )
}

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, một ngăn hoặc ngăn còn lại 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ố: lệnh scaffold (giàn giáo) và giá trị scaffold (giàn giáo). Để 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) dùng để điền vai trò tương ứng trong ngăn danh sách và ngăn chi tiết. 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. Tính năng này trông đẹp hơn nhiều so với trước đây!

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 gì. Cách tốt nhất để lưu giữ thông tin này là 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 tệp đó, ReplyHomeViewModel có 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)
    }
}

Điều cần xem xét là những 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. 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 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 được chọn, hãy đặt email đó 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 rồi sử dụng email đã chọn (nếu có) để điền nội dung trong 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.

Tính năng 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 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à vì mặc dù email đã chọn đã được cập nhật, nhưng ListDetailPaneScaffold vẫn đang tập trung vào ngăn danh sách trong các cấu hình này.

  1. Để khắc phục vấn đề này, hãy chèn mã sau đây khi hà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 đã 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 hàm lambda ban đầu được truyền vào hàm này, sau đó gọi navigator.navigateTo() chỉ định ngăn nào sẽ hiển thị. Mỗi ngăn trong Scaffold được liên kết với một vai trò và đối với ngăn chi tiết, đó là ListDetailPaneScaffoldRole.Detail. Trên các cửa sổ nhỏ hơn, thao tác này sẽ tạo ra giao diện rằng ứng dụng đã di chuyển về phía trước.

Ứ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 qua 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ợ đ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 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ả 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 hàm lambda gọi 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ị. Bạn sẽ nhận thấy rằng bất cứ khi nào cấu hình màn hình thay đổi hoặc khi bạn mở thiết bị gập, nội dung điều hướng và màn hình sẽ linh hoạt thay đổi để đáp ứng 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 rồi 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 những 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 đã thiết lập thành công ứng dụng của mình có thể thích ứng với mọi loại trạng thái và kích thước của thiết bị. Hãy tiếp tục và khám phá bằng cách 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 giúp ứ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 giúp cải thiện phạm vi 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 Compose.

Ứng dụng mẫu

  • Mẫu Compose là tập hợp nhiều ứng dụng kết hợp những 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