Status dalam Jetpack Compose

1. Sebelum memulai

Codelab ini menjelaskan konsep inti terkait penggunaan Status di Jetpack Compose. Ini menunjukkan cara status aplikasi menentukan apa yang ditampilkan di UI, cara Compose memperbarui UI saat status berubah dengan menggunakan API yang berbeda, cara mengoptimalkan struktur fungsi composable, dan menggunakan ViewModels di lingkup Compose.

Prasyarat

  • Pengetahuan tentang sintaksis Kotlin.
  • Pemahaman dasar tentang Compose (Anda dapat memulai dengan tutorial Jetpack Compose).
  • Pemahaman dasar tentang ViewModel Komponen Arsitektur.

Yang akan Anda pelajari

  • Cara memikirkan status dan peristiwa di UI Jetpack Compose.
  • Cara Compose menggunakan status untuk menentukan elemen mana yang akan ditampilkan di layar.
  • Apa itu pengangkatan status.
  • Cara kerja fungsi composable stateful dan stateless.
  • Cara Compose melacak secara otomatis status dengan API State<T>.
  • Cara kerja memori dan status internal dalam fungsi composable: menggunakan API remember dan rememberSaveable.
  • Cara menggunakan daftar dan status: menggunakan API mutableStateListOf dan toMutableStateList.
  • Cara menggunakan ViewModel dengan Compose.

Yang akan Anda butuhkan

Direkomendasikan/Opsional

Yang akan Anda build

Anda akan menerapkan aplikasi Kesehatan sederhana:

4888b02619969c55.png

Aplikasi ini memiliki dua fungsi utama:

  • Penghitung air untuk melacak asupan air Anda.
  • Daftar tugas kesehatan yang harus dilakukan sepanjang hari.

2. Memulai persiapan

Memulai project Compose baru

  1. Untuk memulai project Compose baru, buka Android Studio.
  2. Jika baru memulai di jendela Welcome to Android Studio, klik Start a new Android Studio project. Jika sudah membuka project Android Studio, pilih File > New > New Project dari panel menu.
  3. Untuk project baru, pilih Empty Compose Activity dari template yang tersedia.

a67ba73a4f06b7ac.png

  1. Klik Next dan konfigurasikan project Anda, dengan nama "BasicStateCodelab".

Pastikan Anda memilih minimumSdkVersion minimal API level 21, yang merupakan API Compose minimum yang didukung.

Saat Anda memilih template Empty Compose Activity, Android Studio akan menyiapkan hal berikut untuk project:

  • Class MainActivity yang dikonfigurasi dengan fungsi composable yang menampilkan beberapa teks di layar.
  • File AndroidManifest.xml, yang menentukan izin, komponen, dan resource kustom aplikasi Anda.
  • File build.gradle dan app/build.gradle berisi opsi dan dependensi yang diperlukan untuk Compose.

Solusi untuk codelab

Anda dapat memperoleh kode untuk solusi codelab ini dari GitHub:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Atau, Anda dapat mendownload repositori sebagai file ZIP.

Anda akan menemukan kode solusi di project BasicStateCodelab. Sebaiknya ikuti codelab ini langkah demi langkah sesuai kemampuan Anda dan periksa solusinya jika Anda memerlukan bantuan. Selama codelab, Anda akan melihat cuplikan kode yang harus ditambahkan ke project Anda.

3. Status dalam Compose

"Status" aplikasi adalah nilai yang dapat berubah dari waktu ke waktu. Ini adalah definisi yang sangat luas dan mencakup semua dari database Room hingga variabel di class.

Semua aplikasi Android menampilkan status kepada pengguna. Beberapa contoh status di aplikasi Android:

  • Pesan terbaru yang diterima di aplikasi chat.
  • Foto profil pengguna.
  • Posisi scroll dalam daftar item.

Mari kita mulai menulis aplikasi Kesehatan Anda.

Untuk mempermudah, selama codelab:

  • Anda dapat menambahkan semua file Kotlin dalam paket com.codelabs.basicstatecodelab root modul app. Namun, dalam aplikasi produksi, file harus terstruktur secara logis dalam sub-paket.
  • Anda akan melakukan hardcode semua string secara inline dalam cuplikan. Dalam aplikasi yang sebenarnya, string harus ditambahkan sebagai resource string dalam file strings.xml dan direferensikan menggunakan stringResource API Compose.

Bagian pertama fungsi yang perlu Anda build adalah penghitung air untuk menghitung jumlah gelas air yang Anda konsumsi sepanjang hari.

Buat fungsi composable yang disebut WaterCounter yang berisi composable Text yang menampilkan jumlah gelas. Jumlah gelas harus disimpan dalam nilai bernama count, yang dapat Anda hardcode untuk saat ini.

Buat file baru WaterCounter.kt dengan fungsi composable WaterCounter, seperti ini:

import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   val count = 0
   Text(
       text = "You've had $count glasses.",
       modifier = modifier.padding(16.dp)
   )
}

Mari membuat fungsi composable yang mewakili seluruh layar, yang akan memiliki dua bagian, penghitung air, dan daftar tugas kesehatan. Untuk saat ini, kita akan tambahkan penghitung.

  1. Buat file WellnessScreen.kt, yang mewakili layar utama, dan panggil fungsi WaterCounter:
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   WaterCounter(modifier)
}
  1. Buka MainActivity.kt. Hapus composable Greeting dan DefaultPreview. Panggil composable WellnessScreen yang baru dibuat di dalam blok setContent Aktivitas, seperti ini:
class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           BasicStateCodelabTheme {
               // A surface container using the 'background' color from the theme
               Surface(
                   modifier = Modifier.fillMaxSize(),
                   color = MaterialTheme.colors.background
               ) {
                   WellnessScreen()
               }
           }
       }
   }
}
  1. Jika menjalankan aplikasi sekarang, Anda akan melihat layar penghitung air dasar dengan jumlah gelas air yang di-hardcode.

e1778709d7fb5a68.png

Status fungsi composable WaterCounter adalah variabel count. Namun, memiliki status statis tidak terlalu berguna karena status statis tidak dapat dimodifikasi. Untuk mengatasi hal ini, Anda akan menambahkan Button untuk meningkatkan jumlah dan melacak jumlah gelas air yang Anda konsumsi sepanjang hari.

Setiap tindakan yang menyebabkan perubahan status disebut "peristiwa" dan kita akan mempelajari hal ini lebih lanjut di bagian berikutnya.

4. Peristiwa di Compose

Kita membahas status sebagai nilai apa pun yang berubah dari waktu ke waktu, misalnya, pesan terakhir yang diterima di aplikasi chat. Namun, apa yang menyebabkan status diperbarui? Di aplikasi Android, status diperbarui sebagai respons terhadap peristiwa.

Peristiwa adalah input yang dihasilkan dari luar atau di dalam aplikasi, seperti:

  • Misalnya, pengguna berinteraksi dengan UI dengan menekan tombol.
  • Faktor lain, seperti sensor yang mengirimkan nilai baru, atau respons jaringan.

Meskipun status aplikasi menawarkan deskripsi tentang hal yang akan ditampilkan di UI, peristiwa adalah mekanisme yang digunakan untuk mengubah status, sehingga menghasilkan perubahan pada UI.

Peristiwa memberi tahu bagian program bahwa sesuatu telah terjadi. Di semua aplikasi Android, terdapat loop update UI inti yang berjalan seperti ini:

f415ca9336d83142.png

  • Peristiwa – Peristiwa dihasilkan oleh pengguna atau bagian lain dari program.
  • Status Update – Pengendali peristiwa mengubah status yang digunakan oleh UI.
  • Status Tampilan – UI diupdate untuk menampilkan status baru.

Mengelola status di Compose adalah tentang memahami cara status dan peristiwa saling berinteraksi.

Sekarang, tambahkan tombol tersebut sehingga pengguna dapat mengubah status dengan menambahkan lebih banyak gelas air.

Buka fungsi composable WaterCounter untuk menambahkan Button di bawah label kita Text. Column akan membantu Anda menyelaraskan Text secara vertikal dengan composable Button. Anda dapat memindahkan padding eksternal ke composable Column dan menambahkan beberapa padding tambahan ke bagian atas Button sehingga terpisah dari Teks.

Fungsi composable Button menerima fungsi lambdaonClick - ini adalah peristiwa yang terjadi saat tombol diklik. Anda akan melihat contoh fungsi lambda lainnya nanti.

Ubah count menjadi var, bukan val agar dapat diubah.

import androidx.compose.material.Button
import androidx.compose.foundation.layout.Column

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count = 0
       Text("You've had $count glasses.")
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Saat Anda menjalankan aplikasi dan mengklik tombol, perhatikan bahwa tidak ada yang terjadi. Menetapkan nilai yang berbeda untuk variabel count tidak akan membuat Compose mendeteksinya sebagai perubahan status sehingga tidak ada yang terjadi. Hal ini karena Anda belum memberi tahu Compose bahwa Compose harus menggambar ulang layar (yaitu, "mengomposisi ulang" fungsi composable), saat status berubah. Anda akan memperbaikinya di langkah berikutnya.

86caa22e4f7f9072.gif

5. Memori dalam fungsi composable

Aplikasi Compose mengubah data menjadi UI dengan memanggil fungsi composable. Kami merujuk pada Komposisi sebagai deskripsi UI yang di-build oleh Compose saat menjalankan fungsi composable. Jika terjadi perubahan status, Compose akan menjalankan kembali fungsi composable yang terpengaruh dengan status baru, membuat UI yang diupdate—ini disebut rekomposisi. Compose juga melihat data yang diperlukan setiap composable, sehingga hanya merekomposisi komponen yang datanya telah berubah dan melewati komponen yang tidak terpengaruh.

Agar dapat melakukannya, Compose perlu mengetahui status yang harus dilacak, sehingga saat menerima update, Compose dapat menjadwalkan rekomposisi.

Compose memiliki sistem pelacakan status khusus yang menjadwalkan rekomposisi untuk setiap composable yang membaca status tertentu. Hal ini memungkinkan Compose menjadi terperinci dan hanya merekomposisi fungsi composable yang perlu berubah, bukan seluruh UI. Hal ini dilakukan dengan melacak tidak hanya "penulisan" (yaitu, perubahan status), tetapi juga "membaca" status.

Gunakan jenis State dan MutableState Compose agar status dapat diamati oleh Compose.

Compose melacak setiap composable yang membaca properti value Status dan memicu rekomposisi saat value berubah. Anda dapat menggunakan fungsi mutableStateOf untuk membuat MutableState yang dapat diamati. Fungsi ini menerima nilai awal sebagai parameter yang digabungkan dalam objek State, yang kemudian membuat value-nya dapat diamati.

Update composable WaterCounter, sehingga count menggunakan mutableStateOf API dengan 0 sebagai nilai awal. Saat mutableStateOf menampilkan jenis MutableState, Anda dapat mengupdate value untuk mengupdate status, dan Compose akan memicu rekomposisi ke fungsi tersebut tempat value dibaca.

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
      // Changes to count are now tracked by Compose
       val count: MutableState<Int> = mutableStateOf(0)

       Text("You've had ${count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Seperti yang disebutkan sebelumnya, setiap perubahan pada count akan menjadwalkan rekomposisi fungsi composable yang membaca value count secara otomatis. Dalam hal ini, WaterCounter dikomposisi ulang setiap kali tombol diklik.

Jika menjalankan aplikasi sekarang, Anda akan melihat lagi bahwa belum ada yang terjadi.

86caa22e4f7f9072.gif

Penjadwalan rekomposisi berfungsi dengan baik. Namun, saat rekomposisi terjadi, variabel count akan diinisialisasi ulang ke 0, sehingga kita memerlukan cara untuk mempertahankan nilai ini di seluruh rekomposisi.

Untuk itu, kita dapat menggunakan fungsi inline composable remember. Nilai yang dihitung oleh remember disimpan dalam Komposisi selama komposisi awal, dan nilai yang disimpan, juga disimpan di seluruh rekomposisi.

Biasanya remember dan mutableStateOf digunakan bersama-sama dalam fungsi composable.

Ada beberapa cara yang setara untuk menulis ini seperti yang ditampilkan dalam dokumentasi Status Compose.

Ubah WaterCounter, yang mengelilingi panggilan ke mutableStateOf dengan fungsi composable inline remember:

import androidx.compose.runtime.remember

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        val count: MutableState<Int> = remember { mutableStateOf(0) }
        Text("You've had ${count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
            Text("Add one")
        }
    }
}

Atau, kita dapat menyederhanakan penggunaan count dengan menggunakan properti yang didelegasikan Kotlin.

Anda dapat menggunakan kata kunci menurut untuk menentukan count sebagai var. Menambahkan impor pengambil dan penyetel memungkinkan kita membaca dan mengubah count secara tidak langsung tanpa secara eksplisit merujuk ke properti value MutableState setiap saat.

Sekarang WaterCounter terlihat seperti ini:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       Text("You've had $count glasses.")
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Anda harus memilih sintaksis yang menghasilkan kode yang paling mudah dibaca dalam composable yang ditulis.

Sekarang, mari kita periksa apa yang telah kita lakukan sejauh ini:

  • Menentukan variabel yang diingat dari waktu ke waktu yang disebut count.
  • Membuat tampilan teks yang memberi tahu pengguna nomor yang kita ingat.
  • Menambahkan tombol yang menaikkan angka yang kita ingat setiap kali diklik.

Pengaturan ini membentuk feedback loop aliran data dengan pengguna:

  • UI menampilkan status kepada pengguna (jumlah saat ini ditampilkan sebagai teks).
  • Pengguna menghasilkan peristiwa yang digabungkan dengan status yang ada untuk menghasilkan status baru (mengklik tombol akan menambahkan status ke jumlah saat ini)

Penghitung Anda sudah siap dan berfungsi!

c2a8eeeaa73d7315.gif

6. UI berbasis status

Compose adalah framework UI deklaratif. Daripada menghapus komponen UI atau mengubah visibilitasnya saat status berubah, kami menjelaskan bagaimana UI berada dalam kondisi status tertentu. Akibat dari rekomposisi yang dipanggil dan UI yang diperbarui, composable mungkin akhirnya masuk atau keluar dari Komposisi.

7d3509d136280b6c.png

Pendekatan ini menghindari kompleksitas pembaruan tampilan secara manual seperti yang Anda lakukan pada sistem View. Tampilan ini juga tidak terlalu rentan mengalami error, karena Anda tidak lupa mengupdate tampilan berdasarkan status baru, karena hal ini terjadi secara otomatis.

Jika fungsi composable dipanggil selama komposisi awal atau dalam rekomposisi, kita mengatakan bahwa fungsi tersebut ada dalam Komposisi. Fungsi composable yang tidak dipanggil—misalnya, karena fungsi dipanggil di dalam pernyataan if dan kondisi tidak terpenuhi—-tidak ada dari Komposisi.

Anda dapat mempelajari lebih lanjut siklus proses composable dalam dokumentasi.

Output Komposisi adalah struktur hierarki yang mendeskripsikan UI.

Anda dapat memeriksa tata letak aplikasi yang dibuat oleh Compose menggunakan alat Layout Inspector Android Studio, yang akan Anda lakukan berikutnya.

Untuk mendemonstrasikan ini, ubah kode untuk menampilkan UI berdasarkan status. Buka WaterCounter dan tampilkan Text jika count lebih besar dari 0:

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       if (count > 0) {
           // This text is present if the button has been clicked
           // at least once; absent otherwise
           Text("You've had $count glasses.")
       }
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Jalankan aplikasi dan buka alat Layout Inspector Android Studio dengan membuka Tools > Layout Inspector.

Anda akan melihat layar terpisah: hierarki komponen di sebelah kiri dan pratinjau aplikasi di sebelah kanan.

Jelajahi hierarki dengan mengetuk elemen root BasicStateCodelabTheme di sebelah kiri layar. Luaskan seluruh hierarki komponen dengan mengklik tombol Luaskan semua.

Klik elemen di layar sebelah kanan untuk membuka elemen hierarki yang sesuai.

49ee0aa7f520832.png

Jika Anda menekan tombol Tambahkan satu:

  • Jumlah meningkat menjadi 1 dan status berubah.
  • Rekomposisi dipanggil.
  • Layar dikomposisi ulang dengan elemen baru.

Saat Anda memeriksa hierarki komponen dengan alat Layout Inspector Android Studio, kini Anda juga melihat composable Text:

3bde9ddfdbb1ab78.png

Status mendorong elemen mana yang ditampilkan di UI pada waktu tertentu.

Bagian UI yang berbeda dapat bergantung pada status yang sama. Ubah Button sehingga diaktifkan hingga count adalah 10, lalu dinonaktifkan (dan Anda mencapai sasaran untuk hari tersebut). Gunakan parameter enabled Button untuk melakukannya.

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    ...
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
    ...
}

Jalankan aplikasi sekarang. Perubahan pada status count menentukan apakah Text akan ditampilkan atau tidak, dan apakah Button diaktifkan atau dinonaktifkan.

e7f75abce3e3d781.gif

7. Ingat dalam Komposisi

remember menyimpan objek dalam Komposisi, dan melupakan objek jika lokasi sumber tempat remember dipanggil tidak dipanggil lagi selama rekomposisi.

Jika pengguna meminum setidaknya satu gelas air, tampilkan tugas kesehatan yang dapat mereka lakukan. Karena composable harus berukuran kecil dan dapat digunakan kembali, buat composable baru bernama WellnessTaskItem yang menampilkan tugas kesehatan berdasarkan string yang diterima sebagai parameter, bersama dengan tombol ikon Close.

Buat file baru WellnessTaskItem.kt, lalu tambahkan kode berikut. Anda akan menggunakan fungsi composable ini nanti di codelab.

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding

@Composable
fun WellnessTaskItem(
    taskName: String,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier.weight(1f).padding(start = 16.dp),
            text = taskName
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = "Close")
        }
    }
}

Fungsi WellnessTaskItem menerima deskripsi tugas dan fungsi lambda onClose (sama seperti composable Button bawaan yang menerima onClick).

WellnessTaskItem terlihat seperti ini:

6e8b72a529e8dedd.png

Untuk meningkatkan aplikasi kita dengan lebih banyak fitur, update WaterCounter untuk menampilkan WellnessTaskItem saat count > 0.

Jika count lebih besar dari 0, tentukan variabel showTask yang menentukan apakah akan menampilkan WellnessTaskItem atau tidak, dan lakukan inisialisasi ke benar.

Tambahkan pernyataan if baru untuk menampilkan WellnessTaskItem jika showTask benar. Gunakan API yang Anda pelajari di bagian sebelumnya untuk memastikan nilai showTask bertahan dari rekomposisi.

@Composable
fun WaterCounter() {
   Column(modifier = Modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }
       if (count > 0) {
           var showTask by remember { mutableStateOf(true) }
           if (showTask) {
               WellnessTaskItem(
                   onClose = { },
                   task = "Have you taken your 15 minute walk today?"
               )
           }
           Text("You've had $count glasses.")
       }

       Button(onClick = { count++ }, enabled = count < 10) {
           Text("Add one")
       }
   }
}

Gunakan fungsi lambda onClose WellnessTaskItem, sehingga saat tombol X ditekan, variabel showTask akan berubah menjadi false dan tugas tidak ditampilkan lagi.

   ...
   WellnessTaskItem(
      onClose = { showTask = false },
      task = "Have you taken your 15 minute walk today?"
   )
   ...

Selanjutnya, tambahkan Button baru dengan teks "Jumlah air jernih" dan tempatkan di samping "Tambahkan satu" Button. Row dapat membantu menyelaraskan kedua tombol tersebut. Anda juga dapat menambahkan beberapa padding ke Row. Saat tombol "Hapus jumlah air" ditekan, variabel count direset ke 0.

Fungsi composable WaterCounter yang sudah selesai akan terlihat seperti ini:

import androidx.compose.foundation.layout.Row

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }
       if (count > 0) {
           var showTask by remember { mutableStateOf(true) }
           if (showTask) {
               WellnessTaskItem(
                   onClose = { showTask = false },
                   task = "Have you taken your 15 minute walk today?"
               )
           }
           Text("You've had $count glasses.")
       }

       Row(Modifier.padding(top = 8.dp)) {
           Button(onClick = { count++ }, enabled = count < 10) {
               Text("Add one")
           }
           Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) {
               Text("Clear water count")
           }
       }
   }
}

Saat Anda menjalankan aplikasi, layar akan menampilkan status awal:

Diagram hierarki komponen, yang menampilkan status awal aplikasi, jumlahnya 0

Di sebelah kanan, kami memiliki versi sederhana dari hierarki komponen yang akan membantu Anda menganalisis apa yang terjadi saat perubahan status. count dan showTask adalah nilai yang diingat.

Sekarang Anda dapat mengikuti langkah-langkah ini di aplikasi:

  • Tekan tombol Tambahkan satu. Kenaikan tersebut count (ini menyebabkan rekomposisi) dan WellnessTaskItem serta penghitung Text mulai ditampilkan.

Diagram hierarki komponen, yang menampilkan perubahan status, saat tombol Tambahkan satu diklik, Teks dengan informasi akan muncul dan Teks dengan jumlah gelas muncul.

9257d150a5952931.png

  • Tekan X dari komponen WellnessTaskItem (ini menyebabkan rekomposisi lain). showTask sekarang salah, yang berarti WellnessTaskItem tidak ditampilkan lagi.

Diagram hierarki komponen, yang menunjukkan bahwa saat tombol tutup diklik, composable tugas akan hilang.

6bf6d3991a1c9fd1.png

  • Tekan tombol Tambahkan satu (rekomposisi lain). showTask mengingat Anda telah menutup WellnessTaskItem di rekomposisi berikutnya jika Anda terus menambahkan gelas.

  • Tekan tombol Hapus jumlah air untuk mereset count ke 0 dan menyebabkan rekomposisi. Text yang menampilkan count, dan semua kode yang terkait dengan WellnessTaskItem, tidak dipanggil dan keluar dari Komposisi.

ae993e6ddc0d654a.png

  • showTask dilupakan karena lokasi kode tempat mengingat showTask dipanggil, tidak dipanggil. Anda kembali ke langkah pertama.

  • Tekan tombol Tambahkan satu yang membuat count lebih besar dari 0 (rekomposisi).

9257d150a5952931.png

  • Composable WellnessTaskItem akan ditampilkan lagi, karena nilai showTask sebelumnya yang terlupakan saat keluar dari Komposisi di atas.

Bagaimana jika kita memerlukan showTask untuk dipertahankan setelah count kembali ke 0, lebih lama dari yang diizinkan oleh remember (yaitu, meskipun lokasi kode tempat remember dipanggil, tidak dipanggil selama rekomposisi)? Kita akan mempelajari cara memperbaiki skenario ini dan contoh lainnya di bagian berikutnya.

Setelah Anda memahami cara UI dan status direset saat keluar dari Komposisi, hapus kode dan kembali ke WaterCounter yang Anda miliki di awal bagian ini:

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }
        if (count > 0) {
            Text("You've had $count glasses.")
        }
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
            Text("Add one")
        }
    }
}

8. Memulihkan status di Compose

Jalankan aplikasi, tambahkan beberapa gelas air ke penghitung, lalu putar perangkat Anda. Pastikan Anda mengaktifkan setelan Putar Otomatis perangkat.

Karena Aktivitas dibuat ulang setelah perubahan konfigurasi (dalam hal ini, orientasi), status yang disimpan akan dilupakan: penghitung menghilang karena kembali ke 0.

34f1e94c368af98d.gif

Hal yang sama terjadi jika Anda mengubah bahasa, beralih antara mode gelap dan terang, atau perubahan konfigurasi lainnya yang membuat Android membuat ulang Aktivitas yang berjalan.

Meskipun remember membantu Anda mempertahankan status di seluruh rekomposisi, tidak dipertahankan di seluruh perubahan konfigurasi. Untuk melakukannya, Anda harus menggunakan rememberSaveable, bukan remember.

rememberSaveable otomatis menyimpan nilai apa pun yang dapat disimpan di Bundle. Untuk nilai lain, Anda dapat meneruskan objek penghemat kustom. Untuk informasi selengkapnya tentang Memulihkan status di Compose, lihat dokumentasi.

Di WaterCounter, ganti remember dengan rememberSaveable:

import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
        ...
        var count by rememberSaveable { mutableStateOf(0) }
        ...
}

Jalankan aplikasi sekarang dan coba beberapa perubahan konfigurasi. Anda akan melihat penghitung disimpan dengan benar.

45beaedccab5e331.gif

Pembuatan ulang aktivitas hanyalah salah satu kasus penggunaan rememberSaveable. Kita akan menjelajahi kasus penggunaan lain nanti saat menangani daftar.

Pertimbangkan apakah akan menggunakan remember atau rememberSaveable, bergantung pada status aplikasi dan kebutuhan UX Anda.

9. Pengangkatan status

Fungsi composable yang menggunakan remember untuk menyimpan objek akan berisi status internal, sehingga menjadikan fungsi composable tersebut bersifat stateful. Hal ini dapat berguna dalam situasi saat pemanggil tidak perlu mengontrol status dan dapat menggunakannya tanpa harus mengelola status itu sendiri. Namun, fungsi composable dengan status internal cenderung kurang dapat digunakan kembali dan lebih sulit diuji.

Composable yang tidak memiliki status apa pun disebut composable stateless. Cara mudah untuk membuat composable stateless adalah dengan menggunakan pengangkatan status.

Pengangkatan status di Compose adalah pola pemindahan status ke pemanggil fungsi composable untuk menjadikan fungsi composable bersifat stateless. Pola umum untuk pengangkatan status di Jetpack Compose adalah mengganti variabel status dengan dua parameter:

  • value: T - nilai saat ini yang akan ditampilkan
  • onValueChange: (T) -> Unit - peristiwa yang meminta perubahan nilai, dengan T adalah nilai baru yang diusulkan

dengan nilai ini mewakili status apa pun yang dapat diubah.

Status yang diangkat dengan cara ini memiliki beberapa properti penting:

  • Satu sumber kebenaran: Dengan memindahkan status dan bukan membuat duplikatnya, kita memastikan hanya ada satu sumber kebenaran. Tindakan ini membantu menghindari bug.
  • Dapat dibagikan: Status yang diangkat dapat dibagikan dengan beberapa fungsi composable.
  • Dapat dicegat: Pemanggil fungsi composable stateless dapat memutuskan untuk mengabaikan atau mengubah peristiwa sebelum mengubah status.
  • Dipisahkan: Status untuk fungsi composable stateless dapat disimpan di mana saja. Misalnya, di ViewModel.

Coba terapkan untuk WaterCounter sehingga dapat memanfaatkan semua hal di atas.

Stateful vs Stateless

Jika semua status dapat diekstrak dari fungsi composable, fungsi composable yang dihasilkan akan disebut stateless.

Faktorkan ulang composable WaterCounter dengan memisahkannya menjadi dua bagian: Penghitung stateful dan stateless.

Peran StatelessCounter adalah menampilkan count dan memanggil fungsi saat Anda menambahkan count. Untuk melakukannya, ikuti pola yang dijelaskan di atas dan teruskan status, count (sebagai parameter ke fungsi composable), dan lambda (onIncrement), yang dipanggil saat status perlu ditingkatkan. StatelessCounter terlihat seperti ini:

@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       if (count > 0) {
           Text("You've had $count glasses.")
       }
       Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
           Text("Add one")
       }
   }
}

StatefulCounter memiliki status. Artinya, fungsi tersebut memiliki status count dan mengubahnya saat memanggil fungsi StatelessCounter.

@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
   var count by rememberSaveable { mutableStateOf(0) }
   StatelessCounter(count, { count++ }, modifier)
}

Bagus! Anda mengangkat count dari StatelessCounter ke StatefulCounter.

Anda dapat mencolokkannya ke aplikasi Anda dan mengupdate WellnessScreen dengan StatefulCounter:

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   StatefulCounter(modifier)
}

Seperti yang disebutkan, pengangkatan status memiliki beberapa manfaat. Kita akan menjelajahi variasi kode ini untuk menjelaskan beberapa di antaranya, Anda tidak perlu menyalin cuplikan berikut di aplikasi.

  1. Composable stateless Anda sekarang dapat digunakan kembali. Misalnya, perhatikan contoh berikut.

Untuk menghitung gelas air dan jus, Anda mengingat waterCount dan juiceCount, tetapi gunakan contoh fungsi composable StatelessCounter untuk menampilkan dua status independen yang berbeda.

@Composable
fun StatefulCounter() {
    var waterCount by remember { mutableStateOf(0) }

    var juiceCount by remember { mutableStateOf(0) }

    StatelessCounter(waterCount, { waterCount++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}

8211bd9e0a4c5db2.png

Jika juiceCount diubah, StatefulCounter akan dikomposisi ulang. Selama rekomposisi, Compose mengidentifikasi fungsi mana yang membaca juiceCount dan memicu rekomposisi fungsi tersebut saja.

2cb0dcdbe75dcfbf.png

Saat pengguna mengetuk untuk menambahkan juiceCount, StatefulCounter akan merekomposisi, dan begitu juga StatelessCounter yang membaca juiceCount. Namun, StatelessCounter yang membaca waterCount tidak akan dikomposisi ulang.

7fe6ee3d2886abd0.png

  1. Fungsi composable stateful Anda dapat memberikan status yang sama ke beberapa fungsi composable.
@Composable
fun StatefulCounter() {
   var count by remember { mutableStateOf(0) }

   StatelessCounter(count, { count++ })
   AnotherStatelessMethod(count, { count * 2 })
}

Dalam hal ini, jika hitungan diperbarui oleh StatelessCounter atau AnotherStatelessMethod, semuanya akan dikomposisi ulang, yang diharapkan.

Karena status yang diangkat dapat dibagikan, pastikan untuk hanya meneruskan status yang diperlukan composable untuk menghindari rekomposisi yang tidak perlu, dan untuk meningkatkan penggunaan kembali.

Untuk membaca lebih lanjut tentang status dan pengangkatan status, lihat dokumentasi Status Compose.

10. Menangani daftar

Selanjutnya, tambahkan fitur kedua di aplikasi Anda, yaitu daftar tugas kesehatan. Anda dapat melakukan dua tindakan dengan item di daftar:

  • Centang item daftar untuk menandai tugas sebagai selesai.
  • Hapus tugas dari daftar yang tidak ingin Anda selesaikan.

Penyiapan

  1. Pertama, ubah item daftar. Anda dapat menggunakan kembali WellnessTaskItem dari bagian Ingat di Komposisi, dan memperbaruinya agar berisi Checkbox. Pastikan Anda mengangkat status checked dan callback onCheckedChange untuk membuat fungsi menjadi stateless.

16b3672772c44395.png

Composable WellnessTaskItem untuk bagian ini akan terlihat seperti ini:

import androidx.compose.material.Checkbox

@Composable
fun WellnessTaskItem(
    taskName: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = taskName
        )
        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = "Close")
        }
    }
}
  1. Dalam file yang sama, tambahkan fungsi composable WellnessTaskItem stateful yang menentukan variabel status checkedState dan meneruskannya ke metode stateless dengan nama yang sama. Untuk saat ini, jangan khawatir tentang onClose. Anda dapat meneruskan fungsi lambda kosong.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember

@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
   var checkedState by remember { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -> checkedState = newValue },
       onClose = {}, // we will implement this later!
       modifier = modifier,
   )
}
  1. Buat WellnessTask.kt file untuk membuat model tugas yang berisi ID dan label. Tentukan sebagai class data.
data class WellnessTask(val id: Int, val label: String)
  1. Untuk daftar tugas itu sendiri, buat file baru bernama WellnessTasksList.kt dan tambahkan metode yang menghasilkan beberapa data palsu:
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

Perhatikan bahwa dalam aplikasi yang sebenarnya, Anda mendapatkan data dari lapisan data.

  1. Di WellnessTasksList.kt, tambahkan fungsi composable yang membuat daftar. Tentukan LazyColumn dan item dari metode daftar yang Anda buat. Lihat Dokumentasi daftar jika Anda memerlukan bantuan.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember

@Composable
fun WellnessTasksList(
    modifier: Modifier = Modifier,
    list: List<WellnessTask> = remember { getWellnessTasks() }
) {
    LazyColumn(
        modifier = modifier
    ) {
        items(list) { task ->
            WellnessTaskItem(taskName = task.label)
        }
    }
}
  1. Tambahkan daftar ke WellnessScreen. Gunakan Column untuk membantu menyelaraskan daftar secara vertikal dengan penghitung yang sudah Anda miliki.
import androidx.compose.foundation.layout.Column

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()
       WellnessTasksList()
   }
}
  1. Jalankan aplikasi dan cobalah! Sekarang Anda dapat memeriksa tugas, tetapi tidak dapat menghapusnya. Anda akan menerapkannya di bagian selanjutnya.

2903e244176c2dda.gif

Memulihkan status item di LazyList

Pelajari lebih lanjut beberapa hal dalam composable WellnessTaskItem.

checkedState termasuk dalam setiap composable WellnessTaskItem secara independen, seperti variabel pribadi. Saat checkedState berubah, hanya instance WellnessTaskItem tersebut yang akan dikomposisi ulang, bukan semua instance WellnessTaskItem di LazyColumn.

Cobalah dengan mengikuti langkah-langkah berikut:

  1. Centang elemen apa pun di bagian atas daftar ini (misalnya elemen 1 dan 2).
  2. Scroll ke bagian bawah daftar sehingga berada di luar layar.
  3. Scroll kembali ke atas ke item yang Anda centang sebelumnya.
  4. Perhatikan bahwa keduanya tidak dicentang.

Ada masalah, seperti yang Anda lihat di bagian sebelumnya, bahwa saat item keluar dari Komposisi, status yang diingat akan dilupakan. Untuk item di LazyColumn, item akan keluar dari Komposisi sepenuhnya saat Anda men-scroll melewatinya dan item tersebut tidak lagi terlihat.

d3c12f57cc98db16.gif

Bagaimana cara mengatasi masalah ini? Sekali lagi, gunakan rememberSaveable, karena nilai yang tersimpan mempertahankan aktivitas atau proses pembuatan ulang menggunakan mekanisme status instance yang disimpan—dalam hal ini, saat item kita keluar dari Komposisi.

Cukup ganti remember dengan rememberSaveable di WellnessTaskItem stateful, dan selesai:

import androidx.compose.runtime.saveable.rememberSaveable

var checkedState by rememberSaveable { mutableStateOf(false) }

9b4ba365481ae97a.gif

Pola umum di Compose

Perhatikan penerapan LazyColumn:

@Composable
fun LazyColumn(
...
    state: LazyListState = rememberLazyListState(),
...

Fungsi composable rememberLazyListState membuat status awal untuk daftar menggunakan rememberSaveable. Saat Aktivitas dibuat ulang, status scroll dipertahankan tanpa Anda harus membuat kode apa pun.

Banyak aplikasi yang perlu bereaksi dan memproses untuk men-scroll posisi, perubahan tata letak item, dan peristiwa lain yang terkait dengan status daftar. Komponen lambat, seperti LazyColumn atau LazyRow, mendukung kasus penggunaan ini melalui pengangkatan LazyListState. Anda dapat mempelajari lebih lanjut pola ini di dokumentasi untuk status dalam daftar.

Memiliki parameter status dengan nilai default yang disediakan oleh fungsi rememberX publik adalah pola yang umum dalam fungsi composable bawaan. Contoh lainnya dapat ditemukan di Scaffold, yang mengangkat status menggunakan rememberScaffoldState.

11. MutableList yang dapat diobservasi

Selanjutnya, untuk menambahkan perilaku penghapusan tugas dari daftar, langkah pertama adalah membuat daftar Anda menjadi daftar yang dapat diubah.

Penggunaan objek yang dapat diubah untuk hal ini, seperti ArrayList<T> atau mutableListOf, tidak akan berfungsi. Jenis ini tidak akan memberi tahu Compose bahwa item dalam daftar telah berubah dan menjadwalkan rekomposisi UI. Anda memerlukan API yang berbeda.

Anda perlu membuat instance MutableList yang dapat diamati oleh Compose. Struktur ini memungkinkan Compose melacak perubahan untuk merekomposisi UI saat item ditambahkan atau dihapus dari daftar.

Mulai dengan menentukan MutableList yang dapat diamati. Fungsi ekstensi toMutableStateList() adalah cara untuk membuat MutableList yang dapat diamati dari Collection awal yang tidak dapat diubah atau tidak dapat diubah, seperti List.

Atau, Anda juga dapat menggunakan metode factory mutableStateListOf untuk membuat MutableList yang dapat diamati, lalu menambahkan elemen untuk status awal Anda.

  1. Buka file WellnessScreen.kt. Pindahkan metode getWellnessTasks ke file ini agar dapat menggunakannya. Buat daftar dengan memanggil getWellnessTasks() terlebih dahulu, lalu menggunakan fungsi ekstensi toMutableStateList yang Anda pelajari sebelumnya.
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()

       val list = remember { getWellnessTasks().toMutableStateList() }
       WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
  1. Ubah fungsi composable WellnessTaskList dengan menghapus nilai default daftar, karena daftar diangkat ke tingkat layar. Tambahkan parameter fungsi lambda baru onCloseTask (menerima WellnessTask untuk menghapus). Teruskan onCloseTask ke WellnessTaskItem.

Ada satu perubahan lagi yang perlu Anda lakukan. Metode items menerima parameter key. Secara default, setiap status item dimasukkan ke posisi item dalam daftar.

Dalam daftar yang dapat diubah, ini menyebabkan masalah saat set data berubah, karena item yang mengubah posisi secara efektif kehilangan status yang diingat.

Anda dapat memperbaikinya dengan mudah menggunakan id dari setiap WellnessTaskItem sebagai kunci untuk setiap item.

Untuk mempelajari lebih lanjut tentang kunci item dalam daftar, lihat dokumentasinya.

WellnessTaskList akan terlihat seperti ini:

@Composable
fun WellnessTasksList(
   list: List<WellnessTask>,
   onCloseTask: (WellnessTask) -> Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(modifier = modifier) {
       items(
           items = list,
           key = { task -> task.id }
       ) { task ->
           WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
       }
   }
}
  1. Ubah WellnessTaskItem: tambahkan fungsi lambda onClose sebagai parameter ke WellnessTaskItem stateful dan panggil fungsi tersebut.
@Composable
fun WellnessTaskItem(
   taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
   var checkedState by rememberSaveable { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -> checkedState = newValue },
       onClose = onClose,
       modifier = modifier,
   )
}

Bagus! Fungsinya sudah lengkap, dan menghapus item dari daftar akan berhasil.

Jika Anda mengklik X di setiap baris, peristiwa akan masuk ke daftar yang memiliki status, menghapus item dari daftar dan menyebabkan Compose merekomposisi layar.

47f4a64c7e9a5083.png

Jika mencoba menggunakan rememberSaveable() untuk menyimpan daftar di WellnessScreen, Anda akan mendapatkan pengecualian runtime:

Error ini memberi tahu Anda bahwa Anda harus memberikan saver kustom. Namun, Anda tidak boleh menggunakan rememberSaveable untuk menyimpan data dalam jumlah besar atau struktur data kompleks yang memerlukan serialisasi atau deserialisasi yang panjang.

Aturan serupa berlaku saat menggunakan onSaveInstanceState Aktivitas; Anda dapat menemukan informasi selengkapnya di dokumentasi Menyimpan status UI. Jika ingin melakukannya, Anda memerlukan mekanisme penyimpanan alternatif. Anda dapat mempelajari lebih lanjut berbagai opsi untuk mempertahankan status UI dalam dokumentasi.

Selanjutnya, kita akan melihat peran ViewModel sebagai holder untuk status aplikasi.

12. Status dalam ViewModel

Layar, atau status UI, menunjukkan apa yang harus ditampilkan di layar (misalnya, daftar tugas). Status ini biasanya terhubung dengan lapisan hierarki lain karena berisi data aplikasi.

Sementara status UI menjelaskan apa yang akan ditampilkan di layar, logika aplikasi menjelaskan bagaimana aplikasi berperilaku dan harus bereaksi terhadap perubahan status. Ada dua jenis logika: perilaku UI atau logika UI, dan logika bisnis.

  • Logika UI berkaitan dengan cara menampilkan perubahan status di layar (misalnya, logika navigasi atau menampilkan snackbar).
  • Logika bisnis adalah apa yang harus dilakukan dengan perubahan status (misalnya, melakukan pembayaran atau menyimpan preferensi pengguna). Logika ini biasanya ditempatkan di lapisan bisnis atau data, bukan di lapisan UI.

ViewModels menyediakan status UI dan akses ke logika bisnis yang ada di lapisan lain pada aplikasi. Selain itu, ViewModel bertahan dari perubahan konfigurasi, sehingga memiliki masa aktif lebih lama daripada Komposisi. ViewModel dapat mengikuti siklus proses host konten Compose—yaitu, aktivitas, fragmen, atau tujuan grafik Navigasi jika Anda menggunakan Navigasi Compose.

Untuk mempelajari arsitektur dan lapisan UI lebih lanjut, lihat dokumentasi lapisan UI.

Memigrasikan daftar dan menghapus metode

Mari migrasikan status UI, daftar, ke ViewModel Anda dan juga mulai mengekstrak logika bisnis ke dalamnya.

  1. Buat WellnessViewModel.kt file untuk menambahkan class ViewModel Anda.

Pindahkan getWellnessTasks() "sumber data" ke WellnessViewModel.

Tentukan variabel _tasks internal, menggunakan toMutableStateList seperti yang Anda lakukan sebelumnya, dan tampilkan tasks sebagai daftar, sehingga tidak dapat diubah dari luar ViewModel.

Implementasikan fungsi remove sederhana yang didelegasikan ke fungsi hapus bawaan dari daftar.

import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel

class WellnessViewModel : ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List<WellnessTask>
        get() = _tasks

   fun remove(item: WellnessTask) {
       _tasks.remove(item)
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
  1. Kita dapat mengakses ViewModel ini dari composable mana pun dengan memanggil fungsi viewModel().

Untuk menggunakan fungsi ini, buka file app/build.gradle, tambahkan library berikut, dan sinkronkan dependensi baru di Android Studio:

implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"
  1. Buka WellnessScreen. Buat instance ViewModel wellnessViewModel dengan memanggil viewModel(), sebagai parameter composable Layar, sehingga dapat diganti saat menguji composable ini, dan diangkat jika diperlukan. Sediakan WellnessTasksList dengan daftar tugas dan hapus fungsi ke lambda onCloseTask.
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCloseTask = { task -> wellnessViewModel.remove(task) })
   }
}

viewModel() menampilkan ViewModel yang sudah ada atau membuat yang baru dalam cakupan yang ditentukan. Instance ViewModel dipertahankan selama cakupan masih aktif. Misalnya, jika composable digunakan dalam suatu aktivitas, viewModel() akan menampilkan instance yang sama sampai aktivitas itu selesai atau prosesnya diakhiri.

Dan selesai. Anda telah mengintegrasikan ViewModel dengan bagian status dan logika bisnis dengan layar. Karena status dipertahankan di luar Komposisi dan disimpan oleh ViewModel, mutasi ke daftar tetap memiliki perubahan konfigurasi.

ViewModel tidak akan mempertahankan status aplikasi secara otomatis dalam skenario apa pun (misalnya, untuk penghentian proses yang dimulai oleh sistem). Untuk informasi mendetail tentang mempertahankan status UI aplikasi, periksa dokumentasi.

Memigrasikan status yang dicentang

Pemfaktoran ulang terakhir adalah memigrasikan status dan logika yang dicentang ke ViewModel. Dengan demikian, kode menjadi lebih sederhana dan lebih mudah diuji, dengan semua status dikelola oleh ViewModel.

  1. Pertama, ubah class model WellnessTask agar dapat menyimpan status yang dicentang dan menetapkan salah sebagai nilai default.
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
  1. Di ViewMode, implementasikan metode changeTaskChecked yang menerima tugas untuk memodifikasi dengan nilai baru untuk status yang dicentang.
class WellnessViewModel : ViewModel() {
   ...
   fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
       tasks.find { it.id == item.id }?.let { task ->
           task.checked = checked
       }
}
  1. Di WellnessScreen, berikan perilaku untuk onCheckedTask daftar dengan memanggil metode changeTaskChecked ViewModel. Fungsi kini akan terlihat seperti ini:
@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = viewModel.tasks,
           onCheckedTask = { task, checked ->
               wellnessViewModel.changeTaskChecked(task, checked)
           },
           onCloseTask = { task ->
               wellnessViewModel.remove(task)
           }
       )
   }
}
  1. Buka WellnessList dan tambahkan parameter fungsi lambda onCheckedTask sehingga Anda dapat meneruskannya ke WellnessTaskItem.
@Composable
fun WellnessTasksList(
   list: List<WellnessTask>,
   onCheckedTask: (WellnessTask, Boolean) -> Unit,
   onCloseTask: (WellnessTask) -> Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(
       modifier = modifier
   ) {
       items(
           items = list,
           key = { task -> task.id }
       ) { task ->
           WellnessTaskItem(
               taskName = task.label,
               checked = task.checked,
               onCheckedChange = { checked -> onCheckedTask(task, checked) },
               onClose = { onCloseTask(task) }
           )
       }
   }
}
  1. Bersihkan file WellnessTaskItem.kt. Kita tidak lagi memerlukan metode stateful, karena status CheckBox akan diangkat ke tingkat List. File hanya memiliki fungsi composable ini:
@Composable
fun WellnessTaskItem(
   taskName: String,
   checked: Boolean,
   onCheckedChange: (Boolean) -> Unit,
   onClose: () -> Unit,
   modifier: Modifier = Modifier
) {
   Row(
       modifier = modifier, verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           modifier = Modifier
               .weight(1f)
               .padding(start = 16.dp),
           text = taskName
       )
       Checkbox(
           checked = checked,
           onCheckedChange = onCheckedChange
       )
       IconButton(onClick = onClose) {
           Icon(Icons.Filled.Close, contentDescription = "Close")
       }
   }
}
  1. Jalankan aplikasi dan coba periksa tugas apa pun. Perhatikan bahwa memeriksa tugas apa pun belum berfungsi.

8e2e731c58123cd8.gif

Hal ini karena yang dilacak Compose untuk MutableList adalah perubahan terkait dengan penambahan dan penghapusan elemen. Inilah alasan mengapa penghapusan berfungsi. Namun, contoh ini tidak mengetahui perubahan pada nilai item baris (checkedState dalam kasus kita), kecuali jika Anda memintanya untuk melacaknya juga.

Ada dua cara untuk memperbaiki masalah ini:

  • Ubah class data WellnessTask agar checkedState menjadi MutableState<Boolean>, bukan Boolean, yang menyebabkan Compose melacak perubahan item.
  • Salin item yang akan Anda mutasi, hapus item dari daftar, dan tambahkan kembali item yang bermutasi ke daftar, yang menyebabkan Compose melacak perubahan daftar tersebut.

Ada pro dan kontra untuk kedua pendekatan ini. Misalnya, bergantung pada penerapan daftar yang Anda gunakan, menghapus dan membaca elemen mungkin mahal.

Jadi, misalnya Anda ingin menghindari operasi daftar yang mungkin mahal, dan membuat checkedState dapat diamati karena lebih efisien dan Compose-idiomatis.

WellnessTask baru dapat terlihat seperti ini:

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))

Seperti yang Anda lihat sebelumnya, Anda dapat menggunakan properti yang didelegasikan, yang menghasilkan penggunaan variabel checked yang lebih sederhana untuk kasus ini.

Ubah WellnessTask menjadi class, bukan class data. Buat WellnessTask menerima variabel initialChecked dengan nilai default false di konstruktor, lalu kita dapat menginisialisasi variabel checked dengan metode factory mutableStateOf dan mengambil initialChecked sebagai nilai default.

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

class WellnessTask(
    val id: Int,
    val label: String,
    initialChecked: Boolean = false
) {
    var checked by mutableStateOf(initialChecked)
}

Selesai. Solusi ini berfungsi, dan semua perubahan tetap ada selama rekomposisi dan konfigurasi.

c8e049d56e7b0eac.gif

Pengujian

Setelah logika bisnis difaktorkan ulang ke ViewModel, bukan digabungkan dalam fungsi composable, pengujian unit jauh lebih sederhana.

Anda dapat menggunakan uji instrumentasi untuk memverifikasi perilaku kode Compose yang benar dan bahwa status UI berfungsi dengan benar. Sebaiknya ikuti codelab Pengujian di Compose untuk mempelajari cara menguji UI Compose Anda.

13. Selamat

Bagus! Anda berhasil menyelesaikan codelab ini dan mempelajari semua API dasar supaya berfungsi dengan status di aplikasi Jetpack Compose.

Anda telah mempelajari cara memikirkan status dan peristiwa untuk mengekstrak composable stateless di Compose, dan cara Compose menggunakan pembaruan status untuk mendorong perubahan di UI.

Apa selanjutnya?

Lihat codelab lain di jalur Compose

Aplikasi contoh

  • JetNews menunjukkan praktik terbaik yang dijelaskan dalam codelab ini.

Dokumentasi lainnya

API Referensi