Ke mana status sebaiknya diangkat

Di aplikasi Compose, tempat Anda mengangkat status UI bergantung pada apakah logika UI atau logika bisnis memerlukannya. Dokumen ini menguraikan dua skenario utama ini.

Praktik terbaik

Anda harus mengangkat status UI ke ancestor umum terendah di antara semua composable yang membaca dan menulisnya. Anda harus mempertahankan status terdekat dengan tempat penggunaannya. Dari pemilik status, tampilkan status dan peristiwa yang tidak dapat diubah kepada konsumen untuk mengubah status.

Ancestor umum terendah juga bisa berada di luar Komposisi. Misalnya, saat mengangkat status di ViewModel karena logika bisnis digunakan.

Halaman ini menjelaskan praktik terbaik ini secara mendetail dan peringatan yang perlu diingat.

Jenis status UI dan logika UI

Di bawah ini adalah definisi untuk jenis status dan logika UI yang digunakan di seluruh dokumen ini.

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

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.

Logika UI

Saat logika UI perlu membaca atau menulis status, Anda harus mencakup status tersebut ke UI, mengikuti siklus prosesnya. Untuk mencapai ini, Anda harus mengangkat status pada tingkat yang benar dalam fungsi composable. Cara lain, Anda dapat melakukannya di class holder status biasa, yang juga mencakup siklus proses UI.

Di bawah ini adalah deskripsi untuk kedua solusi dan penjelasan tentang waktu yang tepat untuk menggunakannya.

Composable sebagai pemilik status

Memiliki logika UI dan status elemen UI dalam composable adalah pendekatan yang baik jika status dan logikanya sederhana. Anda dapat membiarkan status internal Anda menjadi composable atau mengangkatnya sesuai kebutuhan.

Tidak diperlukan pengangkatan status

Status pengangkatan tidak selalu diperlukan. Status dapat disimpan secara internal dalam composable saat tidak ada composable lain yang perlu mengontrolnya. Dalam cuplikan ini, ada composable yang diperluas dan diciutkan saat diketuk:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails } // Apply simple UI logic
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

Variabel showDetails adalah status internal untuk elemen UI ini. Variabel ini hanya dibaca dan diubah dalam composable ini dan logika yang diterapkan ke variabel ini sangat sederhana. Oleh karena itu, mengangkat status dalam hal ini tidak akan memberikan banyak manfaat sehingga Anda dapat membiarkannya tetap internal. Dengan melakukannya, composable ini akan menjadi pemilik dan satu sumber tepercaya dari status yang diperluas.

Pengangkatan dalam composable

Jika perlu membagikan status elemen UI dengan composable lain dan menerapkan logika UI di elemen lain, Anda dapat mengangkatnya lebih tinggi dalam hierarki UI. Hal ini juga akan membuat composable Anda lebih dapat digunakan kembali dan lebih mudah diuji.

Contoh berikut adalah aplikasi chat yang menerapkan dua bagian fungsi:

  • Tombol JumpToBottom men-scroll daftar pesan ke bawah. Tombol menjalankan logika UI pada status daftar.
  • Daftar MessagesList akan di-scroll ke bagian bawah setelah pengguna mengirim pesan baru. UserInput menjalankan logika UI pada status daftar.
Aplikasi chat dengan tombol JumpToBottom dan scroll ke bawah pada pesan baru
Gambar 1. Aplikasi chat dengan tombol JumpToBottom dan scroll ke bawah pada pesan baru

Hierarki composable adalah sebagai berikut:

Hierarki composable Chat
Gambar 2. Hierarki composable Chat

Status LazyColumn diangkat ke layar percakapan sehingga aplikasi dapat menjalankan logika UI dan membaca status dari semua composable yang memerlukannya:

Mengangkat status LazyColumn dari LazyColumn ke ConversationScreen
Gambar 3. Mengangkat status LazyColumn dari LazyColumn ke ConversationScreen

Jadi, composable dapat berupa:

Hierarki composable Chat dengan LazyListState yang diangkat ke ConversationScreen
Gambar 4. Hierarki composable Chat dengan LazyListState yang diangkat ke ConversationScreen

Kodenya adalah sebagai berikut ini:

@Composable
private fun ConversationScreen(/*...*/) {
    val scope = rememberCoroutineScope()

    val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen

    MessagesList(messages, lazyListState) // Reuse same state in MessageList

    UserInput(
        onMessageSent = { // Apply UI logic to lazyListState
            scope.launch {
                lazyListState.scrollToItem(0)
            }
        },
    )
}

@Composable
private fun MessagesList(
    messages: List<Message>,
    lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value
) {

    LazyColumn(
        state = lazyListState // Pass hoisted state to LazyColumn
    ) {
        items(messages, key = { message -> message.id }) { item ->
            Message(/*...*/)
        }
    }

    val scope = rememberCoroutineScope()

    JumpToBottom(onClicked = {
        scope.launch {
            lazyListState.scrollToItem(0) // UI logic being applied to lazyListState
        }
    })
}

LazyListState diangkat setinggi yang diperlukan untuk logika UI yang harus diterapkan. Karena diinisialisasi dalam fungsi composable, composable ini disimpan di Komposisi, mengikuti siklus prosesnya.

Perhatikan bahwa lazyListState ditentukan dalam metode MessagesList, dengan nilai default rememberLazyListState(). Ini adalah pola umum di Compose. Ini membuat composable lebih fleksibel dan dapat digunakan kembali. Selanjutnya, Anda dapat menggunakan composable di berbagai bagian aplikasi yang mungkin tidak perlu mengontrol status. Hal ini biasanya terjadi saat menguji atau melihat pratinjau composable. Inilah cara LazyColumn menentukan statusnya.

Ancestor umum terendah untuk LazyListState adalah ConversationScreen
Gambar 5. Ancestor umum terendah untuk LazyListState adalah ConversationScreen

Class holder status biasa sebagai pemilik status

Jika composable berisi logika UI kompleks yang melibatkan satu atau beberapa kolom status elemen UI, elemen tersebut harus mendelegasikan tanggung jawab tersebut ke holder status, seperti class holder status biasa. Hal ini membuat logika composable lebih mudah diuji secara terpisah, dan mengurangi kompleksitasnya. Pendekatan ini mendukung prinsip pemisahan fokus: composable bertanggung jawab mengirimkan elemen UI, dan holder status berisi logika UI dan status elemen UI.

Class holder status biasa menyediakan fungsi yang mudah bagi pemanggil fungsi composable, sehingga mereka tidak perlu menulis logika ini sendiri.

Class biasa ini dibuat dan diingat dalam Komposisi. Karena mengikuti siklus proses composable, holder status dapat menggunakan jenis yang disediakan oleh library Compose seperti rememberNavController() atau rememberLazyListState().

Contohnya adalah class holder status biasa LazyListState, yang diimplementasikan di Compose untuk mengontrol kompleksitas UI LazyColumn atau LazyRow.

// LazyListState.kt

@Stable
class LazyListState constructor(
    firstVisibleItemIndex: Int = 0,
    firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
    /**
     *   The holder class for the current scroll position.
     */
    private val scrollPosition = LazyListScrollPosition(
        firstVisibleItemIndex, firstVisibleItemScrollOffset
    )

    suspend fun scrollToItem(/*...*/) { /*...*/ }

    override suspend fun scroll() { /*...*/ }

    suspend fun animateScrollToItem() { /*...*/ }
}

LazyListState mengenkapsulasi status LazyColumn yang menyimpan scrollPosition untuk elemen UI ini. Tindakan ini juga mengekspos metode untuk mengubah posisi scroll, misalnya dengan men-scroll ke item tertentu.

Seperti yang dapat Anda lihat, menambah tanggung jawab composable akan meningkatkan kebutuhan untuk holder status. Tanggung jawab dapat berada di logika UI, atau hanya dalam jumlah status yang harus dilacak.

Pola umum lainnya adalah menggunakan class holder status biasa untuk menangani kompleksitas fungsi composable root di aplikasi. Anda dapat menggunakan class tersebut untuk mengenkapsulasi status tingkat aplikasi seperti status navigasi dan ukuran layar. Deskripsi lengkap ini dapat ditemukan di logika UI dan halaman holder statusnya.

Logika bisnis

Jika composable dan class holder status biasa bertanggung jawab atas logika UI dan status elemen UI, holder status tingkat layar bertanggung jawab atas tugas berikut:

  • Memberikan akses ke logika bisnis aplikasi yang biasanya ditempatkan di lapisan hierarki lain seperti lapisan bisnis dan data.
  • Menyiapkan data aplikasi untuk ditampilkan di layar tertentu, yang menjadi status UI layar.

ViewModel sebagai pemilik status

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

Saat mengangkat status UI di ViewModel, Anda memindahkannya ke luar Komposisi.

Status yang diangkat ke ViewModel disimpan di luar Komposisi.
Gambar 6. Status yang diangkat ke ViewModel disimpan di luar Komposisi.

ViewModels tidak disimpan sebagai bagian dari Komposisi. Library ini disediakan oleh framework dan dicakupkan ke ViewModelStoreOwner yang dapat berupa Aktivitas, Fragmen, grafik navigasi, atau tujuan grafik navigasi. Untuk mengetahui informasi selengkapnya tentang cakupan ViewModel, Anda dapat meninjau dokumentasi.

Kemudian, ViewModel adalah sumber kebenaran dan ancestor umum terendah untuk status UI.

Status UI Layar

Sesuai definisi di atas, status UI layar dihasilkan dengan menerapkan aturan bisnis. Mengingat bahwa pemegang status tingkat layar bertanggung jawab atas hal ini, ini berarti status UI layar biasanya diangkat di pemegang status tingkat layar, dalam hal ini ViewModel.

Pertimbangkan ConversationViewModel aplikasi chat dan caranya menampilkan status UI layar dan peristiwa untuk memodifikasinya:

class ConversationViewModel(
    channelId: String,
    messagesRepository: MessagesRepository
) : ViewModel() {

    val messages = messagesRepository
        .getLatestMessages(channelId)
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )

    // Business logic
    fun sendMessage(message: Message) { /* ... */ }
}

Composable menggunakan status UI layar yang diangkat di ViewModel. Anda harus memasukkan instance ViewModel dalam composable tingkat layar untuk memberikan akses ke logika bisnis.

Berikut ini adalah contoh ViewModel yang digunakan dalam composable tingkat layar. Di sini, ConversationScreen() composable menggunakan status UI layar yang diangkat di ViewModel:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

Penambahan properti ke setiap komponen bertingkat

“Penambahan properti ke setiap komponen bertingkat” mengacu pada penerusan data melalui beberapa komponen turunan bertingkat ke lokasi tempat properti tersebut dibaca.

Contoh umum tempat penambahan properti ke setiap komponen bertingkat dapat muncul di Compose adalah saat Anda memasukkan holder status tingkat layar di tingkat atas serta meneruskan status dan peristiwa ke composable turunan. Hal ini juga dapat menimbulkan kelebihan tanda tangan fungsi composable.

Meskipun mengekspos peristiwa sebagai parameter lambda individual dapat membebani tanda tangan fungsi, hal ini memaksimalkan visibilitas tanggung jawab fungsi composable. Anda dapat melihat fungsinya secara sekilas.

Penambahan properti ke setiap komponen bertingkat lebih baik daripada membuat class wrapper untuk mengenkapsulasi status dan peristiwa di satu tempat karena mengurangi visibilitas tanggung jawab composable. Dengan tidak memiliki class wrapper, Anda juga kemungkinan besar dapat meneruskan composable hanya yang diperlukan, yang merupakan praktik terbaik.

Praktik terbaik yang sama berlaku jika peristiwa ini adalah peristiwa navigasi, Anda dapat mempelajarinya lebih lanjut di dokumen navigasi.

Jika telah mengidentifikasi masalah performa, Anda juga dapat memilih untuk menunda pembacaan status. Anda dapat memeriksa dokumen performa untuk mempelajari lebih lanjut.

Status elemen UI

Anda dapat mengangkat status elemen UI ke holder status tingkat layar jika ada logika bisnis yang perlu membaca atau menulisnya.

Melanjutkan contoh aplikasi chat, aplikasi menampilkan saran pengguna dalam chat grup saat pengguna mengetik @ dan petunjuk. Saran tersebut berasal dari lapisan data dan logika untuk menghitung daftar saran pengguna dianggap sebagai logika bisnis. Fitur tersebut akan terlihat seperti ini:

Fitur yang menampilkan saran pengguna di chat grup saat pengguna mengetik `@` dan petunjuk
Gambar 7. Fitur yang menampilkan saran pengguna di chat grup saat pengguna mengetik @ dan petunjuk

ViewModel yang menerapkan fitur ini akan terlihat sebagai berikut:

class ConversationViewModel(/*...*/) : ViewModel() {

    // Hoisted state
    var inputMessage by mutableStateOf("")
        private set

    val suggestions: StateFlow<List<Suggestion>> =
        snapshotFlow { inputMessage }
            .filter { hasSocialHandleHint(it) }
            .mapLatest { getHandle(it) }
            .mapLatest { repository.getSuggestions(it) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = emptyList()
            )

    fun updateInput(newInput: String) {
        inputMessage = newInput
    }
}

inputMessage adalah variabel yang menyimpan status TextField. Setiap kali pengguna mengetik input baru, aplikasi memanggil logika bisnis untuk menghasilkan suggestions.

suggestions adalah status UI layar dan digunakan dari Compose UI dengan mengumpulkan dari StateFlow.

Peringatan

Untuk beberapa status elemen UI Compose, pengangkatan ke ViewModel mungkin memerlukan pertimbangan khusus. Misalnya, beberapa holder status elemen UI Compose mengekspos metode untuk mengubah status. Beberapa di antaranya mungkin adalah fungsi penangguhan yang memicu animasi. Fungsi penangguhan ini dapat menampilkan pengecualian jika Anda memanggilnya dari CoroutineScope yang tidak dicakupkan ke Komposisi.

Misalnya konten panel samping aplikasi bersifat dinamis dan Anda perlu mengambil serta memperbaruinya dari lapisan data setelah ditutup. Anda harus mengangkat status panel samping ke ViewModel sehingga Anda dapat memanggil UI dan logika bisnis di elemen ini dari pemilik status.

Namun, memanggil metode close() dari DrawerState menggunakan viewModelScope dari Compose UI menyebabkan IllegalStateException pengecualian jenis runtime dengan pesan yang bertuliskan “ MonotonicFrameClock tidak tersedia di CoroutineContext” ini.

Untuk memperbaikinya, gunakan CoroutineScope yang dicakupkan ke Komposisi. Fungsi ini memberikan MonotonicFrameClock di CoroutineContext yang diperlukan agar fungsi penangguhan berfungsi.

Untuk memperbaiki error ini, alihkan CoroutineContext coroutine di ViewModel ke yang dicakupkan ke Komposisi. Ikon tampak seperti berikut:

class ConversationViewModel(/*...*/) : ViewModel() {

    val drawerState = DrawerState(initialValue = DrawerValue.Closed)

    private val _drawerContent = MutableStateFlow(DrawerContent.Empty)
    val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow()

    fun closeDrawer(uiScope: CoroutineScope) {
        viewModelScope.launch {
            withContext(uiScope.coroutineContext) { // Use instead of the default context
                drawerState.close()
            }
            // Fetch drawer content and update state
            _drawerContent.update { content }
        }
    }
}

// in Compose
@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {
    val scope = rememberCoroutineScope()

    ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) })
}

Pelajari lebih lanjut

Untuk mempelajari status dan Jetpack Compose lebih lanjut, lihat referensi tambahan berikut.

Contoh

Codelab

Video