Lapisan UI (View)

Konsep dan penerapan Jetpack Compose

Peran UI adalah untuk menampilkan data aplikasi di layar dan 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 harus 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.

Mengekspos status UI

Setelah Anda menentukan status UI dan menentukan cara mengelola produksi status tersebut, langkah berikutnya adalah menampilkan status yang dihasilkan ke UI. Karena Anda menggunakan UDF untuk mengelola produksi status, Anda dapat menganggap status yang dihasilkan sebagai aliran—dengan kata lain, beberapa versi status akan dihasilkan dari waktu ke waktu. Akibatnya, Anda harus memperlihatkan status UI dalam holder data yang dapat diamati seperti LiveData atau StateFlow. Alasannya adalah agar UI dapat bereaksi terhadap perubahan apa pun yang dilakukan dalam status tanpa harus menarik data secara manual langsung dari ViewModel. Jenis 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: StateFlow<NewsUiState> = 
}

Cara umum untuk membuat aliran UiState adalah dengan mengekspos aliran data pendukung yang dapat diubah sebagai aliran yang tidak dapat diubah dari ViewModel—misalnya, mengekspos MutableStateFlow<UiState> sebagai StateFlow<UiState>.

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

ViewModel kemudian dapat mengekspos metode yang mengubah status secara internal, dengan memublikasikan update untuk UI yang akan digunakan. Misalnya, kasus saat tindakan asinkron perlu dilakukan; coroutine dapat diluncurkan menggunakan viewModelScope, dan status yang dapat diubah dapat diperbarui setelah selesai.

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                }
            }
        }
    }
}

Menggunakan status UI

Saat menggunakan holder data yang dapat diamati di UI, pastikan Anda mempertimbangkan siklus proses UI. Hal ini penting karena UI tidak boleh mengamati status UI saat tampilan tidak ditampilkan kepada pengguna. Untuk mempelajari topik ini lebih lanjut, lihat postingan blog ini. Saat menggunakan LiveData, LifecycleOwner secara implisit menangani masalah siklus proses. Saat menggunakan alur, sebaiknya tangani ini dengan cakupan coroutine yang sesuai dan repeatOnLifecycle API:

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

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.

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Animasi

Untuk memberikan transisi navigasi level atas yang lancar, Anda mungkin harus menunggu layar kedua memuat data sebelum memulai animasi. Framework tampilan Android menyediakan hook untuk menunda transisi antara tujuan fragmen dengan postponeEnterTransition() dan startPostponedEnterTransition() API. API ini menyediakan cara untuk memastikan bahwa elemen UI di layar kedua (biasanya gambar yang diambil dari jaringan) siap ditampilkan sebelum UI menganimasikan transisi ke layar tersebut.