Produksi Status UI

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 vs. status
Gambar 1: Peristiwa mengakibatkan 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 open di DrawerState 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 hal-hal di atas.
  • 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.
Pipeline produksi status
Gambar 2: Pipeline produksi status

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 withContext untuk menjalankan Coroutine dalam konteks serentak yang berbeda.
  • Saat menggunakan MutableStateFlow, gunakan metode update seperti biasa.
  • Saat menggunakan Status Compose, gunakan metode Snapshot.withMutableSnapshot untuk 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.WhileSubscribed jika pipeline hanya perlu 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, 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:

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