Merancang Compose UI Anda

Di Compose, UI tidak dapat diubah—tidak ada cara untuk mengupdatenya setelah digambar. Yang dapat Anda kontrol adalah status UI Anda. Setiap kali status UI berubah, Compose membuat ulang bagian hierarki UI yang telah berubah. Composable dapat menerima status dan menampilkan peristiwa—misalnya TextField menerima nilai dan menampilkan onValueChange callback yang meminta handler callback untuk mengubah nilai.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

Karena composable menerima status dan menampilkan peristiwa, pola aliran data searah akan sesuai dengan Jetpack Compose. Panduan ini berfokus pada cara menerapkan pola aliran data searah di Compose, cara menerapkan peristiwa dan pemegang status, dan cara menggunakan ViewModel di Compose.

Aliran data searah

Aliran data searah (ADS) adalah pola desain dengan status mengalir ke bawah dan peristiwa mengalir ke atas. Dengan mengikuti aliran data searah, Anda dapat memisahkan composable yang menampilkan status di UI dari bagian aplikasi yang menyimpan dan mengubah status.

Loop update UI untuk aplikasi yang menggunakan aliran data searah terlihat seperti ini:

  • Peristiwa: Bagian UI menghasilkan peristiwa dan meneruskannya ke atas, seperti klik tombol yang diteruskan ke ViewModel untuk ditangani; atau peristiwa diteruskan dari lapisan lain aplikasi Anda, seperti menunjukkan bahwa sesi pengguna telah berakhir.
  • Status update: Handler peristiwa dapat mengubah status.
  • Status tampilan: Pemegang status akan menurunkan status, dan UI akan menampilkannya.

Aliran data searah

Mengikuti pola ini saat menggunakan Jetpack Compose memberikan beberapa keuntungan:

  • Kemampuan untuk diuji: Memisahkan status dari UI yang menampilkannya akan mempermudah pengujian keduanya secara terpisah.
  • Enkapsulasi status: Karena status hanya dapat diupdate di satu tempat dan hanya ada satu sumber ketepatan untuk status composable, kecil kemungkinan Anda membuat bug karena status yang tidak konsisten.
  • Konsistensi UI: Semua update status langsung tercermin di UI dengan menggunakan pemegang status yang dapat diamati, seperti LiveData atau StateFlow.

Aliran data searah di Jetpack Compose

Composable bekerja berdasarkan status dan peristiwa. Misalnya, TextField hanya akan diupdate jika parameter value diupdate, dan menampilkan callback onValueChange, yaitu peristiwa yang meminta nilai diubah ke callback baru. Compose menentukan objek State sebagai penampung nilai, dan perubahan pada nilai status akan memicu rekomposisi. Anda dapat menyimpan status dalam remember { mutableStateOf(value) } atau rememberSaveable { mutableStateOf(value), bergantung pada berapa lama Anda perlu mengingat nilainya.

Jenis nilai composable TextField adalah String, sehingga nilai ini dapat berasal dari mana saja—dari nilai hardcode, dari ViewModel, atau diteruskan dari composable induk. Anda tidak perlu menyimpannya dalam objek State, tetapi Anda perlu memperbarui nilainya saat onValueChange dipanggil.

Menentukan parameter composable

Saat menentukan parameter status composable, Anda harus memperhatikan pertanyaan berikut:

  • Seberapa dapat digunakan kembali atau seberapa fleksibel komposisinya?
  • Bagaimana pengaruh parameter status terhadap performa composable ini?

Untuk mendorong pemisahan dan penggunaan ulang, setiap composable harus memiliki jumlah informasi minimum. Misalnya, saat mem-build composable untuk menyimpan header artikel berita, lebih baik hanya menyampaikan informasi yang perlu ditampilkan, bukan seluruh artikel berita.

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

Terkadang, menggunakan parameter individual juga meningkatkan performa—misalnya, jika News berisi lebih banyak informasi selain title dan subtitle, setiap kali instance baru News diteruskan ke Header(news), composable akan mengomposisi ulang, meskipun title dan subtitle tidak berubah.

Pertimbangkan baik-baik jumlah parameter yang Anda berikan. Fungsi yang memiliki terlalu banyak parameter akan menurunkan ergonomi fungsi tersebut, sehingga dalam hal ini, mengelompokkannya dalam class lebih diutamakan.

Peristiwa di Compose

Setiap input ke aplikasi Anda harus direpresentasikan sebagai peristiwa: ketukan, perubahan teks, dan bahkan timer atau pembaruan lainnya. Karena peristiwa ini mengubah status UI, ViewModel harus menjadi yang menanganinya dan mengupdate status UI.

Lapisan UI tidak boleh mengubah status di luar pengendali peristiwa karena hal ini dapat menimbulkan ketidakkonsistenan dan bug dalam aplikasi Anda.

Memilih meneruskan nilai yang tidak dapat diubah untuk lambda status dan pengendali peristiwa. Pendekatan ini memiliki manfaat sebagai berikut:

  • Anda meningkatkan penggunaan kembali.
  • Anda memastikan UI tidak mengubah nilai status secara langsung.
  • Anda menghindari masalah serentak karena memastikan bahwa status tidak dimutasi dari rangkaian pesan lain.
  • Sering kali, Anda mengurangi kompleksitas kode.

Misalnya, composable yang menerima String dan lambda sebagai parameter dapat dipanggil dari berbagai konteks dan sangat dapat digunakan kembali. Misalnya panel aplikasi atas di aplikasi Anda selalu menampilkan teks dan memiliki tombol kembali. Anda dapat menentukan fungsi MyAppTopAppBar yang dapat dikomposisi dan lebih umum yang menerima teks dan handle tombol kembali sebagai parameter:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                        Icons.Filled.ArrowBack,
                        contentDescription = localizedString
                    )
            }
        },
        // ...
    )
}

ViewModels, status, dan peristiwa: contoh

Dengan menggunakan ViewModel dan mutableStateOf, Anda juga dapat memperkenalkan aliran data yang searah dalam aplikasi jika salah satu hal berikut terjadi:

  • Status UI Anda terekspos melalui LiveData sebagai penerapan pemegang status yang dapat diamati.
  • ViewModel menangani peristiwa yang berasal dari UI atau lapisan lain aplikasi Anda dan mengupdate pemegang status berdasarkan peristiwa tersebut.

Misalnya, saat menerapkan layar masuk, mengetuk tombol Login akan menyebabkan aplikasi menampilkan indikator lingkaran berputar progres dan panggilan jaringan. Jika proses login berhasil, aplikasi Anda membuka layar yang berbeda; jika terjadi error, aplikasi menampilkan Snackbar. Berikut cara membuat model status layar dan peristiwa:

Layar memiliki empat status:

  • Logout: saat pengguna belum login.
  • Dalam proses: saat aplikasi Anda sedang mencoba membuat pengguna login dengan melakukan panggilan jaringan.
  • Error: saat terjadi error saat login.
  • Login: saat pengguna login.

Anda dapat membuat model status ini sebagai class tertutup. ViewModel mengekspos status sebagai State, menyetel status awal, dan mengupdate status sesuai kebutuhan. ViewModel juga menangani peristiwa login dengan menampilkan metode onSignIn().

sealed class UiState {
    object SignedOut : UiState()
    object InProgress : UiState()
    object Error : UiState()
    object SignIn : UiState()
}

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

Selain mutableStateOf API, Compose menyediakan ekstensi untuk LiveData, Flow, dan Observable untuk mendaftar sebagai pemroses dan mewakili nilai sebagai status.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}