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. Di akhir proses, Anda akan dapat:
- Mengetahui API yang harus digunakan untuk menghasilkan status UI. Jenis API tersebut bergantung pada sumber perubahan status yang tersedia di holder status, dengan mengikuti prinsip aliran data searah.
- Mengetahui cara menentukan cakupan produksi status UI untuk memahami resource sistem.
- Mengetahui 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 | Negara Bagian |
---|---|
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 vs. status](https://developer.android.com/static/images/topic/architecture/ui-layer/events-vs-state.png?authuser=9&hl=id)
Peristiwa dapat berasal dari:
- 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. Input ini mungkin saja:
- 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
open
diDrawerState
di 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
NewsRepository
atau peristiwa lainnya. - Kombinasi dari semua 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/atau logika UI ke sumber perubahan status dan memproses peristiwa pengguna untuk menghasilkan status UI.
- Output: Status UI yang dapat dirender oleh aplikasi untuk memberikan informasi yang dibutuhkan pengguna.
![Pipeline produksi status](https://developer.android.com/static/images/topic/architecture/ui-layer/state-production-pipeline.png?authuser=9&hl=id)
API produksi status
Ada dua API utama yang digunakan dalam produksi status, bergantung pada tahap pipeline yang sedang Anda lalui:
Tahap pipeline | API |
---|---|
Input | Anda harus menggunakan API asinkron untuk menjalankan pekerjaan di luar UI thread agar UI tetap bebas jank. Misalnya, Coroutine atau Flow di Kotlin, dan RxJava atau callback di Bahasa Pemrograman Java. |
Output | Anda harus menggunakan API holder data yang dapat diamati untuk membatalkan validasi dan merender ulang UI saat status berubah. Misalnya, StateFlow, Compose State, atau LiveData. Holder data yang dapat diamati menjamin UI selalu memiliki status UI untuk ditampilkan di layar |
Dari dua pilihan tersebut, 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:
- 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. Pertimbangan untuk output pipeline produksi status akan berbeda-beda di berbagai View API seperti sistem View atau Jetpack Compose.
Input dalam pipeline produksi status
Input dalam pipeline produksi status dapat memberikan sumber perubahan status melalui:
- Operasi satu kali yang mungkin bersifat sinkron atau asinkron, misalnya
panggilan ke fungsi
suspend
. - API stream, 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
Gunakan MutableStateFlow
API sebagai penampung status yang dapat
diamati dan berubah. Di aplikasi Jetpack Compose, Anda juga dapat mempertimbangkan mutableStateOf
terutama saat menggunakan API teks Compose. Kedua API menawarkan metode yang memungkinkan update
atomik yang aman pada nilai yang dihosting, baik update tersebut
bersifat sinkron maupun asinkron.
Misalnya, update status di aplikasi lempar dadu yang sederhana. Setiap lemparan
dadu dari pengguna akan memanggil metode
Random.nextInt()
sinkron, dan hasilnya ditulis ke dalam
status UI.
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,
)
}
}
}
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
}
}
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.
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))
}
}
}
}
}
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))
}
}
}
}
Mengubah status UI dari thread latar belakang
Sebaiknya luncurkan Coroutine pada dispatcher utama untuk produksi
status UI. Artinya, di luar blok withContext
dalam cuplikan kode
di bawah. Namun, jika harus memperbarui status UI dalam konteks latar belakang
yang berbeda, Anda dapat melakukannya menggunakan API berikut:
- Gunakan metode
withContext
untuk menjalankan Coroutine dalam konteks serentak yang berbeda. - Saat menggunakan
MutableStateFlow
, gunakan metodeupdate
seperti biasa. - Saat menggunakan Status Compose, gunakan
Snapshot.withMutableSnapshot
untuk memastikan update atomik ke Status dalam konteks serentak.
Misalnya, asumsikan dalam cuplikan DiceRollViewModel
di bawah bahwa
SlowRandom.nextInt()
adalah fungsi suspend
yang membutuhkan banyak komputasi, yang
perlu dipanggil dari Coroutine yang terikat ke CPU.
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,
)
}
}
}
}
}
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
}
}
}
}
}
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.WhileSubscribed()
jika pipeline hanya diizinkan aktif saat UI terlihat sambil mengumpulkan alur dengan cara yang mendukung siklus proses. - Gunakan
SharingStarted.Lazily
jika pipeline harus aktif selama pengguna dapat kembali ke UI, yaitu ketika 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 di bawah ini:
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, task ->
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 }
}
}
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, task ->
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
}
}
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. Di aplikasi Android, Anda dapat memilih untuk menggunakan View atau Jetpack Compose. Pertimbangan di sini meliputi:
- Membaca status dengan cara yang mendukung siklus proses.
- Apakah status harus ditampilkan dalam satu atau beberapa kolom dari holder status ataukah tidak.
Tabel berikut meringkas API yang akan digunakan untuk pipeline produksi status untuk input dan konsumen tertentu:
Input | Konsumen | Output |
---|---|---|
API satu kali | View | StateFlow atau LiveData |
API satu kali | Compose | StateFlow atau Compose State |
API stream | View | StateFlow atau LiveData |
API stream | Compose | StateFlow |
API satu kali dan stream | View | StateFlow atau LiveData |
API satu kali dan stream | Compose | 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, Anda harus melakukan inisialisasi pipeline produksi status dengan lambat
untuk menghemat resource sistem.
Dari segi kepraktisan, sering kali hal ini berarti menunggu sampai ada konsumen dari
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:
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}