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
danset
dari classTextView
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 adalahScaffoldState
untuk composableScaffold
.
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](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-chat.png?authuser=9&hl=id)
JumpToBottom
dan scroll ke bawah pada pesan baruHierarki composable adalah sebagai berikut:
![Hierarki composable Chat](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-initial-tree.png?authuser=9&hl=id)
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](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-animated.gif?authuser=9&hl=id)
LazyColumn
dari LazyColumn
ke ConversationScreen
Jadi, composable dapat berupa:
![Hierarki composable Chat dengan LazyListState yang diangkat ke ConversationScreen](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-passing-state.png?authuser=9&hl=id)
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](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-lca.png?authuser=9&hl=id)
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.](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-vm.png?authuser=9&hl=id)
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](https://developer.android.com/static/develop/ui/compose/images/state-hoisting-suggestions.png?authuser=9&hl=id)
@
dan petunjukViewModel
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
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Menyimpan status UI di Compose
- Daftar dan petak
- Merancang Compose UI Anda