Pertimbangan lainnya

Meskipun migrasi dari View ke Compose sepenuhnya berkaitan dengan UI, ada banyak hal yang perlu dipertimbangkan untuk melakukan migrasi yang aman dan bertahap. Halaman ini berisi beberapa pertimbangan saat memigrasikan aplikasi berbasis View ke Compose.

Memigrasikan tema aplikasi

Desain Material adalah sistem desain yang direkomendasikan untuk penerapan tema aplikasi Android.

Untuk aplikasi berbasis View, tersedia tiga versi Material:

  • Desain Material 1 menggunakan library AppCompat (yaitu Theme.AppCompat.*)
  • Desain Material 2 menggunakan library MDC-Android (yaitu Theme.MaterialComponents.*)
  • Desain Material 3 menggunakan library MDC-Android (yaitu Theme.Material3.*)

Untuk aplikasi Compose, tersedia dua versi Material:

  • Desain Material 2 menggunakan library Compose Material (yaitu androidx.compose.material.MaterialTheme)
  • Desain Material 3 menggunakan library Compose Material 3 (yaitu androidx.compose.material3.MaterialTheme)

Sebaiknya gunakan versi terbaru (Material 3) jika sistem desain aplikasi Anda dapat melakukannya. Tersedia panduan migrasi untuk View dan Compose:

Saat membuat layar baru di Compose, terlepas dari versi Desain Material yang Anda gunakan, pastikan Anda menerapkan MaterialTheme sebelum composable mana pun yang membuat UI dari library Compose 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.

Lihat Mendesain sistem di Compose dan Memigrasikan tema XML ke Compose untuk mempelajari lebih lanjut.

Jika Anda menggunakan komponen Navigation di aplikasi, lihat Menavigasi dengan Compose - Interoperabilitas dan Melakukan migrasi Navigasi Jetpack ke Navigation Compose untuk mengetahui informasi selengkapnya.

Menguji UI Compose/Views campuran

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

Saat aktivitas atau fragmen menggunakan Compose, Anda harus menggunakan createAndroidComposeRule, bukan menggunakan ActivityScenarioRule. createAndroidComposeRule mengintegrasikan ActivityScenarioRule dengan ComposeTestRule yang memungkinkan Anda menguji kode Compose dan View secara bersamaan.

class MyActivityTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MyActivity>()

    @Test
    fun testGreeting() {
        val greeting = InstrumentationRegistry.getInstrumentation()
            .targetContext.resources.getString(R.string.greeting)

        composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
    }
}

Lihat Menguji tata letak Compose untuk mempelajari pengujian lebih lanjut. Untuk interoperabilitas dengan framework pengujian UI, lihat interoperabilitas dengan Espresso dan interoperabilitas dengan UiAutomator.

Mengintegrasi Compose dengan arsitektur aplikasi yang sudah ada

Pola arsitektur Aliran Data Searah (UDF) berfungsi lancar dengan Compose. Jika aplikasi menggunakan jenis pola arsitektur lain, seperti Presenter View Model (MVP), sebaiknya migrasikan bagian UI tersebut ke UDF sebelum atau saat mengadopsi Compose.

Menggunakan ViewModel di Compose

Jika Anda menggunakan library Komponen Arsitektur ViewModel Anda dapat mengakses ViewModel dari composable mana pun dengan memanggil fungsi viewModel(), seperti yang dijelaskan dalam Compose dan library lainnya.

Saat menggunakan Compose, berhati-hatilah saat menggunakan jenis ViewModel yang sama dalam berbagai composable karena elemen ViewModel mengikuti cakupan siklus proses View. Cakupan akan berupa aktivitas host, fragmen, atau grafik navigasi jika library Navigasi digunakan.

Misalnya, jika composable dapat dihosting dalam aktivitas, viewModel() selalu menampilkan instance yang sama yang hanya akan dihapus saat aktivitas telah selesai. Pada contoh berikut, pengguna yang sama ("user1") disapa dua kali karena instance GreetingViewModel yang sama digunakan kembali di semua composable dalam aktivitas host. Instance ViewModel pertama yang telah dibuat digunakan kembali di composable lain.

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

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(  
        factory = GreetingViewModelFactory(userId)  
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

Karena grafik navigasi juga mencakup elemen ViewModel, composable yang merupakan tujuan dalam grafik navigasi memiliki instance ViewModel yang berbeda. Dalam hal ini, ViewModel dimasukkan ke siklus proses tujuan dan akan dihapus saat tujuan dihapus dari backstack. Pada contoh berikut, saat pengguna membuka layar Profil, instance baru GreetingViewModel akan dibuat.

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

Sumber kebenaran status

Saat Anda mengadopsi Compose di satu bagian UI, Compose dan kode sistem View mungkin perlu membagikan data. Jika memungkinkan, sebaiknya Anda merangkum status bersama tersebut di class lain yang mengikuti praktik terbaik UDF dan digunakan oleh kedua platform tersebut, misalnya, di ViewModel yang mengekspos aliran data dari data bersama untuk memberikan pembaruan data.

Namun, hal ini tidak selalu mungkin jika data yang akan dibagikan dapat diubah atau terikat erat dengan elemen UI. Dalam hal ini, satu sistem harus menjadi sumber kebenaran, dan sistem tersebut perlu membagikan setiap update data ke sistem lain. Sebagai aturan umum, sumber kebenaran harus dimiliki oleh elemen mana pun yang lebih dekat dengan root hierarki UI.

Compose sebagai sumber kebenaran

Gunakan fungsi SideEffect untuk memublikasikan status Compose ke kode non-Compose. Dalam hal ini, sumber kebenaran disimpan dalam composable mengirim update status.

Contohnya, library analisis Anda dapat digunakan untuk mengelompokkan populasi pengguna dengan melampirkan metadata khusus (properti pengguna pada contoh ini) ke semua peristiwa analisis berikutnya. Untuk memberitahukan jenis pengguna dari pengguna saat ini ke library analisis Anda, gunakan SideEffect untuk memperbarui nilainya.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

Untuk informasi selengkapnya, lihat Efek samping di Compose.

Sistem View sebagai sumber kebenaran

Jika sistem View memiliki status dan membagikannya dengan Compose, sebaiknya gabungkan status dalam objek mutableStateOf agar Compose aman dari thread. Jika Anda menggunakan pendekatan ini, fungsi composable akan disederhanakan karena tidak lagi memiliki sumber kebenaran, tetapi sistem View harus mengupdate status yang dapat diubah dan View yang menggunakan status tersebut.

Dalam contoh berikut, CustomViewGroup berisi TextView dan ComposeView dengan komponen TextField di dalamnya. TextView harus menampilkan konten yang diketik pengguna di TextField.

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

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

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 composable dan dapat digunakan kembali di seluruh aplikasi, apa pun elemen yang telah diberi gaya menggunakan XML atau menjadi tampilan khusus. Misalnya, Anda akan membuat composable CallToActionButton untuk komponen Button pesan ajakan (CTA) khusus.

Untuk menggunakan composable di layar berbasis View, buat wrapper tampilan khusus yang diperluas dari AbstractComposeView. Pada composable Content yang telah diganti, tempatkan composable 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(
            containerColor = MaterialTheme.colorScheme.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("")
    var onClick by mutableStateOf({})

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

Perhatikan bahwa parameter composable akan menjadi variabel yang dapat diubah di dalam tampilan khusus. Ini akan membuat tampilan CallToActionViewButton khusus menjadi dapat di-inflate dan digunakan, seperti tampilan tradisional. Lihat contohnya dengan View Binding di bawah:

class ViewBindingActivity : ComponentActivity() {

    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.greeting)
            onClick = { /* Do something */ }
        }
    }
}

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

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:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    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 Anda dapat menulis composable yang dapat digunakan kembali tanpa harus mengetahui lokasi penggunaannya secara persis.

@Composable
fun AScreen() {
    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. Lihat Menggunakan class ukuran jendela untuk mempelajari lebih lanjut.

Selain itu, lihat Mendukung berbagai ukuran tampilan untuk mempelajari teknik yang ditawarkan Compose untuk mem-build UI adaptif.

Scrolling bertingkat dengan Views

Untuk informasi selengkapnya tentang cara mengaktifkan interop scrolling bertingkat antara elemen View yang dapat di-scroll dan composable yang dapat di-scroll, yang disusun bertingkat di kedua arah, baca Interop scrolling bertingkat.

Compose di RecyclerView

Composable di RecyclerView berperforma tinggi sejak RecyclerView versi 1.3.0-alpha02. Pastikan Anda menggunakan setidaknya versi 1.3.0-alpha02 RecyclerView untuk melihat manfaat tersebut.

Interop WindowInsets dengan View

Anda mungkin perlu mengganti inset default saat layar memiliki kode View dan Compose dalam hierarki yang sama. Dalam hal ini, Anda harus menentukan dengan jelas mana yang harus menggunakan inset, dan mana yang harus mengabaikannya.

Misalnya, jika tata letak terluar Anda adalah tata letak View Android, Anda harus menggunakan inset di sistem View dan mengabaikannya untuk Compose. Atau, jika tata letak terluar Anda adalah composable, Anda harus menggunakan inset di Compose, dan menambahkan padding pada composable AndroidView sebagaimana mestinya.

Secara default, setiap ComposeView menggunakan semua inset pada tingkat konsumsi WindowInsetsCompat. Untuk mengubah perilaku default ini, tetapkan ComposeView.consumeWindowInsets ke false.

Untuk informasi selengkapnya, baca dokumentasi WindowInsets di Compose.