Mengintegrasi Compose dengan UI yang sudah ada

Jika memiliki aplikasi dengan UI berbasis View, Anda mungkin tidak ingin menulis ulang seluruh UI-nya sekaligus. Halaman ini akan membantu Anda menambahkan elemen Compose baru ke UI yang sudah ada.

Memigrasikan UI bersama

Jika bermigrasi secara bertahap ke Compose, Anda mungkin perlu menggunakan elemen UI bersama di sistem Compose dan View. Misalnya, jika aplikasi Anda memiliki komponen CallToActionButton khusus, Anda mungkin harus menggunakannya di layar Compose dan juga layar berbasis View.

Di Compose, elemen UI bersama akan menjadi komponen dan dapat digunakan kembali di seluruh aplikasi, apa pun elemen yang telah diberi gaya menggunakan XML atau menjadi tampilan khusus. Misalnya, Anda akan membuat komponen CallToActionButton untuk komponen Button pesan ajakan (CTA) khusus.

Untuk menggunakan komponen di layar berbasis View, Anda harus membuat wrapper tampilan khusus yang diperluas dari AbstractComposeView. Pada komponen Content telah yang diganti, tempatkan fungsi yang sudah Anda buat dalam tema Compose seperti yang ditunjukkan pada contoh di bawah:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

Perhatikan bahwa parameter yang dapat disusun akan menjadi variabel yang dapat diubah di dalam tampilan khusus. Ini akan membuat tampilan CallToActionViewButton khusus menjadi inflatable dan dapat digunakan, misalnya View Binding, seperti tampilan tradisional. Lihat contoh di bawah:

class ExampleActivity : Activity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.something)
            onClick = { /* Do something */ }
        }
    }
}

Jika komponen khusus berisi status yang dapat berubah, lihat Sumber status kebenaran.

Tema

Mengikuti Desain Material, menggunakan library Komponen Desain Material untuk Android (MDC) adalah cara yang disarankan untuk tema aplikasi Android. Seperti yang tercakup dalam dokumentasi Compose Theming, Compose menerapkan konsep ini dengan komponen MaterialTheme.

Saat membuat layar baru di Compose, Anda harus memastikan bahwa Anda telah menerapkan MaterialTheme sebelum komponen yang membuat UI dari library komponen material. Komponen material (Button, Text, dll.) bergantung pada MaterialTheme yang ada dan perilakunya tidak ditentukan tanpanya.

Semua sampel Jetpack Compose menggunakan tema Compose khusus yang dibuat berdasarkan MaterialTheme.

Beberapa sumber kebenaran

Aplikasi yang ada cenderung memiliki tema dan gaya visual yang banyak untuk ditampilkan. Saat memperkenalkan Compose di aplikasi yang ada, Anda harus memigrasikan tema untuk menggunakan MaterialTheme untuk layar Compose apa pun. Ini berarti tema aplikasi Anda akan memiliki 2 sumber kebenaran: tema berbasis View dan tema Compose. Setiap perubahan pada gaya visual Anda harus dilakukan di beberapa tempat.

Jika rencana Anda adalah untuk memigrasikan aplikasi secara total ke Compose, Anda harus membuat versi Compose dari tema yang sudah ada. Masalahnya adalah, semakin awal Anda membuat tema Compose, semakin banyak pemeliharaan yang harus dilakukan selama pengembangan.

Adaptor Tema Compose MDC

Jika menggunakan library MDC di aplikasi Android, libraryAdaptor Tema Compose MDC memungkinkan Anda menggunakan kembali tema warna, tipografi, danbentuk dari tema berbasis View yang ada dalam komponen dengan mudah:

import com.google.android.material.composethemeadapter.MdcTheme

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

        setContent {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Lihat dokumentasi library MDC untuk mengetahui informasi selengkapnya.

Adaptor Tema Compose AppCompat

Dengan library Adaptor Tema Compose AppCompat, Anda dapat menggunakan kembali tema XML AppCompat dengan mudah untuk tema di Jetpack Compose. Ini akan membuat MaterialTheme dengan nilai warna dan tipografi dari tema konteks.

import com.google.accompanist.appcompattheme.AppCompatTheme

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

        setContent {
            AppCompatTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Gaya komponen default

Baik library MDC maupun Adaptor Tema Compose AppCompat Compose tidak membaca gaya widget default yang ditentukan oleh tema. Ini karena Compose tidak memiliki konsep komponen default.

Baca gaya komponen dan sistem desain khusus selengkapnya di dokumentasi Tema.

Overlay Tema di Compose

Saat memigrasikan layar berbasis View ke Compose, perhatikan penggunaan atribut android:theme. Sepertinya Anda memerlukan MaterialTheme baru di bagian pohon UI Compose.

Baca selengkapnya di Panduan tema.

WindowInset dan Animasi IME

Anda dapat menangani WindowInsets menggunakan library accompanist-insets yang menyediakan komponen dan pengubah untuk menanganinya dalam tata letak Anda, serta dukungan untuk Animasi IME.

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

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                ProvideWindowInsets {
                    MyScreen()
                }
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding(), // Move it out from under the nav bar
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

Animasi yang menampilkan elemen UI yang menggulir ke atas dan ke bawah untuk memberi jalan bagi keyboard

Gambar 2. Animasi IME menggunakan library accompanist-insets.

Lihat dokumentasi library accompanist-insets untuk mengetahui informasi selengkapnya.

Memprioritaskan status pemisahan dari presentasi

Biasanya, View adalah stateful. View mengelola kolom yang menjelaskan item yang akan ditampilkan, selain cara menampilkannya. Saat Anda mengonversi View ke Compose, lihat cara memisahkan data yang dirender untuk mencapai aliran data searah, seperti yang dijelaskan lebih lanjut dalam pengangkatan status.

Misalnya, View memiliki properti visibility yang menjelaskan apakah properti tersebut terlihat, tidak terlihat, atau hilang. Ini adalah properti yang melekat pada View. Meskipun bagian kode lainnya dapat mengubah visibilitas View, hanya View yang benar-benar mengetahui visibilitasnya saat ini. Logika untuk memastikan bahwa View dapat terlihat cenderung rentan terhadap error dan sering dikaitkan dengan View itu sendiri.

Sebaliknya, Compose memudahkan untuk menampilkan composable yang benar-benar berbeda menggunakan logika bersyarat di Kotlin:

if (showCautionIcon) {
    CautionIcon(/* ... */)
}

Dari segi desain, CautionIcon tidak harus mengetahui atau peduli alasan produk tersebut ditampilkan, dan tidak ada konsep visibility: baik dalam Komposisi atau tidak di dalamnya.

Dengan memisahkan pengelolaan status dan logika presentasi secara rapi, Anda dapat lebih bebas mengubah cara menampilkan konten sebagai konversi status ke UI. Dapat mengangkat status saat diperlukan juga membuat Composable lebih dapat digunakan kembali karena kepemilikan status lebih fleksibel.

Mempromosikan komponen yang dienkapsulasi dan dapat digunakan kembali

Elemen View sering kali memiliki gambaran posisinya: di dalam Activity, Dialog, Fragment, atau di dalam hierarki View lainnya. Karena elemen sering kali di-inflate dari file tata letak statis, struktur keseluruhan View cenderung sangat kaku. Ini menghasilkan pengaitan erat dan mempersulit View diubah atau digunakan ulang.

Misalnya, View kustom dapat mengasumsikan bahwa tampilan tersebut memiliki tampilan turunan dari jenis tertentu dengan ID tertentu, serta mengubah propertinya secara langsung sebagai respons terhadap beberapa tindakan. Ini akan mengaitkan erat elemen View tersebut secara bersamaan: View kustom dapat mengalami error atau rusak jika tidak dapat menemukan turunan, dan turunan mungkin tidak dapat digunakan kembali tanpa induk View kustom.

Masalah di Compose dengan composable yang dapat digunakan kembali menjadi lebih sedikit. Induk dapat dengan mudah menentukan status dan callback agar Composable yang dapat digunakan kembali dapat ditulis tanpa harus mengetahui lokasi penggunaanya secara persis.

var isEnabled by rememberSaveable { mutableStateOf(false) }

Column {
    ImageWithEnabledOverlay(isEnabled)
    ControlPanelWithToggle(
        isEnabled = isEnabled,
        onEnabledChanged = { isEnabled = it }
    )
}

Pada contoh di atas, ketiga bagian lebih dienkapsulasi dan tidak terlalu dikaitkan:

  • ImageWithEnabledOverlay hanya perlu mengetahui status isEnabled saat ini. Tidak perlu mengetahui bahwa ControlPanelWithToggle ada, atau bahkan cara mengontrolnya.

  • ControlPanelWithToggle tidak tahu bahwa ImageWithEnabledOverlay ada. Mungkin tidak ada cara, satu, atau bahkan beberapa cara agar isEnabled ditampilkan, dan ControlPanelWithToggle tidak perlu diubah.

  • Untuk induk, tidak masalah seberapa dalam ImageWithEnabledOverlay atau ControlPanelWithToggle bertingkat. Turunan tersebut dapat menganimasikan perubahan, menukar konten, atau meneruskan konten ke turunan lain.

Pola ini dikenal sebagai inversi kontrol yang dapat Anda baca lebih lanjut dalam dokumentasi CompositionLocal.

Menangani perubahan ukuran layar

Memiliki berbagai resource untuk ukuran jendela yang berbeda adalah salah satu cara utama untuk membuat tata letak View yang responsif. Meskipun resource yang memenuhi syarat masih menjadi opsi untuk keputusan tata letak level layar, Compose memudahkan Anda mengubah keseluruhan tata letak dalam kode dengan logika bersyarat normal. Dengan alat seperti BoxWithConstraints, keputusan dapat dibuat berdasarkan ketersediaan ruang untuk elemen individu yang tidak mungkin dilakukan dengan resource yang memenuhi syarat:

@Composable
fun MyComposable() {
    BoxWithConstraints {
        if (minWidth < 480.dp) {
            /* Show grid with 4 columns */
        } else if (minWidth < 720.dp) {
            /* Show grid with 8 columns */
        } else {
            /* Show grid with 12 columns */
        }
    }
}

Baca mem-build tata letak adaptif guna mempelajari teknik yang ditawarkan Compose untuk membuat UI adaptif.

Scrolling bertingkat dengan Views

Sayangnya, scrolling bertingkat antara sistem View dan Jetpack Compose belum tersedia. Anda dapat memeriksa progresnya di bug issue tracker ini.

Compose di RecyclerView

Jetpack Compose menggunakan DisposeOnDetachedFromWindow sebagai ViewCompositionStrategy default. Ini berarti bahwa Komposisi dibuang setiap kali tampilan dilepaskan dari jendela.

Saat menggunakan ComposeView sebagai bagian dari holder tampilan RecyclerView, strategi default tidak efisien karena instance Komposisi yang mendasarinya akan tetap berada dalam memori sampai RecyclerView dilepaskan dari jendela. Membuang Komposisi yang mendasari saat ComposeView tidak lagi diperlukan oleh RecyclerView merupakan praktik yang tepat.

Fungsi disposeComposition memungkinkan Anda membuang Komposisi dasar ComposeView secara manual. Anda dapat memanggil fungsi ini saat tampilan didaur ulang seperti berikut:

import androidx.compose.ui.platform.ComposeView

class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): MyComposeViewHolder {
        return MyComposeViewHolder(ComposeView(parent.context))
    }

    override fun onViewRecycled(holder: MyComposeViewHolder) {
        // Dispose of the underlying Composition of the ComposeView
        // when RecyclerView has recycled this ViewHolder
        holder.composeView.disposeComposition()
    }

    /* Other methods */
}

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
    /* ... */
}

Seperti yang dibahas di bagian ViewCompositionStrategy for ComposeView, Panduan API Interoperabilitas, untuk membuat fungsi holder tampilan Compose berfungsi di semua skenario, Anda harus menggunakan strategi DisposeOnViewTreeLifecycleDestroyed.

import androidx.compose.ui.platform.ViewCompositionStrategy

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {

    init {
        composeView.setViewCompositionStrategy(
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        )
    }

    fun bind(input: String) {
        composeView.setContent {
            MdcTheme {
                Text(input)
            }
        }
    }
}

Untuk melihat cara ComposeView digunakan di RecyclerView, lihat cabang compose_recyclerview dari aplikasi Sunflower.