Holder status dan Status UI

Panduan lapisan UI membahas aliran data searah (UDF) sebagai sarana untuk memproduksi dan mengelola Status UI untuk lapisan UI.

Data mengalir secara searah dari lapisan data ke UI.
Gambar 1: Aliran data searah

Bagian ini juga menyoroti manfaat mendelegasikan pengelolaan UDF ke class khusus yang disebut holder status. Anda dapat menerapkan holder status melalui ViewModel atau class biasa. Dokumen ini membahas lebih lanjut holder status dan peran yang dilakukannya dalam lapisan UI.

Di akhir dokumen ini, Anda akan memiliki pemahaman tentang cara mengelola status aplikasi di lapisan UI; yaitu pipeline produksi status UI. Anda akan dapat memahami dan mengetahui hal-hal berikut:

  • Memahami jenis status UI yang ada di lapisan UI.
  • Memahami jenis logika yang beroperasi pada status UI tersebut di lapisan UI.
  • Mengetahui cara memilih implementasi yang sesuai dari holder status, seperti ViewModel atau class sederhana.

Elemen pipeline produksi status UI

Status UI dan logika yang menghasilkannya menentukan lapisan UI.

Status UI

Status UI adalah properti yang mendeskripsikan UI. Ada dua jenis status UI:

  • Status UI Layar adalah apa yang perlu Anda tampilkan di layar. Misalnya, class NewsUiState dapat berisi artikel berita dan informasi lainnya yang diperlukan untuk merender UI. Status ini biasanya terhubung dengan lapisan hierarki lain karena berisi data aplikasi.
  • Status elemen UI mengacu pada properti yang bersifat intrinsik pada elemen UI yang memengaruhi cara renderingnya. Elemen UI dapat ditampilkan atau disembunyikan dan dapat memiliki font, ukuran font, atau warna font tertentu. Dalam Android View, View mengelola status ini sendiri karena stateful secara inheren, dengan mengekspos metode untuk mengubah atau mengkueri statusnya. Contohnya adalah metode get dan set dari class TextView untuk teksnya. Di Jetpack Compose, status berada di luar composable, dan Anda bahkan dapat mengangkatnya dari area sekitar composable ke dalam fungsi composable panggilan atau holder status. Contohnya adalah ScaffoldState untuk composable Scaffold.

Logika

Status UI bukan properti statis, karena data aplikasi dan peristiwa pengguna menyebabkan status UI berubah dari waktu ke waktu. Logika menentukan detail perubahan, termasuk bagian status UI apa yang berubah, alasan UI berubah, dan waktu perubahannya.

Logika menghasilkan status UI
Gambar 2: Logika sebagai produser status UI

Logika dalam aplikasi dapat berupa logika bisnis atau logika UI:

  • Logika bisnis adalah penerapan persyaratan produk untuk data aplikasi. Misalnya, mem-bookmark artikel di aplikasi pembaca berita saat pengguna mengetuk tombol. Logika untuk menyimpan bookmark ke file atau database ini biasanya ditempatkan di lapisan domain atau data. Holder status biasanya mendelegasikan logika ini ke lapisan tersebut dengan memanggil metode yang diekspos.
  • Logika UI berkaitan dengan cara menampilkan status UI di layar. Misalnya, mendapatkan petunjuk kotak penelusuran yang tepat saat pengguna memilih kategori, men-scroll ke item tertentu dalam daftar, atau logika navigasi ke layar tertentu saat pengguna mengklik tombol.

Siklus proses Android serta jenis status dan logika UI

Lapisan UI memiliki dua bagian: satu dependen dan lainnya tidak bergantung pada siklus proses UI. Pemisahan ini menentukan sumber data yang tersedia untuk setiap bagian, sehingga memerlukan berbagai jenis status dan logika UI.

  • Siklus proses UI independen: Bagian lapisan UI ini berhubungan dengan lapisan penghasil data aplikasi (lapisan data atau domain) dan ditentukan oleh logika bisnis. Siklus proses, perubahan konfigurasi, dan pembuatan ulang Activity di UI dapat memengaruhi apakah pipeline produksi status UI aktif, tetapi tidak memengaruhi validitas data yang dihasilkan.
  • Siklus proses UI dependen: Bagian lapisan UI ini berhubungan dengan logika UI, dan terpengaruh secara langsung oleh perubahan siklus proses atau konfigurasi. Perubahan tersebut secara langsung memengaruhi validitas sumber data yang dibaca di dalamnya, dan akibatnya statusnya hanya dapat berubah jika siklus prosesnya aktif. Contohnya mencakup izin runtime dan mendapatkan resource yang bergantung pada konfigurasi seperti string yang dilokalkan.

Penjelasan di atas dapat diringkas dengan tabel di bawah:

Siklus Proses UI independen Siklus Proses UI dependen
Logika bisnis Logika UI
Status UI Layar

Pipeline produksi status UI

Pipeline produksi status UI mengacu pada langkah-langkah yang dilakukan untuk menghasilkan status UI. Langkah-langkah ini mencakup penerapan jenis logika yang ditentukan sebelumnya, dan sepenuhnya bergantung pada kebutuhan UI Anda. Beberapa UI mungkin memanfaatkan bagian pipeline Siklus Proses UI independen dan Siklus Proses UI dependen, atau tidak keduanya.

Sehingga, permutasi berikut dari pipeline lapisan UI akan valid:

  • Status UI yang dihasilkan dan dikelola oleh UI itu sendiri. Misalnya, penghitung dasar yang sederhana dan dapat digunakan kembali:

    @Composable
    fun Counter() {
        // The UI state is managed by the UI itself
        var count by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { ++count }) {
                Text(text = "Increment")
            }
            Button(onClick = { --count }) {
                Text(text = "Decrement")
            }
        }
    }
    
  • Logika UI → UI. Misalnya, menampilkan atau menyembunyikan tombol yang memungkinkan pengguna langsung menuju ke bagian atas daftar.

    @Composable
    fun ContactsList(contacts: List<Contact>) {
        val listState = rememberLazyListState()
        val isAtTopOfList by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex < 3
            }
        }
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Show or hide the button (UI logic) based on the list scroll position
        AnimatedVisibility(visible = !isAtTopOfList) {
            ScrollToTopButton()
        }
    }
    
  • Logika bisnis → UI. Elemen UI yang menampilkan foto pengguna saat ini di layar.

    @Composable
    fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
        // Call on the UserAvatar Composable to display the photo
        UserAvatar(picture = uiState.profilePicture)
    }
    
  • Logika bisnis → Logika UI → UI. Elemen UI yang men-scroll untuk menampilkan informasi yang tepat di layar untuk status UI tertentu.

    @Composable
    fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        val contacts = uiState.contacts
        val deepLinkedContact = uiState.deepLinkedContact
    
        val listState = rememberLazyListState()
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Perform UI logic that depends on information from business logic
        if (deepLinkedContact != null && contacts.isNotEmpty()) {
            LaunchedEffect(listState, deepLinkedContact, contacts) {
                val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
                if (deepLinkedContactIndex >= 0) {
                  // Scroll to deep linked item
                  listState.animateScrollToItem(deepLinkedContactIndex)
                }
            }
        }
    }
    

Untuk kasus ketika kedua jenis logika diterapkan ke pipeline produksi status UI, logika bisnis harus selalu diterapkan sebelum logika UI. Mencoba menerapkan logika bisnis setelah logika UI akan menunjukkan bahwa logika bisnis bergantung pada logika UI. Bagian berikut membahas mengapa hal ini menjadi masalah melalui pembahasan mendalam tentang berbagai jenis logika dan holder statusnya.

Data mengalir dari lapisan penghasil data ke UI
Gambar 3: Penerapan logika di lapisan UI

Holder status dan tanggung jawabnya

Tanggung jawab holder status adalah menyimpan status sehingga aplikasi dapat membacanya. Saat logika diperlukan, holder status akan bertindak sebagai perantara dan memberikan akses ke sumber data yang menghosting logika yang diperlukan. Dengan cara ini, holder status akan mendelegasikan logika ke sumber data yang sesuai.

Hal ini menghasilkan manfaat berikut:

  • UI Sederhana: UI hanya mengikat statusnya.
  • Kemudahan pemeliharaan: Logika yang ditentukan dalam holder status dapat diiterasi tanpa mengubah UI itu sendiri.
  • Kemampuan untuk diuji: UI dan logika produksi statusnya dapat diuji secara independen.
  • Keterbacaan: Pembaca kode dapat dengan jelas melihat perbedaan antara kode presentasi UI dan kode produksi status UI.

Terlepas dari ukuran atau cakupannya, setiap elemen UI memiliki hubungan 1:1 dengan holder status yang sesuai. Selain itu, holder status harus dapat menerima dan memproses tindakan pengguna apa pun yang dapat menyebabkan perubahan status UI dan harus menghasilkan perubahan status berikutnya.

Jenis holder status

Serupa dengan jenis status dan logika UI, ada dua jenis holder status di lapisan UI yang ditentukan oleh hubungannya dengan siklus proses UI:

  • Holder status logika bisnis.
  • Holder status logika UI.

Bagian berikut ini membahas lebih lanjut jenis holder status, dimulai dari holder status logika bisnis.

Logika bisnis dan holder statusnya

Holder status logika bisnis memproses peristiwa pengguna dan mengubah data dari lapisan data atau domain menjadi status UI layar. Untuk memberikan pengalaman pengguna yang optimal saat mempertimbangkan siklus proses Android dan perubahan konfigurasi aplikasi, holder status yang menggunakan logika bisnis harus memiliki properti berikut:

Properti Detail
Menghasilkan Status UI Holder status logika bisnis bertanggung jawab untuk menghasilkan status UI untuk UI-nya. Status UI ini sering kali menjadi hasil pemrosesan peristiwa pengguna dan pembacaan data dari domain dan lapisan data.
Dipertahankan melalui pembuatan ulang aktivitas Holder status logika bisnis mempertahankan pipeline pemrosesan status dan status mereka di seluruh pembuatan ulang Activity, membantu memberikan pengalaman pengguna yang lancar. Jika holder status tidak dapat dipertahankan dan dibuat ulang (biasanya setelah penghentian proses), holder status harus dapat dengan mudah membuat ulang status terakhirnya untuk memastikan pengalaman pengguna yang konsisten.
Memiliki status berumur panjang Holder status logika bisnis sering digunakan untuk mengelola status untuk tujuan navigasi. Oleh karena itu, holder tersebut sering kali mempertahankan statusnya di seluruh perubahan navigasi hingga dihapus dari grafik navigasi.
Bersifat unik untuk UI-nya dan tidak dapat digunakan kembali Holder status logika bisnis biasanya menghasilkan status untuk fungsi aplikasi tertentu, misalnya TaskEditViewModel atau TaskListViewModel, sehingga hanya berlaku untuk fungsi aplikasi tersebut. Holder status yang sama dapat mendukung fungsi aplikasi ini di berbagai faktor bentuk. Misalnya, aplikasi versi seluler, TV, dan tablet dapat menggunakan kembali holder status logika bisnis yang sama.

Misalnya, pertimbangkan tujuan navigasi penulis di aplikasi "Now in Android":

Aplikasi Now in Android menunjukkan bagaimana tujuan navigasi yang merepresentasikan fungsi aplikasi utama harus memiliki
holder status logika bisnis sendiri yang unik.
Gambar 4: Aplikasi Now in Android

Dalam hal ini, sebagai holder status logika bisnis, AuthorViewModel menghasilkan status UI:

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = …

    // Business logic
    fun followAuthor(followed: Boolean) {
      …
    }
}

Perhatikan bahwa AuthorViewModel memiliki atribut yang sudah diuraikan sebelumnya:

Properti Detail
Menghasilkan AuthorScreenUiState AuthorViewModel membaca data dari AuthorsRepository dan NewsRepository, lalu menggunakan data tersebut untuk menghasilkan AuthorScreenUiState. Ini juga menerapkan logika bisnis saat pengguna ingin mengikuti atau berhenti mengikuti Author dengan mendelegasikan ke AuthorsRepository.
Memiliki akses ke lapisan data Instance AuthorsRepository dan NewsRepository diteruskan ke instance tersebut dalam konstruktornya, sehingga memungkinkan penerapan logika bisnis dengan mengikuti Author.
Bertahan dalam pembuatan ulang Activity Karena diterapkan dengan ViewModel, data tersebut akan dipertahankan di seluruh pembuatan ulang Activity yang cepat. Dalam kasus penghentian proses, objek SavedStateHandle dapat dibaca untuk memberikan jumlah informasi minimum yang diperlukan untuk memulihkan status UI dari lapisan data.
Memiliki status berumur panjang ViewModel diberi cakupan untuk grafik navigasi, sehingga kecuali tujuan penulis dihapus dari grafik navigasi, status UI di uiState StateFlow tetap ada di memori. Penggunaan StateFlow juga menambah manfaat dari penerapan logika bisnis yang menghasilkan status lambat karena status hanya dihasilkan jika ada kolektor status UI.
Bersifat unik untuk UI-nya AuthorViewModel hanya berlaku untuk tujuan navigasi penulis dan tidak dapat digunakan kembali di tempat lain. Jika ada logika bisnis yang digunakan kembali di seluruh tujuan navigasi, logika bisnis tersebut harus dienkapsulasi dalam komponen cakupan data atau lapisan domain.

ViewModel sebagai holder status logika bisnis

Manfaat ViewModel dalam pengembangan Android membuatnya cocok untuk memberikan akses ke logika bisnis dan menyiapkan data aplikasi untuk presentasi di layar. Manfaat tersebut mencakup:

  • Operasi yang dipicu oleh ViewModel bertahan dari perubahan konfigurasi.
  • Integrasi dengan Navigation:
    • Navigation men-cache ViewModel saat layar berada di data sebelumnya. Hal ini penting agar data yang sebelumnya dimuat langsung tersedia saat Anda kembali ke tujuan. Hal ini lebih sulit dilakukan dengan holder status yang mengikuti siklus proses layar composable.
    • ViewModel juga dihapus saat tujuan dikeluarkan dari data sebelumnya, sehingga memastikan status Anda dibersihkan secara otomatis. Hal ini berbeda dengan memproses penghapusan composable yang dapat terjadi karena beberapa alasan seperti membuka layar baru, karena perubahan konfigurasi, dll.
  • Integrasi dengan library Jetpack lainnya, seperti Hilt.

Logika UI dan holder statusnya

Logika UI adalah logika yang beroperasi pada data yang disediakan UI itu sendiri. Hal ini dapat berupa status elemen UI, atau pada sumber data UI seperti API izin atau Resources. Holder status yang menggunakan logika UI biasanya memiliki properti berikut:

  • Menghasilkan status UI dan mengelola status elemen UI.
  • Tidak bertahan dari pembuatan ulang Activity: Holder status yang dihosting dalam logika UI sering kali bergantung pada sumber data dari UI itu sendiri, dan upaya mempertahankan informasi ini di seluruh perubahan konfigurasi sering kali menyebabkan kebocoran memori. Jika holder status memerlukan data untuk bertahan di seluruh perubahan konfigurasi, holder status perlu didelegasikan ke komponen lain yang lebih sesuai untuk bertahan dari pembuatan ulang Activity. Misalnya, di Jetpack Compose, status elemen UI Composable yang dibuat dengan fungsi remembered sering didelegasikan ke rememberSaveable untuk mempertahankan status di seluruh pembuatan ulang Activity. Contoh fungsi tersebut meliputi rememberScaffoldState() dan rememberLazyListState().
  • Memiliki referensi ke sumber data cakupan UI: Sumber data seperti API siklus proses dan Resource dapat dirujuk dan dibaca dengan aman karena holder status logika UI memiliki siklus proses yang sama dengan UI.
  • Dapat digunakan kembali di beberapa UI: Instance yang berbeda-beda dari holder status logika UI yang sama dapat digunakan kembali di berbagai bagian aplikasi. Misalnya, holder status untuk mengelola peristiwa input pengguna bagi grup chip dapat digunakan di halaman penelusuran untuk filter chip, dan juga di kolom "kepada" untuk penerima email.

Holder status logika UI biasanya diterapkan dengan class biasa. Hal ini karena UI itu sendiri bertanggung jawab atas pembuatan holder status logika UI dan holder status logika UI memiliki siklus proses yang sama dengan UI itu sendiri. Di Jetpack Compose, misalnya, holder status adalah bagian dari Komposisi dan mengikuti siklus proses Komposisi.

Hal ini dapat diilustrasikan dalam contoh berikut dalam aplikasi contoh Now in Android:

Now in Android menggunakan holder status class biasa untuk mengelola logika UI
Gambar 5: Aplikasi contoh Now in Android

Aplikasi contoh Now in Android menampilkan panel aplikasi bawah atau kolom samping navigasi untuk navigasinya, bergantung pada ukuran layar perangkat. Layar yang lebih kecil akan menggunakan panel aplikasi bawah, dan layar yang lebih besar akan menggunakan kolom samping navigasi.

Karena logika untuk menentukan elemen UI navigasi yang sesuai yang digunakan dalam NiaApp fungsi composable tidak bergantung pada logika bisnis, logika ini dapat dikelola oleh holder status class biasa yang disebut NiaAppState:

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

Pada contoh di atas, detail terkait NiaAppState berikut bersifat penting:

  • Tidak bertahan dari pembuatan ulang Activity: NiaAppState adalah remembered di dalam Komposisi dengan membuatnya menggunakan rememberNiaAppState fungsi Composable mengikuti konvensi penamaan Compose. Setelah Activity dibuat ulang, instance sebelumnya akan hilang dan instance baru akan dibuat dengan meneruskan semua dependensinya, sesuai untuk konfigurasi dari pembuatan ulang Activity yang baru. Dependensi ini mungkin baru atau dipulihkan dari konfigurasi sebelumnya. Misalnya, rememberNavController() digunakan dalam konstruktor NiaAppState dan didelegasikan ke rememberSaveable untuk mempertahankan status di seluruh pembuatan ulang Activity.
  • Memiliki referensi ke sumber data cakupan UI: Memberi referensi ke navigationController, Resources, dan jenis cakupan siklus proses serupa lainnya dapat disimpan dengan aman di NiaAppState karena memiliki cakupan siklus proses yang sama.

Memilih antara ViewModel dan class biasa untuk holder status

Dari bagian di atas, memilih antara ViewModel dan holder status class biasa bergantung pada logika yang diterapkan pada status UI dan sumber data tempat logika dioperasikan.

Singkatnya, diagram di bawah menunjukkan posisi holder status dalam pipeline produksi Status UI:

Data mengalir dari lapisan yang menghasilkan data ke lapisan UI
Gambar 6: Holder status di pipeline produksi Status UI. Panah berarti aliran data.

Pada akhirnya, Anda harus membuat status UI menggunakan holder status terdekat dengan tempat penggunaannya. Standarnya, Anda harus mempertahankan status serendah mungkin sekaligus mempertahankan kepemilikan yang tepat. Jika Anda memerlukan akses ke logika bisnis dan memerlukan status UI untuk tetap ada selama layar dapat dibuka, bahkan di seluruh pembuatan ulang Activity, ViewModel adalah pilihan tepat untuk penerapan holder status logika bisnis Anda. Untuk status UI dan logika UI berumur pendek, class biasa yang siklus prosesnya hanya bergantung pada UI seharusnya cukup.

Holder status dapat digabung

Holder status dapat bergantung pada holder status lain selama dependensi memiliki masa aktif yang sama atau lebih pendek. Contohnya adalah:

  • holder status logika UI dapat bergantung pada holder status logika UI lainnya.
  • holder status tingkat layar dapat bergantung pada holder status logika UI.

Cuplikan kode berikut menunjukkan cara DrawerState Compose bergantung pada holder status internal lainnya, SwipeableState, dan cara holder status logika UI aplikasi dapat bergantung pada DrawerState:

@Stable
class DrawerState(/* ... */) {
  internal val swipeableState = SwipeableState(/* ... */)
  // ...
}

@Stable
class MyAppState(
  private val drawerState: DrawerState,
  private val navController: NavHostController
) { /* ... */ }

@Composable
fun rememberMyAppState(
  drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
  navController: NavHostController = rememberNavController()
): MyAppState = remember(drawerState, navController) {
  MyAppState(drawerState, navController)
}

Contoh dependensi yang aktif lebih lama dibandingkan holder status adalah holder status logika UI yang bergantung pada holder status tingkat layar. Hal itu akan mengurangi penggunaan kembali holder status yang berumur lebih pendek dan memberinya akses ke lebih banyak logika dan status daripada yang benar-benar diperlukan.

Jika holder status berumur pendek memerlukan informasi tertentu dari holder status yang cakupannya lebih tinggi, teruskan hanya informasi yang diperlukan sebagai parameter, bukan meneruskan instance holder status. Misalnya, dalam cuplikan kode berikut, class holder status logika UI menerima hanya yang dibutuhkan sebagai parameter dari ViewModel, bukan meneruskan seluruh instance ViewModel sebagai dependensi.

class MyScreenViewModel(/* ... */) {
  val uiState: StateFlow<MyScreenUiState> = /* ... */
  fun doSomething() { /* ... */ }
  fun doAnotherThing() { /* ... */ }
  // ...
}

@Stable
class MyScreenState(
  // DO NOT pass a ViewModel instance to a plain state holder class
  // private val viewModel: MyScreenViewModel,

  // Instead, pass only what it needs as a dependency
  private val someState: StateFlow<SomeState>,
  private val doSomething: () -> Unit,

  // Other UI-scoped types
  private val scaffoldState: ScaffoldState
) {
  /* ... */
}

@Composable
fun rememberMyScreenState(
  someState: StateFlow<SomeState>,
  doSomething: () -> Unit,
  scaffoldState: ScaffoldState = rememberScaffoldState()
): MyScreenState = remember(someState, doSomething, scaffoldState) {
  MyScreenState(someState, doSomething, scaffoldState)
}

@Composable
fun MyScreen(
  modifier: Modifier = Modifier,
  viewModel: MyScreenViewModel = viewModel(),
  state: MyScreenState = rememberMyScreenState(
    someState = viewModel.uiState.map { it.toSomeState() },
    doSomething = viewModel::doSomething
  ),
  // ...
) {
  /* ... */
}

Diagram berikut menunjukkan dependensi antara UI dan berbagai holder status cuplikan kode sebelumnya:

UI bergantung pada holder status logika UI dan holder status tingkat layar
Gambar 7: UI bergantung pada holder status yang berbeda. Panah berarti dependensi.

Contoh

Contoh Google berikut menunjukkan penggunaan holder status di lapisan UI. Jelajahi untuk melihat panduan ini dalam praktik: