Bermigrasi ke Jetpack Compose

1. Pengantar

Sistem Compose dan View dapat bekerja berdampingan.

Dalam codelab ini, Anda akan memigrasikan bagian layar detail tanaman Sunflower ke Compose. Kami membuat salinan project agar Anda dapat mencoba memigrasikan aplikasi yang realistis ke Compose.

Di akhir codelab, Anda dapat melanjutkan migrasi dan mengonversi layar Sunflower lainnya jika ingin.

Untuk dukungan selengkapnya saat Anda mempelajari codelab ini, lihat kode berikut:

Yang akan Anda pelajari

Dalam codelab ini, Anda akan mempelajari:

  • Berbagai jalur migrasi yang dapat Anda ikuti
  • Cara memigrasikan aplikasi secara bertahap ke Compose
  • Cara menambahkan Compose ke layar yang ada dan dibuat menggunakan View
  • Cara menggunakan View dari dalam Compose
  • Cara membuat tema di Compose
  • Cara menguji layar campuran yang ditulis dalam View dan Compose

Prasyarat

Yang akan Anda butuhkan

2. Strategi migrasi

Jetpack Compose dirancang dengan interoperabilitas View sejak awal. Untuk bermigrasi ke Compose, sebaiknya lakukan migrasi inkremental tempat Compose dan View berada berdampingan dalam codebase Anda sampai aplikasi sepenuhnya berada di Compose.

Strategi migrasi yang direkomendasikan adalah:

  1. Membangun layar baru dengan Compose
  2. Saat Anda membangun fitur, identifikasi elemen yang dapat digunakan kembali dan mulai buat library komponen UI umum.
  3. Mengganti fitur yang ada di satu layar dalam satu waktu

Membangun layar baru dengan Compose

Menggunakan Compose untuk membangun fitur baru yang mencakup seluruh layar adalah cara terbaik untuk mendorong penggunaan Compose Anda. Dengan strategi ini, Anda dapat menambahkan fitur dan mendapatkan manfaat Compose sambil tetap memenuhi kebutuhan bisnis perusahaan Anda.

Fitur baru dapat mencakup seluruh layar, yang dalam hal ini seluruh layar akan berada di Compose. Jika Anda menggunakan navigasi berbasis Fragmen, artinya Anda akan membuat Fragmen baru dan memiliki kontennya di Compose.

Anda juga dapat memperkenalkan fitur baru di layar yang sudah ada. Dalam hal ini, View dan Compose akan berdampingan di layar yang sama. Misalnya, fitur yang Anda tambahkan adalah jenis tampilan baru di RecyclerView. Dalam hal ini, jenis tampilan baru akan berada di Compose tanpa mengubah item lainnya.

Membuat library komponen UI umum

Saat membuat fitur dengan Compose, Anda akan segera menyadari bahwa Anda akhirnya membuat library komponen. Sebaiknya Anda mengidentifikasi komponen yang dapat digunakan kembali untuk mempromosikan penggunaan kembali di seluruh aplikasi Anda agar komponen yang dibagikan memiliki satu sumber tepercaya. Fitur baru yang Anda buat kemudian dapat bergantung pada library ini.

Mengganti fitur yang ada dengan Compose

Selain membuat fitur baru, Anda juga perlu memigrasikan secara bertahap fitur yang ada di aplikasi ke Compose. Anda dapat menggunakan cara Anda sendiri untuk menangani hal ini, tetapi berikut ini beberapa alternatif yang baik:

  1. Layar sederhana - layar sederhana di aplikasi Anda dengan sedikit elemen UI dan dinamika seperti layar sambutan, layar konfirmasi, atau layar setelan. Bermigrasi ke Compose adalah alternatif yang baik karena dapat dilakukan dengan beberapa baris kode.
  2. Layar campuran View dan Compose - layar yang sudah berisi sedikit kode Compose adalah alternatif bagus lainnya karena Anda dapat terus memigrasikan elemen di layar tersebut bagian demi bagian. Jika Anda memiliki layar yang hanya memiliki sub-hierarki di Compose, Anda dapat terus memigrasikan bagian-bagian hierarki lainnya hingga seluruh UI berada di Compose. Hal ini disebut pendekatan bottom-up untuk migrasi.

Pendekatan bottom-up untuk memigrasikan campuran UI View dan Compose ke Compose

Pendekatan dalam Codelab ini

Dalam codelab ini, Anda akan melakukan migrasi inkremental layar detail tanaman Sunflower ke Compose dengan kerja sama antara Compose dan View. Setelah itu, Anda akan memiliki cukup pengetahuan untuk melanjutkan migrasi jika Anda ingin.

3. Mempersiapkan

Mendapatkan kode

Dapatkan kode codelab dari GitHub:

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

Atau, Anda dapat mendownload repositori sebagai file Zip:

Menjalankan aplikasi contoh

Kode yang baru saja Anda download berisi kode untuk semua codelab Compose yang tersedia. Untuk menyelesaikan codelab ini, buka project MigrationCodelab di dalam Android Studio.

Dalam codelab ini, Anda akan memigrasikan layar detail tanaman Sunflower ke Compose. Anda dapat membuka layar detail tanaman dengan mengetuk salah satu tanaman yang tersedia di layar daftar tanaman.

bb6fcf50b2899894.png

Penyiapan project

Project ini dibuat di beberapa cabang git:

  • Cabang main adalah titik awal codelab.
  • end berisi solusi untuk codelab ini

Sebaiknya Anda memulai dengan kode di cabang main dan mengikuti codelab langkah demi langkah sesuai kemampuan Anda.

Selama codelab, Anda akan melihat cuplikan kode yang harus ditambahkan ke project. Di beberapa tempat, Anda juga harus menghapus kode yang disebutkan secara eksplisit dalam komentar pada cuplikan kode.

Untuk mendapatkan cabang end menggunakan git, cd ke direktori project MigrationCodelab diikuti dengan penggunaan perintah:

$ git checkout end

Atau download kode solusi dari sini:

Pertanyaan umum (FAQ)

4. Compose di Sunflower

Compose telah ditambahkan ke kode yang Anda download dari cabang main. Namun, mari kita lihat apa yang diperlukan agar berfungsi.

Jika Anda membuka file build.gradle level aplikasi, lihat cara file tersebut mengimpor dependensi Compose dan memungkinkan Android Studio berfungsi dengan Compose menggunakan tanda buildFeatures { compose true }.

app/build.gradle

android {
    //...
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        //...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.3.2'
    }
}

dependencies {
    //...
    // Compose
    def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
    implementation(composeBom)
    androidTestImplementation(composeBom)

    implementation "androidx.compose.runtime:runtime"
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.foundation:foundation"
    implementation "androidx.compose.foundation:foundation-layout"
    implementation "androidx.compose.material3:material3"
    implementation "androidx.compose.runtime:runtime-livedata"
    implementation "androidx.compose.ui:ui-tooling"
    //...
}

Versi dependensi tersebut ditetapkan dalam file build.gradle level project.

5. Halo Compose!

Di layar detail tanaman, kita akan memigrasikan deskripsi tanaman ke Compose tanpa mengubah keseluruhan struktur layar.

Compose memerlukan Activity atau Fragment host untuk merender UI. Di Sunflower, karena semua layar menggunakan fragmen, Anda akan menggunakan ComposeView: Android View yang dapat menghosting konten UI Compose menggunakan metode setContent.

Menghapus kode XML

Mari kita mulai proses migrasi! Buka fragment_plant_detail.xml, lalu lakukan hal berikut ini:

  1. Beralih ke Tampilan kode
  2. Hapus kode ConstraintLayout dan TextView bertingkat 4 di dalam NestedScrollView (codelab akan membandingkan dan mereferensikan kode XML saat memigrasikan setiap item, menjadikan kode sebagai komentar akan berguna)
  3. Tambahkan ComposeView yang akan menghosting kode Compose dengan compose_view sebagai ID tampilan

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>

Menambahkan kode Compose

Pada tahap ini, Anda siap untuk mulai memigrasikan layar detail tanaman ke Compose!

Dalam codelab ini, Anda akan menambahkan kode Compose ke file PlantDetailDescription.kt di folder plantdetail. Buka dan lihat bagaimana teks "Hello Compose" placeholder sudah tersedia dalam project.

PlantDetailDescription.kt

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

Mari kita tampilkan ini di layar dengan memanggil composable ini dari ComposeView yang kita tambahkan pada langkah sebelumnya. Buka PlantDetailFragment.kt.

Karena layar menggunakan data binding, Anda dapat langsung mengakses composeView dan memanggil setContent untuk menampilkan kode Compose di layar. Panggil composable PlantDetailDescription di dalam MaterialTheme karena Sunflower menggunakan desain material.

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()
                }
            }
        }
        // ...
    }
}

Jika menjalankan aplikasi, Anda akan melihat "Hello Compose" ditampilkan di layar.

66f3525ecf6669e0.png

6. Membuat Composable dari XML

Mari mulai dengan memigrasikan nama tanaman. Lebih tepatnya, TextView dengan ID @+id/plant_detail_name yang Anda hapus di fragment_plant_detail.xml. Berikut adalah kode 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"
    ... />

Lihat bagaimana kode tersebut memiliki gaya textAppearanceHeadline5, memiliki margin horizontal 8.dp, dan dipusatkan secara horizontal di layar. Namun, judul yang akan ditampilkan diamati dari LiveData yang diekspos oleh PlantDetailViewModel yang berasal dari lapisan repositori.

Karena mengamati LiveData akan dibahas nanti, anggaplah kita memiliki nama yang tersedia dan diteruskan sebagai parameter ke composable PlantName baru yang kita buat di file PlantDetailDescription.kt. Composable ini nantinya akan dipanggil dari composable PlantDetailDescription.

PlantDetailDescription.kt

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

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

Dengan pratinjau:

d09fe886b98bde91.png

Dalam hal ini:

  • Gaya Text adalah MaterialTheme.typography.headlineSmall yang mirip dengan textAppearanceHeadline5 dari kode XML.
  • Pengubah mendekorasi Teks agar terlihat seperti versi XML:
  • Pengubah fillMaxWidth digunakan sehingga menempati jumlah lebar maksimum yang tersedia. Pengubah ini sesuai dengan nilai match_parent dari atribut layout_width dalam kode XML.
  • Pengubah padding digunakan sehingga nilai padding horizontal margin_small diterapkan. Ini sama dengan deklarasi marginStart dan marginEnd dalam XML. Nilai margin_small juga merupakan resource dimensi yang ada dan diambil menggunakan fungsi bantuan dimensionResource.
  • Pengubah wrapContentWidth digunakan untuk meratakan teks sehingga berada di tengah secara horizontal. Ini mirip dengan memiliki gravity dari center_horizontal dalam XML.

7. ViewModels dan LiveData

Sekarang, mari kita hubungkan judul ke layar. Untuk melakukannya, Anda perlu memuat data menggunakan PlantDetailViewModel. Untuk itu, Compose dilengkapi dengan integrasi untuk ViewModel dan LiveData.

ViewModels

Sebagai instance PlantDetailViewModel yang digunakan dalam Fragment, kita dapat meneruskannya sebagai parameter ke PlantDetailDescription dan selesai.

Buka file PlantDetailDescription.kt dan tambahkan parameter PlantDetailViewModel ke PlantDetailDescription:

PlantDetailDescription.kt

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

Sekarang, teruskan instance ViewModel saat memanggil composable ini dari fragmen:

PlantDetailFragment.kt

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

LiveData

Dengan ini, Anda sudah memiliki akses ke kolom LiveData<Plant> dari PlantDetailViewModel untuk mendapatkan nama tanaman.

Untuk mengamati LiveData dari composable, gunakan fungsi LiveData.observeAsState().

Karena nilai yang dimunculkan oleh LiveData bisa null, Anda harus menggabungkan penggunaannya dalam pemeriksaan null. Karena hal itu, dan agar dapat digunakan kembali, sebaiknya pisahkan pemakaian LiveData dan pemrosesannya dalam composable yang berbeda. Jadi, mari buat composable baru bernama PlantDetailContent yang akan menampilkan informasi Plant.

Dengan update ini, file PlantDetailDescription.kt sekarang akan terlihat seperti ini:

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)
    }
}

PlantNamePreview harus mencerminkan perubahan kita tanpa harus mengupdatenya secara langsung karena PlantDetailContent hanya memanggil PlantName:

3e47e682cf518c71.png

Sekarang, Anda telah menghubungkan ViewModel sehingga nama tanaman ditampilkan di Compose. Dalam beberapa bagian berikutnya, Anda akan membuat composable lainnya dan menghubungkannya ke ViewModel dengan cara yang sama.

8. Migrasi kode XML lainnya

Sekarang, lebih mudah untuk menyelesaikan apa yang kurang di UI kita: info penyiraman dan deskripsi tanaman. Dengan mengikuti pendekatan serupa seperti sebelumnya, Anda dapat memigrasikan bagian layar lainnya.

Kode XML info penyiraman yang dihapus sebelumnya dari fragment_plant_detail.xml terdiri dari dua TextView dengan ID plant_watering_header dan plant_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}"
    .../>

Sama seperti yang Anda lakukan sebelumnya, buat composable baru bernama PlantWatering, lalu tambahkan composable Text untuk menampilkan informasi penyiraman di layar:

PlantDetailDescription.kt

@OptIn(ExperimentalComposeUiApi::class)
@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.colorScheme.primaryContainer,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = pluralStringResource(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

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

Dengan pratinjau:

6f6c17085801a518.png

Beberapa hal yang perlu diperhatikan:

  • Karena padding horizontal dan dekorasi perataan digunakan bersama oleh composable Text, Anda dapat menggunakan kembali Pengubah dengan menetapkannya ke variabel lokal (yaitu centerWithPaddingModifier). Anda dapat melakukannya karena pengubah adalah objek Kotlin reguler.
  • MaterialTheme Compose tidak memiliki kecocokan yang sama persis dengan colorAccent yang digunakan di plant_watering_header. Untuk saat ini, mari gunakan MaterialTheme.colorScheme.primaryContainer yang akan Anda tingkatkan di bagian tema interop.
  • Di Compose 1. 2.1, penggunaan pluralStringResource mengharuskan Anda memilih penggunaan ExperimentalComposeUiApi. Pada versi Compose mendatang, hal ini mungkin tidak diperlukan lagi.

Mari hubungkan semua bagian dan memanggil PlantWatering dari PlantDetailContent juga. Kode XML ConstraintLayout yang kita hapus di awal memiliki margin 16.dp yang perlu disertakan dalam kode Compose.

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

Di PlantDetailContent, buat Column untuk menampilkan nama dan info penyiraman sekaligus dan membuatnya sebagai padding. Selain itu, agar warna latar belakang dan warna teks yang digunakan sesuai, tambahkan Surface yang akan menanganinya.

PlantDetailDescription.kt

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

Jika memuat ulang pratinjau, Anda akan melihat:

56626a7118ce075c.png

9. View di kode Compose

Sekarang, mari migrasikan deskripsi tanaman. Kode dalam fragment_plant_detail.xml memiliki TextView dengan app:renderHtml="@{viewModel.plant.description}" untuk memberi tahu XML teks apa yang ditampilkan di layar. renderHtml adalah adaptor binding yang dapat Anda temukan di file PlantDetailBindingAdapters.kt. Implementasi menggunakan HtmlCompat.fromHtml untuk menetapkan teks di TextView!

Namun, saat ini Compose tidak memiliki dukungan untuk class Spanned atau menampilkan teks berformat HTML. Dengan demikian, kita perlu menggunakan TextView dari sistem View dalam kode Compose untuk mengabaikan batasan ini.

Karena Compose belum dapat merender kode HTML, Anda akan membuat TextView secara terprogram untuk melakukan hal tersebut menggunakan AndroidView API.

AndroidView memungkinkan Anda membuat View dalam lambda factory-nya. Kode ini juga menyediakan lambda update yang dipanggil saat View telah di-inflate dan pada rekomposisi berikutnya.

Mari lakukan ini dengan membuat composable PlantDescription baru. Composable ini memanggil AndroidView yang membuat TextView dalam lambda factory-nya. Di lambda factory, lakukan inisialisasi TextView yang menampilkan teks berformat HTML, kemudian tetapkan movementMethod ke instance LinkMovementMethod. Terakhir, di lambda update, tetapkan teks TextView menjadi htmlDescription.

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")
    }
}

Pratinjau:

deea1d191e9087b4.png

Perhatikan bahwa htmlDescription mengingat deskripsi HTML untuk description yang diberikan yang diteruskan sebagai parameter. Jika parameter description berubah, kode htmlDescription di dalam remember akan dijalankan kembali.

Akibatnya, callback update AndroidView akan direkomposisi jika htmlDescription berubah. Setiap status yang dibaca di dalam lambda update menyebabkan rekomposisi.

Mari tambahkan PlantDescription ke composable PlantDetailContent dan ubah kode pratinjau untuk menampilkan deskripsi HTML juga:

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)
    }
}

Dengan pratinjau:

7843a8d6c781c244.png

Pada tahap ini, Anda telah memigrasikan semua konten dalam ConstraintLayout asli ke Compose. Anda dapat menjalankan aplikasi untuk memastikan aplikasi bekerja seperti yang diharapkan.

c7021c18eb8b4d4e.gif

10. ViewCompositionStrategy

Compose akan menghapus Komposisi setiap kali ComposeView dilepas dari jendela. Hal ini tidak diinginkan jika ComposeView digunakan dalam fragmen karena dua alasan:

  • Komposisi harus mengikuti siklus proses tampilan fragmen untuk jenis View UI Compose guna menyimpan status.
  • Saat transisi terjadi, ComposeView yang mendasarinya akan berada dalam status terpisah. Namun, elemen UI Compose masih akan terlihat selama transisi ini.

Untuk memodifikasi perilaku ini, panggil setViewCompositionStrategy dengan ViewCompositionStrategy yang sesuai sehingga mengikuti siklus proses tampilan fragmen. Secara khusus, Anda dapat menggunakan strategi DisposeOnViewTreeLifecycleDestroyed untuk membuang Komposisi saat LifecycleOwner fragmen dihancurkan.

Karena PlantDetailFragment memiliki transisi masuk dan keluar (lihat nav_garden.xml untuk mengetahui info selengkapnya), dan kita akan menggunakan jenis View di dalam Compose nanti, kita perlu memastikan ComposeView menggunakan strategi DisposeOnViewTreeLifecycleDestroyed. Meskipun demikian, praktik terbaik-nya adalah selalu menetapkan strategi ini saat menggunakan ComposeView dalam fragmen.

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. Penerapan tema material

Kita memiliki konten teks detail tanaman yang dimigrasikan ke Compose. Namun, Anda mungkin melihat bahwa Compose tidak menggunakan warna tema yang tepat. Compose menggunakan warna ungu untuk nama tanaman, yang seharusnya berwarna hijau.

Untuk menggunakan warna tema yang benar, Anda harus menyesuaikan MaterialTheme dengan menentukan tema Anda sendiri dan memberikan warna tema.

Menyesuaikan MaterialTheme

Untuk membuat tema Anda sendiri, buka file Theme.kt pada paket theme. Theme.kt menentukan composable yang disebut SunflowerTheme, yang menerima lambda konten dan meneruskannya ke MaterialTheme.

Belum ada yang terjadi—Anda masih harus menyesuaikannya.

Theme.kt

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

@Composable
fun SunflowerTheme(
    content: @Composable () -> Unit
) {
    MaterialTheme(content = content)
}

MaterialTheme memungkinkan Anda menyesuaikan warna, tipografi, dan bentuknya. Untuk saat ini, lanjutkan dan sesuaikan warna dengan memberikan warna yang sama pada tema View Sunflower. SunflowerTheme juga dapat menerima parameter boolean yang disebut darkTheme yang akan ditetapkan secara default ke true jika sistem dalam mode gelap. Jika tidak, false. Dengan menggunakan parameter ini, kita dapat meneruskan nilai warna yang tepat ke MaterialTheme agar cocok dengan tema sistem yang saat ini ditetapkan.

Theme.kt

@Composable
fun SunflowerTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val lightColors  = lightColorScheme(
        primary = colorResource(id = R.color.sunflower_green_500),
        primaryContainer = colorResource(id = R.color.sunflower_green_700),
        secondary = colorResource(id = R.color.sunflower_yellow_500),
        background = colorResource(id = R.color.sunflower_green_500),
        onPrimary = colorResource(id = R.color.sunflower_black),
        onSecondary = colorResource(id = R.color.sunflower_black),
    )
    val darkColors  = darkColorScheme(
        primary = colorResource(id = R.color.sunflower_green_100),
        primaryContainer = colorResource(id = R.color.sunflower_green_200),
        secondary = colorResource(id = R.color.sunflower_yellow_300),
        onPrimary = colorResource(id = R.color.sunflower_black),
        onSecondary = colorResource(id = R.color.sunflower_black),
        onBackground = colorResource(id = R.color.sunflower_black),
        surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
        onSurface = colorResource(id = R.color.sunflower_white),
    )
    val colors = if (darkTheme) darkColors else lightColors
    MaterialTheme(
        colorScheme = colors,
        content = content
    )
}

Untuk menggunakannya, ganti penggunaan MaterialTheme untuk SunflowerTheme. Misalnya, dalam PlantDetailFragment:

PlantDetailFragment.kt

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

Dan semua composable pratinjau dalam file PlantDetailDescription.kt:

PlantDetailDescription.kt

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

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

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

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

Seperti yang dapat Anda lihat di pratinjau, warna kini akan sesuai dengan warna tema Sunflower.

886d7eaea611f4eb.png

Anda juga dapat melihat pratinjau UI dalam tema gelap dengan membuat fungsi baru dan meneruskan Configuration.UI_MODE_NIGHT_YES ke uiMode dari pratinjau:

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, "")
    SunflowerTheme {
        PlantDetailContent(plant)
    }
}

Dengan pratinjau:

cfe11c109ff19eeb.png

Jika Anda menjalankan aplikasi, aplikasi akan berperilaku sama persis seperti sebelum migrasi dalam tema terang dan gelap:

438d2dd9f8acac39.gif

12. Pengujian

Setelah memigrasikan bagian layar detail tanaman ke Compose, pengujian sangat penting dilakukan untuk memastikan Anda tidak merusak apa pun.

Di Sunflower, PlantDetailFragmentTest yang terletak di folder androidTest menguji beberapa fungsi aplikasi. Buka file dan lihat kode saat ini:

  • testPlantName memeriksa nama tanaman di layar
  • testShareTextIntent memeriksa apakah intent yang benar dipicu setelah mengetuk tombol bagikan

Saat aktivitas atau fragmen menggunakan compose, daripada menggunakan ActivityScenarioRule, Anda harus menggunakan createAndroidComposeRule yang mengintegrasikan ActivityScenarioRule dengan ComposeTestRule yang memungkinkan Anda menguji kode Compose.

Di PlantDetailFragmentTest, ganti ActivityScenarioRule penggunaan dengan createAndroidComposeRule. Saat aturan aktivitas diperlukan untuk mengonfigurasi pengujian, gunakan atribut activityRule dari createAndroidComposeRule sebagai berikut:

@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)
        }
    }

    ...
}

Jika Anda menjalankan pengujian, testPlantName akan gagal. testPlantName memeriksa apakah TextView berada di layar. Namun, Anda telah memigrasikan bagian UI tersebut ke Compose. Dengan demikian, Anda perlu menggunakan pernyataan Compose:

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

Jika Anda menjalankan pengujian, Anda akan melihat semuanya lolos.

dd59138fac1740e4.png

13. Selamat

Selamat, Anda berhasil menyelesaikan codelab ini.

Cabang compose dari project github Sunflower asli memigrasikan layar detail tanaman sepenuhnya ke Compose. Selain dari yang telah Anda kerjakan di codelab ini, codelab juga menyimulasikan perilaku CollapsingToolbarLayout. Hal ini meliputi:

  • Memuat gambar dengan Compose
  • Animasi
  • Penanganan dimensi yang lebih baik
  • Dan banyak lagi!

Apa selanjutnya?

Lihat codelab lain di jalur Compose:

Bacaan lebih lanjut