Peran UI adalah menampilkan data aplikasi di layar. UI juga berfungsi sebagai titik utama interaksi pengguna. Setiap kali data berubah, baik karena interaksi pengguna (seperti menekan tombol) atau input eksternal (seperti respons jaringan), UI akan diupdate untuk mencerminkan perubahan tersebut. Secara efektif, UI adalah representasi visual dari status aplikasi yang diambil dari lapisan data.
Namun, data aplikasi yang Anda dapatkan dari lapisan data biasanya dalam format yang berbeda dari informasi yang perlu ditampilkan. Misalnya, Anda mungkin hanya memerlukan bagian data untuk UI, atau Anda mungkin perlu menggabungkan dua sumber data yang berbeda untuk menyajikan informasi yang relevan bagi pengguna. Terlepas dari logika yang Anda terapkan, Anda harus meneruskan semua informasi yang diperlukan ke UI untuk merender sepenuhnya. Lapisan UI adalah pipeline yang mengonversi perubahan data aplikasi menjadi bentuk yang dapat ditampilkan oleh UI, lalu menampilkannya.
Studi kasus dasar
Pertimbangkan aplikasi yang mengambil artikel berita untuk dibaca oleh pengguna. Aplikasi ini memiliki layar artikel yang menyajikan artikel untuk dibaca, dan juga memungkinkan pengguna yang login membuat bookmark artikel yang benar-benar menarik. Mengingat mungkin ada banyak artikel kapan saja, pembaca harus dapat menjelajahi artikel menurut kategori. Singkatnya, aplikasi memungkinkan pengguna melakukan hal berikut:
- Melihat artikel yang tersedia untuk dibaca.
- Menjelajahi artikel menurut kategori.
- Login dan membuat bookmark artikel tertentu.
- Mengakses beberapa fitur premium jika memenuhi syarat.
Bagian berikut menggunakan contoh ini sebagai studi kasus untuk memperkenalkan prinsip-prinsip aliran data searah, serta menggambarkan masalah-masalah yang diatasi dengan bantuan prinsip-prinsip tersebut dalam konteks arsitektur aplikasi untuk lapisan UI.
Arsitektur lapisan UI
Istilah UI mengacu pada elemen UI seperti penampung dan fungsi composable yang menampilkan data. Untuk membuat UI Android, toolkit yang direkomendasikan adalah Jetpack Compose. Karena peran lapisan data adalah menahan, mengelola, dan memberikan akses ke data aplikasi, lapisan UI harus melakukan langkah-langkah berikut:
- Gunakan data aplikasi dan ubah menjadi data yang dapat dirender dengan mudah oleh UI.
- Gunakan data yang dapat dirender UI dan ubah menjadi elemen UI untuk ditampilkan kepada pengguna.
- Gunakan peristiwa input pengguna dari elemen UI yang telah disusun dan cerminkan efeknya dalam data UI sesuai kebutuhan.
- Ulangi langkah 1 sampai 3 selama diperlukan.
Bagian selanjutnya dari panduan ini menunjukkan cara menerapkan lapisan UI yang menjalankan langkah-langkah ini. Secara khusus, panduan ini mencakup tugas dan konsep berikut:
- Cara menentukan status UI
- Aliran data searah (UDF) sebagai sarana untuk memproduksi dan mengelola status UI
- Cara mengekspos status UI dengan jenis data yang dapat diamati sesuai prinsip UDF
- Cara menerapkan UI yang menggunakan status UI yang dapat diamati
Yang paling mendasar adalah definisi status UI.
Menentukan status UI
Dalam studi kasus yang diuraikan sebelumnya, UI menampilkan daftar artikel beserta beberapa metadata untuk setiap artikel. Informasi ini yang ditampilkan aplikasi kepada pengguna adalah status UI.
Dengan kata lain, jika UI adalah apa yang dilihat pengguna, status UI adalah tampilan berdasarkan apa yang diberitahukan aplikasi. Seperti dua sisi koin yang sama, UI adalah representasi visual status UI. Setiap perubahan pada status UI akan segera ditampilkan di UI.
Pertimbangkan studi kasus: untuk memenuhi persyaratan aplikasi Berita, informasi yang diperlukan untuk merender UI sepenuhnya dapat dienkapsulasi dalam class data NewsUiState yang ditentukan sebagai berikut:
data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf(),
val userMessages: List<Message> = listOf()
)
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
...
)
Untuk mengetahui informasi selengkapnya tentang status UI, lihat Status dan Jetpack Compose.
Ketetapan
Definisi status UI dalam contoh sebelumnya tidak dapat diubah. Manfaat utama dari hal ini adalah objek yang tidak dapat diubah memberikan jaminan terkait status aplikasi secara instan. Tindakan ini akan mengosongkan UI untuk berfokus pada peran utamanya: membaca status dan mengubah elemen UI-nya sebagaimana mestinya. Jangan pernah memodifikasi status UI secara langsung di UI, kecuali jika UI tersebut adalah satu-satunya sumber datanya. Melanggar prinsip ini akan menghasilkan beberapa sumber kebenaran untuk informasi yang sama, sehingga mengakibatkan inkonsistensi data dan bug halus.
Misalnya, pertimbangkan studi kasus sebelumnya.
Jika flag bookmarked dalam objek NewsItemUiState dari status UI diperbarui di class Activity, flag tersebut akan bersaing dengan lapisan data sebagai sumber status bookmark sebuah artikel. Class data yang tidak dapat diubah sangat berguna untuk mencegah jenis
inkonsistensi ini.
Konvensi penamaan dalam panduan ini
Dalam panduan ini, class status UI diberi nama berdasarkan fungsi layar atau bagian layar yang dijelaskan. Konvensinya adalah sebagai berikut:
fungsi + UiState.
Misalnya, status layar yang menampilkan berita mungkin disebut
NewsUiState, dan status item berita dalam daftar item berita mungkin adalah
NewsItemUiState.
Mengelola status dengan Aliran Data Searah
Bagian sebelumnya menetapkan bahwa status UI adalah snapshot yang tidak dapat diubah dari detail yang diperlukan oleh UI untuk dirender. Namun, sifat data yang dinamis dalam aplikasi berarti status dapat berubah dari waktu ke waktu. Hal ini mungkin disebabkan oleh interaksi pengguna atau peristiwa lain yang mengubah data yang mendasari yang digunakan untuk mengisi aplikasi.
Interaksi ini dapat memanfaatkan mediator untuk memprosesnya, dengan menentukan logika yang akan diterapkan pada setiap peristiwa dan mengubah sumber data pendukung untuk membuat status UI. Meskipun interaksi ini dan logikanya dapat ditempatkan di UI itu sendiri, cara ini bisa jadi sulit karena UI memikul terlalu banyak tanggung jawab. Selain itu, hal ini dapat memengaruhi kemampuan pengujian karena kode yang dihasilkan sangat erat. Kecuali status UI sangat sederhana, pastikan tanggung jawab penuh UI adalah menggunakan dan menampilkan status UI.
Bagian ini membahas Aliran Data Searah (UFD), yaitu pola arsitektur yang membantu menegakkan pemisahan tanggung jawab yang sehat ini.
Holder status
Holder status adalah class yang bertanggung jawab untuk menghasilkan status UI dan logika yang diperlukan untuk menghasilkan status tersebut. Holder status tersedia dalam berbagai ukuran, bergantung pada cakupan elemen UI terkait yang dikelolanya, mulai dari satu widget seperti panel aplikasi bawah hingga keseluruhan layar atau tujuan navigasi.
Pada kasus terakhir, implementasi standarnya adalah instance ViewModel, meskipun bergantung pada persyaratan aplikasi, class sederhana mungkin sudah cukup. Misalnya, aplikasi Berita dari studi kasus menggunakan class NewsViewModel sebagai holder status untuk menghasilkan status UI untuk layar yang ditampilkan di bagian tersebut.
Ada banyak cara untuk membuat model kodependensi antara UI dan pembuat
statusnya. Namun, karena interaksi antara UI dan class ViewModel-nya
sangat dapat dipahami sebagai input peristiwa dan output status berikutnya,
hubungan tersebut dapat ditampilkan seperti dalam diagram berikut:
Pola aliran status ke bawah dan peristiwa yang mengalir ke atas disebut aliran data searah (UDF). Implikasi dari pola ini untuk arsitektur aplikasi adalah sebagai berikut:
- ViewModel menyimpan dan mengekspos status yang akan digunakan oleh UI. Status UI adalah data aplikasi yang diubah oleh ViewModel.
- UI memberi tahu ViewModel tentang peristiwa pengguna.
- ViewModel menangani tindakan pengguna dan memperbarui status.
- Status yang diperbarui dimasukkan kembali ke UI untuk dirender.
- Semua tindakan di atas diulang untuk setiap peristiwa yang menyebabkan mutasi status.
Untuk tujuan atau layar navigasi, ViewModel berfungsi dengan repositori atau class kasus penggunaan untuk mendapatkan data dan mengubahnya menjadi status UI sekaligus menggabungkan efek dari peristiwa yang dapat menyebabkan mutasi status. Studi kasus yang disebutkan sebelumnya berisi daftar artikel, setiap artikel memiliki judul, deskripsi, sumber, nama penulis, tanggal publikasi, dan apakah artikel tersebut di-bookmark atau tidak. UI untuk setiap item artikel akan terlihat seperti ini:
Pengguna yang mengajukan permintaan untuk mem-bookmark artikel adalah contoh peristiwa yang dapat menyebabkan mutasi status. Sebagai pembuat status, ViewModel bertanggung jawab untuk menentukan semua logika yang diperlukan untuk mengisi semua kolom dalam status UI dan memproses peristiwa yang diperlukan oleh UI untuk dirender sepenuhnya.
Bagian berikut ini membahas peristiwa yang menyebabkan perubahan status dan cara memprosesnya menggunakan UDF lebih lanjut.
Jenis logika
Mem-bookmark artikel adalah contoh logika bisnis karena memberikan nilai bagi aplikasi Anda. Untuk mempelajari hal ini lebih lanjut, lihat halaman lapisan data. Namun, ada berbagai jenis logika yang penting untuk ditentukan:
- Logika bisnis adalah penerapan persyaratan produk untuk data aplikasi. Seperti yang sudah disebutkan, satu contoh adalah mem-bookmark artikel di aplikasi studi kasus. Logika bisnis biasanya ditempatkan di lapisan domain atau data, tetapi tidak pernah di lapisan UI.
- Logika perilaku UI atau logika UI adalah cara menampilkan perubahan status di
layar. Contohnya termasuk mendapatkan teks yang tepat untuk ditampilkan di layar
menggunakan
ResourcesAndroid, membuka layar tertentu saat pengguna mengklik tombol, atau menampilkan pesan pengguna di layar menggunakan toast atau snackbar.
Simpan logika UI di UI, bukan di ViewModel, terutama saat melibatkan
jenis UI seperti Context.
Jika UI semakin kompleks dan Anda ingin mendelegasikan logika
UI ke class lain untuk mendukung pengujian dan pemisahan fokus, Anda
dapat membuat class sederhana sebagai holder status. Class sederhana yang dibuat di UI
dapat menggunakan dependensi Android SDK karena mengikuti siklus proses UI;
objek ViewModel memiliki masa aktif yang lebih lama.
Untuk informasi selengkapnya tentang holder status dan kecocokannya dengan konteks membantu mem-build UI, lihat panduan Status Jetpack Compose.
Apa alasan menggunakan UDF?
UDF membuat model siklus produksi status seperti yang ditunjukkan pada Gambar 4. UDF juga memisahkan tempat asal perubahan status, tempat perubahan tersebut dilakukan, dan tempat perubahan tersebut akhirnya digunakan. Pemisahan ini memungkinkan UI melakukan persis seperti namanya: menampilkan informasi dengan mengamati perubahan status, dan menyampaikan intent pengguna dengan meneruskan perubahan tersebut ke ViewModel.
Dengan kata lain, UDF memungkinkan hal berikut:
- Konsistensi data. Ada satu sumber kebenaran untuk UI.
- Kemampuan untuk diuji. Sumber status diisolasi sehingga dapat diuji secara terpisah dari UI.
- Kemudahan pemeliharaan. Mutasi status mengikuti pola yang ditetapkan dengan baik di mana mutasi disebabkan oleh peristiwa pengguna dan sumber data yang ditarik dari mutasi tersebut.
Mengekspos status UI
Setelah Anda menentukan status UI dan menentukan cara mengelola produksi status tersebut, langkah berikutnya adalah menampilkan status yang dihasilkan ke UI.
Saat menggunakan UDF untuk mengelola produksi status, Anda dapat menganggap status yang dihasilkan sebagai aliran—dengan kata lain, beberapa versi status dihasilkan dari waktu ke waktu. Ekspos status UI dalam
holder data yang dapat diamati seperti StateFlow. Hal ini memungkinkan UI bereaksi terhadap perubahan apa pun yang dilakukan dalam status tanpa harus menarik data secara manual langsung dari ViewModel. Hal ini juga memiliki
manfaat karena selalu memiliki versi terbaru status UI yang di-cache, yang
berguna untuk pemulihan status cepat setelah perubahan konfigurasi.
class NewsViewModel(...) : ViewModel() {
val uiState: NewsUiState = …
}
Untuk pengantar tentang alur Kotlin, lihat Alur Kotlin di Android.
Untuk mempelajari cara menggunakan StateFlow sebagai penampung data yang dapat diamati, lihat codelab Status Lanjutan dan Efek Samping di Jetpack Compose.
Jika data yang diekspos ke UI relatif sederhana, sebaiknya gabungkan data dalam jenis status UI karena hal ini menyampaikan hubungan antara pengiriman holder status dan layar atau elemen UI yang terkait. Seiring dengan semakin kompleksnya elemen UI, akan mudah untuk menambahkan definisi status UI, sehingga Anda dapat mengakomodasi informasi tambahan yang diperlukan untuk merender elemen UI.
Cara umum untuk membuat aliran UiState adalah dengan mengekspos properti mutableStateOf dengan private set, sehingga status tetap dapat diubah di dalam ViewModel, tetapi hanya dapat dibaca untuk UI.
class NewsViewModel(...) : ViewModel() {
var uiState by mutableStateOf(NewsUiState())
private set
...
}
ViewModel kemudian dapat mengekspos metode yang mengubah status secara internal, dengan memublikasikan update untuk UI yang akan digunakan. Misalnya, kasus saat
Anda perlu melakukan tindakan asinkron.
Anda dapat meluncurkan coroutine menggunakan viewModelScope,
lalu memperbarui status yang dapat diubah setelah selesai.
class NewsViewModel(
private val repository: NewsRepository,
...
) : ViewModel() {
var uiState by mutableStateOf(NewsUiState())
private set
private var fetchJob: Job? = null
fun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsForCategory(category)
uiState = uiState.copy(newsItems = newsItems)
} catch (ioe: IOException) {
// Handle the error and notify the UI when appropriate.
val messages = getMessagesFromThrowable(ioe)
uiState = uiState.copy(userMessages = messages)
}
}
}
}
Pada contoh sebelumnya, class NewsViewModel mencoba mengambil artikel untuk
kategori tertentu, lalu menampilkan hasil upaya—apakah berhasil atau
gagal—dalam status UI, tempat UI dapat bereaksi terhadapnya dengan tepat.
Untuk informasi selengkapnya tentang penanganan error, lihat bagian
Menampilkan error di layar.
Pertimbangan tambahan
Selain panduan sebelumnya, pertimbangkan hal-hal berikut saat menampilkan status UI:
Gunakan satu objek status UI untuk menangani status yang terkait satu sama lain. Hal ini menyebabkan inkonsistensi yang lebih sedikit dan membuat kode lebih mudah dipahami. Jika Anda mengekspos daftar item berita dan jumlah bookmark dalam dua aliran yang berbeda, Anda mungkin akan berakhir dalam situasi ketika satu aliran diperbarui dan yang lainnya tidak. Jika Anda menggunakan satu aliran, kedua elemen akan terus diperbarui. Selain itu, beberapa logika bisnis mungkin memerlukan kombinasi sumber. Misalnya, Anda mungkin perlu menampilkan tombol bookmark hanya jika pengguna login dan pengguna tersebut adalah pelanggan layanan berita premium. Anda dapat menentukan class status UI sebagai berikut:
data class NewsUiState( val isSignedIn: Boolean = false, val isPremium: Boolean = false, val newsItems: List<NewsItemUiState> = listOf() ) val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremiumDalam deklarasi ini, visibilitas tombol bookmark adalah properti turunan dari dua properti lainnya. Karena logika bisnis semakin kompleks, memiliki class
UiStatetunggal tempat semua properti langsung tersedia menjadi semakin penting.Status UI: satu atau beberapa aliran? Prinsip panduan utama untuk memilih antara mengekspos status UI dalam satu atau beberapa aliran adalah hubungan antara item yang ditampilkan. Keuntungan terbesar dari eksposur aliran tunggal adalah kemudahan dan konsistensi data: pengguna status selalu memiliki informasi terbaru yang tersedia kapan saja. Namun, ada kalanya aliran status terpisah dari ViewModel mungkin sesuai:
Jenis data yang tidak terkait: Beberapa status yang diperlukan untuk merender UI mungkin tidak saling bergantung satu sama lain. Dalam kasus seperti itu, biaya menggabungkan berbagai status ini mungkin lebih besar daripada manfaatnya, terutama jika salah satu status ini diperbarui lebih sering daripada yang lain.
Diffing
UiState: Semakin banyak kolom dalam objekUiState, makin besar kemungkinan aliran data akan dimunculkan sebagai hasil dari salah satu kolomnya sedang diperbarui. Karena elemen UI tidak memiliki mekanisme diffing untuk memahami apakah pengiriman berturut-turut berbeda atau sama, setiap pengiriman menyebabkan pembaruan pada elemen UI. Artinya, mitigasi menggunakan metode APIFlowsepertidistinctUntilChanged()mungkin diperlukan.
Untuk mengetahui informasi selengkapnya tentang rendering dan status UI, lihat Siklus proses composable.
Menggunakan status UI
Untuk menggunakan aliran objek UiState di UI, gunakan operator terminal untuk jenis data yang dapat diamati yang Anda gunakan. Misalnya, untuk alur Kotlin, gunakan metode collect() atau variasinya.
Saat menggunakan holder data yang dapat diamati di UI, pastikan untuk mempertimbangkan siklus proses UI. Jangan membuat UI mengamati status UI saat composable
tidak ditampilkan kepada pengguna. Untuk mempelajari topik ini lebih lanjut, lihat
postingan blog ini. Saat menggunakan alur, sebaiknya tangani masalah siklus proses dengan
cakupan coroutine yang sesuai dan collectAsStateWithLifecycle API:
@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) /* ... */ }
Menampilkan operasi yang sedang berlangsung
Cara mudah untuk merepresentasikan status pemuatan di class UiState adalah dengan
kolom boolean:
data class NewsUiState(
val isFetchingArticles: Boolean = false,
...
)
Nilai flag ini menunjukkan ada tidaknya status progres di UI.
@Composable
fun LatestNewsScreen(
modifier: Modifier = Modifier,
viewModel: NewsViewModel = viewModel()
) {
Box(modifier.fillMaxSize()) {
if (viewModel.uiState.isFetchingArticles) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
// Add other UI elements. For example, the list.
}
}
Menampilkan error di layar
Menampilkan error di UI mirip dengan menampilkan operasi yang sedang berlangsung karena keduanya direpresentasikan dengan mudah oleh nilai boolean yang menunjukkan kehadiran atau ketiadaannya. Akan tetapi, error juga dapat mencakup pesan terkait untuk disampaikan kembali ke pengguna, atau tindakan yang terkait dengan percobaan kembali operasi yang gagal. Oleh karena itu, saat operasi yang sedang berlangsung sedang memuat atau tidak memuat, status error mungkin perlu dimodelkan dengan class data yang menghosting metadata yang sesuai untuk konteks error.
Pertimbangkan contoh sebelumnya yang menampilkan status progres saat mengambil artikel. Jika operasi ini menghasilkan error, Anda mungkin ingin menampilkan satu atau beberapa pesan kepada pengguna yang menjelaskan error tersebut.
data class Message(val id: Long, val message: String)
data class NewsUiState(
val userMessages: List<Message> = listOf(),
...
)
Kemudian, Anda dapat menampilkan pesan error kepada pengguna dalam bentuk elemen UI seperti snackbar. Untuk mengetahui informasi selengkapnya tentang cara peristiwa UI dihasilkan dan digunakan, lihat peristiwa UI.
Threading dan konkurensi
Pastikan semua pekerjaan yang dilakukan di ViewModel bersifat main-safe—aman untuk dipanggil dari thread utama. Lapisan data dan domain bertanggung jawab untuk memindahkan pekerjaan ke thread yang berbeda.
Jika ViewModel menjalankan operasi yang berjalan lama, tugas tersebut juga bertanggung jawab untuk memindahkan logika tersebut ke thread latar belakang. Coroutine Kotlin adalah cara bagus untuk mengelola operasi serentak, dan Komponen Arsitektur Jetpack menyediakan dukungan bawaan untuk hal itu. Untuk mempelajari penggunaan coroutine di aplikasi Android lebih lanjut, lihat Coroutine Kotlin di Android.
Navigasi
Perubahan dalam navigasi aplikasi sering kali didorong oleh pengiriman seperti peristiwa. Misalnya,
setelah class SignInViewModel menjalankan login, kolom UiState mungkin memiliki
kolom isSignedIn yang ditetapkan ke true. Gunakan pemicu seperti ini seperti yang tercakup dalam bagian Menggunakan status UI sebelumnya, tetapi tunda penerapan konsumsi ke Komponen navigasi.
Untuk mengetahui informasi selengkapnya tentang navigasi UI, lihat Navigation 3.
Paging
Library Paging
digunakan di UI dengan jenis yang disebut PagingData. Karena PagingData
mewakili dan berisi item yang dapat berubah dari waktu ke waktu—dengan kata lain, jenis ini
tidak dapat diubah—jangan menampilkannya dalam status UI yang tidak dapat diubah.
Sebagai gantinya, ekspos dari ViewModel secara independen di alirannya
sendiri.
Contoh berikut menunjukkan Compose API library Paging:
@Composable fun MyScreen(flow: Flow<PagingData<String>>) { val lazyPagingItems = flow.collectAsLazyPagingItems() LazyColumn { items( lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it } ) { index -> val item = lazyPagingItems[index] Text("Item is $item") } } }
Animasi
Untuk memberikan transisi navigasi level atas yang lancar, Anda mungkin harus menunggu layar kedua memuat data sebelum memulai animasi.
Untuk mengetahui informasi selengkapnya tentang transisi navigasi, lihat Navigation 3 dan Transisi elemen bersama di Compose.
Referensi lainnya
Melihat konten
Contoh
Contoh Google berikut menunjukkan penggunaan lapisan UI. Jelajahi untuk melihat panduan ini dalam praktik:
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Produksi Status UI
- Holder status dan Status UI {:#mad-arch}
- Panduan untuk arsitektur aplikasi