Di chuyển sang Jetpack Compose

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

1. Giới thiệu

Compose và hệ thống Chế độ xem có thể hoạt động cùng nhau.

Trong lớp học lập trình này, bạn sẽ di chuyển các phần của màn hình thông tin chi tiết về cây trồng của Sunflower sang Compose. Chúng tôi đã tạo một bản sao của dự án để bạn có thể thử di chuyển ứng dụng thực tế sang Compose.

Khi kết thúc lớp học lập trình, bạn có thể tiếp tục di chuyển và chuyển đổi các màn hình còn lại của Sunflower nếu muốn.

Để được hỗ trợ thêm khi bạn tham gia lớp học lập trình này, hãy xem các mã sau:

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

Trong lớp học lập trình này, bạn sẽ tìm hiểu:

  • Những đường di chuyển khác nhau bạn có thể làm theo
  • Cách di chuyển dần ứng dụng sang Compose
  • Cách thêm Compose vào màn hình được tạo hiện có bằng chế độ xem Android
  • Cách sử dụng Chế độ xem Android từ bên trong Compose
  • Cách sử dụng giao diện trên hệ thống Chế độ xem trong Compose
  • Cách kiểm tra màn hình bằng hệ thống Chế độ xem và mã Compose

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

Bạn cần có

2. Lên kế hoạch di chuyển

Việc di chuyển đến Compose tuỳ thuộc vào bạn và nhóm của bạn. Có nhiều cách để tích hợp Jetpack Compose vào một ứng dụng Android hiện có. Hai chiến lược di chuyển phổ biến là:

  • Xây dựng một màn hình mới hoàn toàn bằng Compose
  • Sử dụng màn hình hiện có rồi di chuyển dần các thành phần trên màn hình đó.

Compose trong màn hình mới

Một phương pháp phổ biến khi cải tiến ứng dụng sang một công nghệ mới là sử dụng công nghệ đó với các tính năng mới bạn xây dựng cho ứng dụng của mình. Trong trường hợp này, màn hình mới sẽ được áp dụng. Nếu bạn cần tạo màn hình giao diện người dùng mới cho ứng dụng, hãy cân nhắc sử dụng Compose cho màn hình và phần còn lại của ứng dụng có thể vẫn còn trong hệ thống Chế độ xem.

Trong trường hợp này, bạn phải thực hiện tương tác Compose ở cạnh của các tính năng được di chuyển đó.

Compose và Chế độ xem hoạt động cùng nhau

Khi đã thêm một màn hình, bạn có thể chuyển một số phần sang Compose và một số phần khác sang hệ thống Chế độ xem. Ví dụ: bạn có thể di chuyển RecyclerView trong khi để lại phần còn lại của màn hình trong hệ thống Chế độ xem.

Hoặc ngược lại, hãy dùng Compose làm bố cục bên ngoài và dùng một số chế độ xem hiện tại có thể không dùng được trong Compose như MapView hoặc AdView.

Hoàn tất việc di chuyển

Di chuyển lần lượt toàn bộ mảnh hoặc màn hình sang Compose Đơn giản nhất, nhưng rất chi tiết.

Và trong lớp học lập trình này?

Trong lớp học lập trình này, bạn sẽ thực hiện việc di chuyển dần sang màn hình thông tin chi tiết về cây trồng của Sunflower có mục Compose và Chế độ xem hoạt động cùng nhau. Sau đó, bạn sẽ nắm rõ kiến thức đủ để tiếp tục việc di chuyển nếu muốn.

3. Thiết lập

Lấy mã

Lấy mã cho lớp học lập trình từ GitHub:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp Zip:

Chạy ứng dụng mẫu

Mã bạn vừa tải xuống có chứa mã dành cho tất cả lớp học lập trình Compose hiện có. Để hoàn tất lớp học lập trình này, hãy mở dự án MigrationCodelab trong Android Studio.

Trong lớp học lập trình này, bạn sẽ di chuyển màn hình thông tin chi tiết về cây trồng của Sunflower sang Compose. Có thể mở màn hình chi tiết về cây bằng cách nhấn vào một trong các cây có trong màn hình danh sách cây.

bb6fcf50b2899894.png

Thiết lập dự án

Dự án được xây dựng trong nhiều nhánh git.

  • main là nhánh bạn đã coi qua hoặc tải xuống. Đây là điểm bắt đầu của lớp học lập trình.
  • end có chứa giải pháp cho lớp học lập trình này

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 tốc độ của bạn.

Xuyên suốt lớp học lập trình, bạn sẽ thấy các đoạn mã bạn cần thêm vào dự án. Có lúc, bạn cũng sẽ cần phải xoá mã được đề cập rõ ràng trong các nhận xét trên đoạn mã.

Để nhận nhánh end sử dụng git, hãy dùng lệnh sau:

$ git clone -b end https://github.com/googlecodelabs/android-compose-codelabs

Hoặc tải mã giải pháp từ đây:

Câu hỏi thường gặp

4. Compose trong Sunflower

Compose đã được thêm vào mã bạn tải xuống từ nhánh main. Tuy nhiên, hãy xem những yêu cầu để nó có thể hoạt động.

Nếu mở tệp app/build.gradle (hoặc build.gradle (Module: compose-migration.app)), hãy xem cách tệp này nhập phần phụ thuộc Compose và cho phép Android Studio hoạt động với Compose bằng cách sử dụng cờ buildFeatures { compose true }.

app/build.gradle

android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        ...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion rootProject.composeVersion
    }
}

dependencies {
    ...
    // Compose
    implementation "androidx.compose.runtime:runtime:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation:$rootProject.composeVersion"
    implementation "androidx.compose.foundation:foundation-layout:$rootProject.composeVersion"
    implementation "androidx.compose.material:material:$rootProject.composeVersion"
    implementation "androidx.compose.runtime:runtime-livedata:$rootProject.composeVersion"
    implementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
    implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

Phiên bản của các phần phụ thuộc này được xác định trong tệp build.gradle gốc.

5. Xin chào Compose!

Trong màn hình thông tin chi tiết về cây, chúng ta sẽ di chuyển nội dung mô tả về cây sang Compose, đồng thời giữ nguyên cấu trúc tổng thể của màn hình. Tại đây, bạn sẽ theo dõi chiến lược di chuyển Compose và Chế độ xem hoạt động cùng nhau được đề cập trong phần Lên kế hoạch di chuyển.

Compose cần có Hoạt động lưu trữ hoặc Mảnh để hiển thị giao diện người dùng. Trong Sunflower, vì tất cả màn hình đều sử dụng các mảnh nên bạn sẽ dùng ComposeView: một Chế độ xem Android có thể lưu trữ nội dung trên giao diện người dùng của Compose bằng phương thức setContent.

Xoá mã XML

Hãy bắt đầu di chuyển! Mở fragment_plant_detail.xml và làm như sau:

  1. Chuyển sang Chế độ xem mã
  2. Xoá mã ConstraintLayout và các TextView được lồng bên trong NestedScrollView (lớp học lập trình sẽ so sánh và tham chiếu đến mã XML khi di chuyển từng mục riêng lẻ, vì vậy, bạn sẽ thấy mã có nhận xét sẽ hữu ích)
  3. Hãy thêm một ComposeView lưu trữ mã Compose thay vì với compose_view để làm id chế độ xem

fragment_plant_detail.xml

<androidx.core.widget.NestedScrollView
    android:id="@+id/plant_detail_scrollview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingBottom="@dimen/fab_bottom_padding"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    // Step 2) Comment out ConstraintLayout and its children
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="@dimen/margin_normal">

        <TextView
            android:id="@+id/plant_detail_name"
        ...

    </androidx.constraintlayout.widget.ConstraintLayout>
    // End Step 2) Comment out until here

    // Step 3) Add a ComposeView to host Compose code
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.core.widget.NestedScrollView>

Thêm mã Compose

Lúc này, bạn đã sẵn sàng để bắt đầu di chuyển màn hình thông tin chi tiết về cây trồng đến Compose!

Trong suốt lớp học lập trình, bạn phải thêm mã Compose vào tệp PlantDetailDescription.kt trong thư mục plantdetail. Mở tệp này và xem cách chúng tôi đã có một phần giữ chỗ cho văn bản "Hello Compose!" trong dự án.

plantdetail/PlantDetailDescription.kt

@Composable
fun PlantDetailDescription() {
    Text("Hello Compose")
}

Hãy hiển thị thông tin này trên màn hình bằng cách gọi thành phần kết hợp từ ComposeView đã thêm ở bước trước. Mở plantdetail/PlantDetailFragment.kt.

Vì màn hình đang sử dụng tính năng liên kết dữ liệu, nên bạn có thể truy cập trực tiếp vào composeView và gọi setContent để hiển thị mã Compose trên màn hình. Gọi PlantDetailDescription thành phần kết hợp bên trong MaterialTheme vì Sunflower sử dụng Material Design.

plantdetail/PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            ...
            composeView.setContent {
                // You're in Compose world!
                MaterialTheme {
                    PlantDetailDescription()
                }
            }
        }
        ...
    }
}

Nếu chạy ứng dụng, bạn có thể thấy "Hello Compose!" hiển thị trên màn hình.

66f3525ecf6669e0.png

6. Tạo một Thành phần kết hợp từ XML

Hãy bắt đầu bằng cách di chuyển tên của cây. Chính xác hơn là TextView với mã @+id/plant_detail_name đã xoá trong fragment_plant_detail.xml. Sau đây là mã XML:

<TextView
    android:id="@+id/plant_detail_name"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@{viewModel.plant.name}"
    android:textAppearance="?attr/textAppearanceHeadline5"
    ... />

Hãy xem cách phần này có kiểu textAppearanceHeadline5, có lề ngang là 8.dp và nằm ở giữa màn hình theo chiều ngang. Tuy nhiên, tiêu đề được hiển thị lại quan sát được từ LiveData do PlantDetailViewModel hiển thị (bắt nguồn từ lớp kho lưu trữ).

Khi quan sát thấy LiveData được đề cập sau đó, giả sử chúng ta có sẵn tên và được chuyển dưới dạng thông số vào thành phần kết hợp mới PlantName tạo trong tệp PlantDetailDescription.kt. Thành phần kết hợp này sẽ được gọi từ thành phần kết hợp PlantDetailDescription sau.

PlantDetailDescription.kt

@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName("Apple")
    }
}

Bản xem trước:

d09fe886b98bde91.png

Trong trường hợp:

  • Kiểu của TextMaterialTheme.typography.h5 liên kết tới textAppearanceHeadline5 từ mã XML.
  • Công cụ sửa đổi trang trí Văn bản để điều chỉnh sao cho giống như phiên bản XML:
  • Công cụ sửa đổi fillMaxWidth tương ứng với android:layout_width="match_parent" trong mã XML.
  • padding ngang margin_small là một giá trị từ hệ thống Chế độ xem bằng hàm trợ giúp dimensionResource.
  • wrapContentWidth để căn chỉnh Text theo chiều ngang.

7. ViewModel và LiveData

Bây giờ, hãy kết nối tiêu đề với màn hình. Để làm việc đó, bạn cần tải dữ liệu bằng cách sử dụng PlantDetailViewModel. Do đó, Compose tích hợp với ViewModelLiveData.

ViewModel

Vì một bản sao của PlantDetailViewModel được dùng trong Mảnh, nên chúng ta có thể chuyển nó đó dưới dạng thông số cho PlantDetailDescription.

Mở tệp PlantDetailDescription.kt và thêm thông số PlantDetailViewModel vào PlantDetailDescription:

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    ...
}

Bây giờ, hãy chuyển bản sao của ViewModel khi gọi thành phần kết hợp này từ mảnh:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        ...
        composeView.setContent {
            MaterialTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

LiveData

Bằng cách này, bạn đã có quyền truy cập vào trường LiveData<Plant> của PlantDetailViewModel để lấy tên của cây.

Để quan sát LiveData từ một thành phần kết hợp, hãy dùng hàm LiveData.observeAsState().

Vì các giá trị do LiveData phát hành có thể có giá trị rỗng, nên cần kết hợp việc sử dụng nó ở mức rỗng. Do đó, vì thế và vì mục đích sử dụng lại, bạn nên chia nhỏ mức tiêu thụ LiveData và nghe trong các thành phần kết hợp khác nhau. Do đó, hãy tạo một thành phần kết hợp mới có tên là PlantDetailContent để hiển thị thông tin về Plant.

Như đề cập ở trên, tệp PlantDetailDescription.kt sẽ giống như sau khi thêm chế độ quan sát LiveData.

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM's LiveData<Plant> field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent(it)
    }
}

@Composable
fun PlantDetailContent(plant: Plant) {
    PlantName(plant.name)
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

Có cùng bản xem trước như PlantNamePreviewPlantDetailContent hiện chỉ gọi PlantName.

3e47e682cf518c71.png

Bây giờ, bạn đã kết nối toàn bộ ViewModel để hiển thị tên cây trong Compose. Trong phần tiếp theo, bạn sẽ tạo các phần còn lại của thành phần kết hợp và kết nối chúng với ViewModel theo cách tương tự.

8. Di chuyển mã XML khác

Giờ đây, bạn đã có thể dễ dàng hoàn thành những việc còn thiếu trong giao diện người dùng: thông tin tưới cây và nội dung mô tả cây cối. Làm theo cách tương tự như cách tiếp cận mã XML đã làm trước đây, bạn có thể di chuyển phần còn lại của màn hình.

Mã XML thông tin tưới cây bạn đã xoá trước đó khỏi fragment_plant_detail.xml bao gồm hai chế độ TextView có mã plant_watering_headerplant_watering.

<TextView
    android:id="@+id/plant_watering_header"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginTop="@dimen/margin_normal"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@string/watering_needs_prefix"
    android:textColor="?attr/colorAccent"
    android:textStyle="bold"
    ... />

<TextView
    android:id="@+id/plant_watering"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    app:wateringText="@{viewModel.plant.wateringInterval}"
    .../>

Tương tự như cách bạn đã làm trước đây, hãy tạo một thành phần kết hợp mới có tên là PlantWatering và thêm Text để hiển thị thông tin tưới cây trên màn hình:

PlantDetailDescription.kt

@Composable
private fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        // Same modifier used by both Texts
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(R.dimen.margin_normal)

        Text(
            text = stringResource(R.string.watering_needs_prefix),
            color = MaterialTheme.colors.primaryVariant,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = LocalContext.current.resources.getQuantityString(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}

Bản xem trước:

6f6c17085801a518.png

Một số điều cần lưu ý:

  • Vì khoảng đệm ngang và trang trí căn chỉnh do các thành phần được chia sẻ bởi Text, nên bạn có thể sử dụng lại Công cụ sửa đổi này bằng cách chỉ định công cụ này cho một biến cục bộ (tức là centerWithPaddingModifier). Vì công cụ sửa đổi là đối tượng Kotlin thông thường, nên bạn có thể thực hiện việc này.
  • MaterialTheme của Compose không có kết quả khớp chính xác với colorAccent được sử dụng trong plant_watering_header. Bây giờ, hãy dùng MaterialTheme.colors.primaryVariant bạn sẽ cải thiện trong phần giao diện.

Hãy kết nối tất cả các phần với nhau và cũng gọi PlantWatering từ PlantDetailContent. Mã ConstraintLayout XML chúng ta xoá ở phần đầu có lề 16.dp chúng ta cần đưa vào mã Compose.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/margin_normal">

Trong PlantDetailContent, hãy tạo một Column để hiển thị cả tên và thông tin tưới nước, đồng thời đặt tên đó ở dạng khoảng đệm. Ngoài ra, để màu nền và màu văn bản sử dụng đều phù hợp, hãy thêm một Surface để xử lý điều đó.

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}

Nếu làm mới bản xem trước, bạn sẽ thấy:

56626a7118ce075c.png

9. Chế độ xem trong mã Compose

Bây giờ, hãy cùng di chuyển phần mô tả cây. Mã trong fragment_plant_detail.xml có một TextView với app:renderHtml="@{viewModel.plant.description}" để cho XML biết văn bản nào cần hiển thị trên màn hình. renderHtml là bộ chuyển đổi liên kết có thể tìm thấy trong tệp PlantDetailBindingAdapters.kt. Cách triển khai sử dụng HtmlCompat.fromHtml để đặt văn bản trên TextView!

Tuy nhiên, tính năng Compose hiện không hỗ trợ cho các lớp Spanned cũng như không hiển thị văn bản dưới định dạng HTML. Do đó, chúng ta cần phải sử dụng TextView từ hệ thống Chế độ xem trong mã Compose để bỏ qua giới hạn chế.

Vì Compose chưa thể hiển thị mã HTML, nên bạn sẽ tạo TextView theo phương thức lập trình để thực hiện chính xác việc đó thông qua API AndroidView.

AndroidView lấy View làm thông số và cung cấp cho bạn lệnh gọi lại khi Chế độ xem đã được tăng cường.

Hãy thực hiện việc này bằng cách tạo một thành phần kết hợp PlantDescription mới. Thành phần kết hợp này có thể gọi AndroidView bằng TextView chúng ta vừa nhớ trong một hàm lambda. Trong lệnh gọi lại factory, hãy khởi tạo một TextView nhằm phản ứng với các lượt tương tác HTML bằng cách sử dụng Context. Và trong lệnh gọi lại update, hãy đặt văn bản bằng phần mô tả đã định dạng HTML được ghi nhớ.

PlantDetailDescription.kt

@Composable
private fun PlantDescription(description: String) {
    // Remembers the HTML formatted description. Re-executes on a new description
    val htmlDescription = remember(description) {
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    // Displays the TextView on the screen and updates with the HTML description when inflated
    // Updates to htmlDescription will make AndroidView recompose and update the text
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = {
            it.text = htmlDescription
        }
    )
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MaterialTheme {
        PlantDescription("HTML<br><br>description")
    }
}

Xem trước:

deea1d191e9087b4.png

Lưu ý htmlDescription ghi nhớ mô tả HTML cho description nhất định được chuyển dưới dạng thông số. Nếu thông số description thay đổi, mã htmlDescription bên trong remember sẽ thực thi lại.

Tương tự, lệnh gọi lại cập nhật AndroidView sẽ khởi tạo lại nếu htmlDescription thay đổi. Bất kỳ trạng thái nào được đọc bên trong lệnh gọi lại đều gây ra việc kết hợp lại.

Hãy thêm PlantDescription vào mã tổng hợp PlantDetailContent và thay đổi mã xem trước để hiển thị nội dung mô tả HTML:

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
            PlantDescription(plant.description)
        }
    }
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

Bản xem trước:

7843a8d6c781c244.png

Lúc này, bạn đã di chuyển tất cả nội dung bên trong ConstraintLayout gốc sang Compose. Bạn có thể chạy ứng dụng để kiểm tra xem ứng dụng có hoạt động như mong đợi không.

c7021c18eb8b4d4e.gif

10. Phương thức ViewCompositionStrategy

Theo mặc định, Compose sẽ xử lý Bản sáng tác bất cứ khi nào ComposeView tách khỏi cửa sổ. Điều này là ngoài mong muốn khi ComposeView được sử dụng trong các mảnh vì nhiều lý do:

  • Bản sáng tác phải tuân theo vòng đời xem của mảnh đối với loại View Giao diện người dùng Compose để lưu trạng thái, và
  • để giữ cho các thành phần trên giao diện người dùng Compose trên màn hình khi quá trình chuyển đổi hoặc chuyển đổi cửa sổ xảy ra. Trong quá trình chuyển đổi, ComposeView vẫn hiển thị ngay cả khi đã tách khỏi cửa sổ.

Bạn có thể gọi theo cách thủ công AbstractComposeView.disposeComposition để vứt bỏ Bản sáng tác theo cách thủ công. Ngoài ra, để tự động xử lý Bản sáng tác khi chúng không cần nữa, hãy đặt một cách khác hoặc tạo chiến lược của riêng bạn bằng cách gọi phương thức setViewCompositionStrategy.

Sử dụng chiến lược DisposeOnViewTreeLifecycleDestroyed để loại bỏ Bản sáng tác khi LifecycleOwner của mảnh bị huỷ bỏ.

Dưới dạngPlantDetailFragment có chuyển đổi nhập và thoát (kiểm tranav_garden.xml để biết thêm thông tin), chúng tôi sẽ sử dụng loại View trong Compose sau, chúng ta cần đảm bảoComposeView sử dụngDisposeOnViewTreeLifecycleDestroyed chiến lược. Tuy nhiên, phương thức hay là luôn đặt chiến lược này khi sử dụng ComposeView trong các mảnh.

plantdetail/PlantDetailFragment.kt

import androidx.compose.ui.platform.ViewCompositionStrategy
...

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            ...
            composeView.apply {
                // Dispose the Composition when the view's LifecycleOwner
                // is destroyed
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                setContent {
                    MaterialTheme {
                        PlantDetailDescription(plantDetailViewModel)
                    }
                }
            }
        }
        ...
    }
}

11. Giao diện tương tác

Chúng ta đã di chuyển nội dung văn bản của thông tin chi tiết về cây sang Compose. Tuy nhiên, bạn có thể nhận thấy Compose không sử dụng màu giao diện phù hợp. Nó sử dụng màu tím cho tên cây trong khi cần phải sử dụng màu xanh lục.

Ở giai đoạn đầu của di chuyển này, bạn có thể muốn hướng Compose theo giao diện có sẵn trong hệ thống Chế độ xem thay vì viết lại giao diện Material của riêng bạn trong Compose từ lúc ban đầu. Các giao diện Material hoạt động hoàn hảo với tất cả các thành phần Material Design đi kèm với Compose.

Để sử dụng lại giao diện Thành phần Material Design (MDC) của hệ thống Chế độ xem trong Compose, bạn có thể sử dụng compose-theme-adapter. Hàm MdcTheme sẽ tự động đọc giao diện MDC của ngữ cảnh máy chủ và chuyển các giao diện này sang MaterialThemecho cả giao diện sáng lẫn tối. Mặc dù bạn chỉ cần màu sắc giao diện cho lớp học lập trình này, thư viện cũng có thể đọc các hình dạng và kiểu chữ của hệ thống Chế độ xem.

Thư viện đã được đưa vào tệp app/build.gradle như sau:

...
dependencies {
    ...
    implementation "com.google.android.material:compose-theme-adapter:$rootProject.composeVersion"
    ...
}

Để sử dụng thuộc tính này, hãy thay thế MaterialTheme bằng MdcTheme. Ví dụ: trong PlantDetailFragment:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    composeView.apply {
        ...
        setContent {
            MdcTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

Và tất cả thành phần kết hợp xem trước trong tệp PlantDetailDescription.kt:

PlantDetailDescription.kt

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

@Preview
@Composable
private fun PlantNamePreview() {
    MdcTheme {
        PlantName("Apple")
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MdcTheme {
        PlantWatering(7)
    }
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MdcTheme {
        PlantDescription("HTML<br><br>description")
    }
}

Như bạn có thể thấy trong bản xem trước, MdcTheme đang chọn màu từ giao diện trong tệp styles.xml.

886d7eaea611f4eb.png

Bạn cũng có thể xem trước giao diện người dùng trong giao diện tối bằng cách tạo một hàm mới và chuyển Configuration.UI_MODE_NIGHT_YES đến uiMode của bản xem trước:

import android.content.res.Configuration
...

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

Bản xem trước:

cfe11c109ff19eeb.png

Nếu bạn chạy ứng dụng, nó hoạt động giống hệt như trước khi di chuyển ở cả giao diện sáng lẫn tối:

c99216fc77699dd7.gif

12. Thử nghiệm

Sau khi di chuyển các phần của màn hình thông tin chi tiết về cây sang Compose, việc kiểm tra rất quan trọng để đảm bảo bạn không làm hỏng bất kỳ nội dung nào.

Trong Sunflower, PlantDetailFragmentTest nằm trong thư mục androidTest sẽ kiểm tra một số chức năng của ứng dụng. Hãy mở tệp và xem mã hiện tại:

  • testPlantName kiểm tra tên của cây trên màn hình
  • testShareTextIntent kiểm tra để đảm bảo ý định phù hợp được kích hoạt sau khi nhấn vào nút chia sẻ

Khi một hoạt động hoặc mảnh sử dụng Compose, thay vì sử dụng ActivityScenarioRule, bạn cần phải sử dụng createAndroidComposeRule tích hợp ActivityScenarioRule với ComposeTestRule để có thể kiểm tra mã Compose.

Trong PlantDetailFragmentTest, hãy thay thế việc sử dụng ActivityScenarioRule bằng createAndroidComposeRule. Khi cần quy tắc hoạt động để định cấu hình thử nghiệm, hãy dùng thuộc tính activityRule từ createAndroidComposeRule như sau:

@RunWith(AndroidJUnit4::class)
class PlantDetailFragmentTest {

    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<GardenActivity>()

    ...

    @Before
    fun jumpToPlantDetailFragment() {
        populateDatabase()

        composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
            activity = gardenActivity

            val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }
    }

    ...
}

Nếu bạn chạy thử nghiệm, testPlantName sẽ không thành công! testPlantName kiểm tra để tìm một TextView hiển thị trên màn hình. Tuy nhiên, bạn đã di chuyển phần đó của giao diện người dùng sang Compose. Do đó, bạn cần phải sử dụng tính năng Xác nhận Compose:

@Test
fun testPlantName() {
    composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}

Nếu chạy thử, bạn sẽ thấy tất cả các thử nghiệm này đạt.

dd59138fac1740e4.png

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

Nhánh compose của dự án Sunflower github gốc sẽ di chuyển hoàn toàn màn hình thông tin chi tiết về cây sang Compose. Ngoài những việc đã hoàn thành trong lớp học lập trình này, bạn còn cần mô phỏng hành vi của CollapsingThanhLayout. Việc này bao gồm:

  • Tải hình ảnh bằng Compose
  • Ảnh động
  • Xử lý phương diện tốt hơn
  • Và nhiều kiến thức khác!

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.

Tài liệu đọc thêm