UI modern jarang bersifat statis. Status UI berubah saat pengguna berinteraksi dengan UI atau saat aplikasi harus menampilkan data baru.
Dokumen ini menentukan pedoman untuk produksi dan pengelolaan status UI. Tujuannya adalah untuk membantu Anda memahami hal berikut:
- API yang akan digunakan untuk menghasilkan status UI. Jenis API tersebut bergantung pada sifat sumber perubahan status yang tersedia di holder status Anda, dengan mengikuti prinsip aliran data searah.
- Cara menentukan cakupan produksi status UI untuk memahami resource sistem.
- Cara menampilkan status UI untuk pemakaian oleh UI.
Pada dasarnya, produksi status adalah penerapan inkremental dari perubahan ini pada status UI. Status selalu ada, dan berubah sebagai akibat dari peristiwa. Perbedaan antara peristiwa dan status dirangkum dalam tabel di bawah:
| Peristiwa | Status |
|---|---|
| Bersifat sementara, tidak dapat diprediksi, dan ada untuk periode yang terbatas. | Selalu ada. |
| Input produksi status. | Output produksi status. |
| Produk UI atau sumber lainnya. | Digunakan oleh UI. |
Agar mudah diingat, hal-hal di atas dapat dirangkum sebagai berikut: status adalah; peristiwa terjadi. Diagram di bawah ini membantu memvisualisasikan perubahan status saat peristiwa terjadi di linimasa. Setiap peristiwa diproses oleh holder status yang sesuai dan menghasilkan perubahan status:
Peristiwa dapat berasal dari berikut ini:
- Pengguna: Saat pengguna berinteraksi dengan UI aplikasi.
- Sumber perubahan status lainnya: API yang menampilkan data aplikasi dari UI, domain, atau lapisan data seperti peristiwa waktu tunggu Snackbar, kasus penggunaan, atau repositori.
Pipeline produksi status UI
Produksi status di aplikasi Android dapat dianggap sebagai pipeline pemrosesan yang terdiri dari:
- Input: Sumber perubahan status. Nilai ini dapat berupa:
- Lokal untuk lapisan UI: Dapat berupa peristiwa pengguna seperti pengguna yang memasukkan
judul untuk "daftar tugas" di aplikasi pengelolaan tugas, atau API yang menyediakan
akses ke logika UI yang memicu perubahan dalam status UI—misalnya,
memanggil metode
opendiDrawerStatedi Jetpack Compose. - Eksternal ke lapisan UI: Merupakan sumber dari lapisan domain atau
data yang menyebabkan perubahan pada status UI—misalnya, berita yang selesai
dimuat dari
NewsRepositoryatau peristiwa lainnya. - Kombinasi dari hal-hal di atas.
- Lokal untuk lapisan UI: Dapat berupa peristiwa pengguna seperti pengguna yang memasukkan
judul untuk "daftar tugas" di aplikasi pengelolaan tugas, atau API yang menyediakan
akses ke logika UI yang memicu perubahan dalam status UI—misalnya,
memanggil metode
- Holder status: Jenis yang menerapkan logika bisnis dan logika UI ke sumber perubahan status, dan yang memproses peristiwa pengguna untuk menghasilkan status UI.
- Output: Status UI yang dapat dirender oleh aplikasi untuk memberikan informasi yang dibutuhkan pengguna.
API produksi status
Ada dua API utama yang digunakan dalam produksi status, bergantung pada tahap pipeline yang sedang Anda lalui:
| Tahap pipeline | API |
|---|---|
| Input | Gunakan API asinkron seperti Coroutine dan Flow untuk menjalankan pekerjaan di luar UI thread agar UI tetap bebas jank. |
| Output | Gunakan API holder data yang dapat diamati seperti Status Compose atau StateFlow untuk membatalkan validasi dan merender ulang UI saat status berubah. Holder data yang dapat diamati memastikan UI selalu memiliki status UI untuk ditampilkan di layar. |
Pilihan API asinkron untuk input memiliki pengaruh yang lebih besar terhadap sifat pipeline produksi status daripada pilihan API yang dapat diamati untuk output. Hal ini karena input mendikte jenis pemrosesan yang dapat diterapkan ke pipeline.
Penyusunan pipeline produksi status
Bagian berikutnya membahas teknik produksi status yang paling sesuai untuk berbagai input, dan API output yang cocok. Setiap pipeline produksi status merupakan kombinasi input dan output dan harus berupa:
- Memperhatikan siklus proses: Jika UI tidak terlihat atau aktif, pipeline produksi status tidak boleh menggunakan resource apa pun kecuali jika secara eksplisit diperlukan.
- Mudah digunakan: UI harus dapat dengan mudah merender status UI yang dihasilkan. Di Jetpack Compose, penggunaan status sangat penting bagi UI, karena composable dapat diperbarui berdasarkan perubahan status.
Input dalam pipeline produksi status
Input dalam pipeline produksi status memberikan sumber perubahan status melalui hal berikut:
- Operasi satu kali yang dapat bersifat sinkron atau asinkron—misalnya,
panggilan ke fungsi
suspend. - Stream API—misalnya,
Flows. - Semua yang di atas.
Bagian berikut membahas cara menyusun pipeline produksi status untuk setiap input di atas.
API satu kali sebagai sumber perubahan status
Mengelola status dengan penampung data yang dapat diamati. Gunakan mutableStateOf API, terutama saat menggunakan API teks Compose. Untuk pengelolaan status yang lebih kompleks atau saat berintegrasi dengan komponen arsitektur lain, gunakan API MutableStateFlow. Kedua API menawarkan metode yang memungkinkan update atomik
yang aman pada nilai yang dihosting, baik update tersebut bersifat sinkron
maupun asinkron.
Misalnya, pertimbangkan update status di aplikasi lempar dadu sederhana. Setiap lemparan
dadu dari pengguna akan memanggil metode Random.nextInt sinkron,
dan hasilnya ditulis ke dalam status UI.
Status Compose
@Stable
interface DiceUiState {
val firstDieValue: Int?
val secondDieValue: Int?
val numberOfRolls: Int?
}
private class MutableDiceUiState: DiceUiState {
override var firstDieValue: Int? by mutableStateOf(null)
override var secondDieValue: Int? by mutableStateOf(null)
override var numberOfRolls: Int by mutableStateOf(0)
}
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
_uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
_uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
StateFlow
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Mengubah status UI dari panggilan asinkron
Untuk perubahan status yang memerlukan hasil asinkron, luncurkan Coroutine di
CoroutineScope yang sesuai. Tindakan ini memungkinkan aplikasi menghapus pekerjaan saat
CoroutineScope dibatalkan. Holder status kemudian akan menulis hasil
panggilan metode penangguhan ke API yang dapat diamati yang digunakan untuk menampilkan status UI.
Misalnya, pertimbangkan AddEditTaskViewModel dalam
contoh Arsitektur. Jika metode saveTask penangguhan menyimpan tugas
secara asinkron, metode update di MutableStateFlow akan menyebarkan
perubahan status ke status UI.
Status Compose
@Stable
interface AddEditTaskUiState {
val title: String
val description: String
val isTaskCompleted: Boolean
val isLoading: Boolean
val userMessage: String?
val isTaskSaved: Boolean
}
private class MutableAddEditTaskUiState : AddEditTaskUiState() {
override var title: String by mutableStateOf("")
override var description: String by mutableStateOf("")
override var isTaskCompleted: Boolean by mutableStateOf(false)
override var isLoading: Boolean by mutableStateOf(false)
override var userMessage: String? by mutableStateOf<String?>(null)
override var isTaskSaved: Boolean by mutableStateOf(false)
}
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableAddEditTaskUiState()
val uiState: AddEditTaskUiState = _uiState
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.isTaskSaved = true
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.userMessage = getErrorMessage(exception))
}
}
}
}
StateFlow
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: String? = null,
val isTaskSaved: Boolean = false
)
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.update {
it.copy(isTaskSaved = true)
}
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.update {
it.copy(userMessage = getErrorMessage(exception))
}
}
}
}
}
Mengubah status UI dari thread latar belakang
Sebaiknya luncurkan Coroutine pada dispatcher utama untuk produksi
status UI—yaitu, di luar blok withContext dalam cuplikan kode di bawah.
Namun, jika harus memperbarui status UI dalam konteks latar belakang yang berbeda,
Anda dapat melakukan hal berikut:
- Gunakan metode
withContextuntuk menjalankan Coroutine dalam konteks serentak yang berbeda. - Saat menggunakan
MutableStateFlow, gunakan metodeupdateseperti biasa. - Saat menggunakan Status Compose, gunakan metode
Snapshot.withMutableSnapshotuntuk memastikan update atomik ke Status dalam konteks serentak.
Misalnya, asumsikan bahwa dalam cuplikan DiceRollViewModel di bawah,
SlowRandom.nextInt adalah fungsi suspend yang membutuhkan banyak komputasi, yang
perlu dipanggil dari Coroutine yang terikat ke CPU.
Status Compose
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
Snapshot.withMutableSnapshot {
_uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
}
}
}
StateFlow
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
}
}
API stream sebagai sumber perubahan status
Untuk sumber perubahan status yang menghasilkan beberapa nilai seiring waktu dalam stream, agregasi output semua sumber menjadi satu kesatuan yang kohesif merupakan pendekatan yang mudah untuk produksi status.
Saat menggunakan Flow Kotlin, Anda dapat mencapainya dengan fungsi combine.
Contohnya dapat dilihat dalam contoh"Now in Android" di
InterestsViewModel:
class InterestsViewModel(
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
}
Dengan menggunakan operator stateIn untuk membuat StateFlows, UI dapat
mengontrol aktivitas pipeline produksi status dengan lebih terperinci karena mungkin hanya
perlu aktif saat UI terlihat.
- Gunakan
SharingStarted.WhileSubscribedjika pipeline hanya perlu aktif saat UI terlihat sambil mengumpulkan alur dengan cara yang mendukung siklus proses. - Gunakan
SharingStarted.Lazilyjika pipeline harus aktif selama pengguna dapat kembali ke UI—yaitu, UI berada di data sebelumnya atau di tab lain di luar layar.
Jika agregasi sumber status berbasis stream tidak berlaku, API streaming seperti Flow Kotlin akan menawarkan beragam rangkaian transformasi seperti penggabungan, perataan, dan sebagainya untuk membantu pemrosesan streaming menjadi status UI.
API satu kali dan stream sebagai sumber perubahan status
Jika pipeline produksi status bergantung pada panggilan satu kali dan streaming sebagai sumber perubahan status, streaming akan menjadi batasan penentunya. Oleh karena itu, konversikan panggilan satu kali ke API stream, atau transfer outputnya ke dalam stream dan lanjutkan pemrosesan seperti yang dijelaskan di bagian stream di atas.
Dengan flow, tindakan ini biasanya berarti membuat satu atau beberapa instance MutableStateFlow
pendukung pribadi untuk menyebarkan perubahan status. Anda juga dapat membuat
alur snapshot dari status Compose.
Pertimbangkan TaskDetailViewModel dari repositori architecture-samples. Status UI bergantung pada aliran untuk tugas saat ini (_task) dan
sumber satu kali (_isTaskDeleted) yang diupdate saat tugas dihapus. Flag ini diperlukan untuk membedakan antara saat tugas tidak ditemukan di database karena ID salah, dan saat tugas tidak ditemukan karena pengguna baru saja menghapusnya:
Status Compose
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _isTaskDeleted by mutableStateOf(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
snapshotFlow { _isTaskDeleted },
_task
) { isTaskDeleted, taskAsync ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted = true
}
}
StateFlow
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, taskAsync ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
Jenis output dalam pipeline produksi status
Pilihan output API untuk status UI dan sifat presentasinya sangat bergantung pada API yang digunakan oleh aplikasi Anda untuk merender UI, seperti Compose. Jetpack Compose adalah toolkit modern yang direkomendasikan untuk mem-build UI native. Pertimbangan di sini mencakup:
- Membaca status dengan cara yang mendukung siklus proses.
- Apakah akan mengekspos status dalam satu atau beberapa kolom dari holder status.
Tabel berikut meringkas API yang akan digunakan untuk pipeline produksi status saat menggunakan Jetpack Compose:
| Input | Output |
|---|---|
| API satu kali | StateFlow atau Compose State |
| API stream | StateFlow |
| API satu kali dan stream | StateFlow |
Inisialisasi pipeline produksi status
Untuk melakukan inisialisasi pipeline produksi status, Anda harus menetapkan kondisi awal
agar pipeline dijalankan. Hal ini mungkin melibatkan penyediaan nilai input awal
yang penting untuk awal pipeline—misalnya, id untuk tampilan
detail artikel berita, atau memulai pemuatan asinkron.
Jika memungkinkan, lakukan inisialisasi pipeline produksi status dengan lambat untuk menghemat
resource sistem. Dari segi kepraktisan, sering kali hal ini berarti menunggu sampai ada
konsumen output. API Flow memungkinkan hal ini dengan argumen started
dalam metode stateIn. Dalam kasus jika hal ini tidak berlaku, tentukan
fungsi initialize idempoten untuk memulai pipeline produksi status secara eksplisit
seperti yang ditampilkan dalam cuplikan berikut:
class MyViewModel : ViewModel() {
private var initializeCalled = false
// This function is idempotent provided it is only called from the UI thread.
@MainThread
fun initialize() {
if(initializeCalled) return
initializeCalled = true
viewModelScope.launch {
// seed the state production pipeline
}
}
}
Contoh
Contoh Google berikut menunjukkan produksi status di lapisan UI. Jelajahi untuk melihat panduan ini dalam praktik:
Referensi lainnya
Untuk mengetahui informasi selengkapnya tentang status UI, lihat referensi tambahan berikut:
Dokumentasi
Melihat konten
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Lapisan UI
- Membangun aplikasi yang mengutamakan versi offline
- Holder status dan Status UI {:#mad-arch}