ViewModel dan Status dalam Compose

1. Sebelum memulai

Pada codelab sebelumnya, Anda telah mempelajari siklus proses aktivitas dan masalah siklus proses terkait dengan perubahan konfigurasi. Saat terjadi perubahan konfigurasi, Anda dapat menyimpan data aplikasi melalui berbagai cara, seperti menggunakan rememberSaveable atau menyimpan status instance. Namun, opsi ini dapat menimbulkan masalah. Biasanya, Anda dapat menggunakan rememberSaveable, tetapi hal ini mungkin berarti mempertahankan logika dalam atau dekat composable. Seiring aplikasi berkembang, Anda harus memindahkan data dan logika dari composable. Dalam codelab ini, Anda akan mempelajari cara efektif untuk mendesain aplikasi dan mempertahankan data aplikasi selama perubahan konfigurasi, dengan memanfaatkan library Android Jetpack, ViewModel, dan panduan arsitektur aplikasi Android.

Library Android Jetpack adalah kumpulan library untuk mempermudah Anda mengembangkan aplikasi Android yang hebat. Library ini membantu Anda mengikuti praktik terbaik, menghindari keharusan menulis kode boilerplate, dan menyederhanakan tugas-tugas rumit sehingga Anda dapat berfokus pada kode yang penting seperti logika aplikasi.

Arsitektur aplikasi adalah sekumpulan aturan desain untuk aplikasi. Mirip dengan cetak biru sebuah rumah, arsitektur memberikan struktur bagi aplikasi Anda. Arsitektur aplikasi yang baik dapat membuat kode Anda menjadi andal, fleksibel, skalabel, dapat diuji, dan mudah dikelola selama bertahun-tahun. Panduan arsitektur aplikasi memberikan rekomendasi tentang arsitektur aplikasi dan praktik terbaik yang disarankan.

Dalam codelab ini, Anda akan mempelajari cara menggunakan ViewModel, salah satu komponen arsitektur dari library Android Jetpack yang dapat menyimpan data aplikasi Anda. Data yang disimpan tidak akan hilang jika framework menghancurkan dan membuat ulang aktivitas selama perubahan konfigurasi atau peristiwa lainnya. Namun, data akan hilang jika aktivitas dihancurkan karena penghentian proses. ViewModel hanya meng-cache data melalui pembuatan ulang aktivitas cepat.

Prasyarat

  • Pengetahuan tentang Kotlin, termasuk fungsi, lambda, dan composable stateless
  • Pengetahuan dasar tentang cara membuat tata letak di Jetpack Compose
  • Pengetahuan dasar tentang Desain Material

Yang akan Anda pelajari

Yang akan Anda build

  • Aplikasi game Unscramble tempat pengguna dapat menebak kata yang ejaannya diacak

Yang akan Anda butuhkan

  • Versi terbaru Android Studio
  • Koneksi internet untuk mendownload kode awal

2. Ringkasan aplikasi

Ringkasan game

Aplikasi Unscramble adalah game pengacak ejaan kata untuk satu pemain. Aplikasi menampilkan kata acak, dan pemain harus menebak kata tersebut menggunakan semua huruf yang ditampilkan. Pemain akan mendapatkan poin jika kata tersebut benar. Jika tidak, pemain dapat mencoba menebak kata sebanyak-banyaknya. Aplikasi ini juga memiliki opsi untuk melewati kata saat ini. Di pojok kanan atas, aplikasi menampilkan jumlah kata, yaitu jumlah kata acak yang dimainkan dalam game saat ini. Ada 10 kata acak per game.

Mendapatkan kode awal

Untuk memulai, download kode awal:

Atau, Anda dapat membuat clone repositori GitHub untuk kode tersebut:

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git
$ cd basic-android-kotlin-compose-training-unscramble
$ git checkout starter

Anda dapat menjelajahi kode awal di repositori GitHub Unscramble.

3. Ringkasan aplikasi awal

Untuk memahami kode awal, selesaikan langkah-langkah berikut:

  1. Buka project dengan kode awal di Android Studio.
  2. Jalankan aplikasi di perangkat Android atau di emulator.
  3. Ketuk tombol Submit dan Skip untuk menguji aplikasi.

Anda akan menemukan bug di aplikasi. Kata acak tidak ditampilkan, tetapi di-hardcode ke "scrambleun" dan tidak ada yang terjadi saat Anda mengetuk tombol.

Dalam codelab ini, Anda akan menerapkan fungsi game menggunakan arsitektur aplikasi Android.

Panduan kode awal

Kode awal memiliki tata letak layar game yang telah didesain sebelumnya untuk Anda. Di jalur ini, Anda akan menerapkan logika game. Anda akan menggunakan komponen arsitektur untuk menerapkan arsitektur aplikasi yang disarankan dan menyelesaikan masalah yang disebutkan di atas. Berikut adalah panduan singkat beberapa file untuk membantu Anda memulai.

WordsData.kt

File ini berisi daftar kata yang digunakan dalam game, konstanta untuk jumlah maksimum kata per game, dan jumlah poin skor pemain untuk setiap kata yang benar.

package com.example.android.unscramble.data

const val MAX_NO_OF_WORDS = 10
const val SCORE_INCREASE = 20

// Set with all the words for the Game
val allWords: Set<String> =
   setOf(
       "animal",
       "auto",
       "anecdote",
       "alphabet",
       "all",
       "awesome",
       "arise",
       "balloon",
       "basket",
       "bench",
      // ...
       "zoology",
       "zone",
       "zeal"
)

MainActivity.kt

File ini sebagian besar berisi kode yang dihasilkan oleh template. Anda menampilkan composable GameScreen di blok setContent{}.

GameScreen.kt

Semua composable UI ditentukan dalam file GameScreen.kt. Bagian berikut memberikan panduan tentang beberapa fungsi composable.

GameStatus

GameStatus adalah fungsi composable yang menampilkan skor game di bagian bawah layar. Fungsi composable berisi composable teks di Card. Untuk saat ini, skor di-hardcode menjadi 0.

1a7e4472a5638d61.png

// No need to copy, this is included in the starter code.

@Composable
fun GameStatus(score: Int, modifier: Modifier = Modifier) {
    Card(
        modifier = modifier
    ) {
        Text(
            text = stringResource(R.string.score, score),
            style = typography.headlineMedium,
            modifier = Modifier.padding(8.dp)
        )
    }
}

GameLayout

GameLayout adalah fungsi composable yang menampilkan fungsi game utama, yang mencakup kata acak, petunjuk game, dan kolom teks yang menerima tebakan pengguna.

b6ddb1f07f10df0c.png

Perhatikan bahwa kode GameLayout di bawah berisi kolom dalam Card dengan tiga elemen turunan: teks kata acak, teks petunjuk, dan kolom teks untuk kata OutlinedTextField pengguna. Untuk saat ini, kata acak akan di-hardcode agar menjadi scrambleun. Selanjutnya di codelab, Anda akan mengimplementasikan fungsi untuk menampilkan kata dari file WordsData.kt.

// No need to copy, this is included in the starter code.

@Composable
fun GameLayout(modifier: Modifier = Modifier) {
   val mediumPadding = dimensionResource(R.dimen.padding_medium)
   Card(
       modifier = modifier,
       elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
   ) {
       Column(
           verticalArrangement = Arrangement.spacedBy(mediumPadding),
           horizontalAlignment = Alignment.CenterHorizontally,
           modifier = Modifier.padding(mediumPadding)
       ) {
           Text(
               modifier = Modifier
                   .clip(shapes.medium)
                   .background(colorScheme.surfaceTint)
                   .padding(horizontal = 10.dp, vertical = 4.dp)
                   .align(alignment = Alignment.End),
               text = stringResource(R.string.word_count, 0),
               style = typography.titleMedium,
               color = colorScheme.onPrimary
           )
           Text(
               text = "scrambleun",
               style = typography.displayMedium
           )
           Text(
               text = stringResource(R.string.instructions),
               textAlign = TextAlign.Center,
               style = typography.titleMedium
           )
           OutlinedTextField(
               value = "",
               singleLine = true,
               shape = shapes.large,
               modifier = Modifier.fillMaxWidth(),
               colors = TextFieldDefaults.textFieldColors(containerColor = colorScheme.surface),
               onValueChange = { },
               label = { Text(stringResource(R.string.enter_your_word)) },
               isError = false,
               keyboardOptions = KeyboardOptions.Default.copy(
                   imeAction = ImeAction.Done
               ),
               keyboardActions = KeyboardActions(
                   onDone = { }
               )
           )
       }
   }
}

Composable OutlinedTextField mirip dengan composable TextField dari aplikasi di codelab sebelumnya.

Kolom teks terdiri dari dua jenis:

  • Kolom teks yang terisi
  • Kolom teks dengan garis batas

3df34220c3d177eb.png

Kolom teks dengan garis batas memiliki lebih sedikit penekanan visual daripada kolom teks yang terisi. Saat muncul di tempat-tempat seperti formulir, tempat banyak kolom teks ditempatkan sekaligus, pengurangan penekanannya membantu menyederhanakan tata letak.

Dalam kode awal, OutlinedTextField tidak akan diperbarui saat pengguna memasukkan tebakan. Anda akan memperbarui fitur ini di codelab.

GameScreen

Composable GameScreen berisi fungsi composable GameStatus dan GameLayout, judul game, jumlah kata, dan composable untuk tombol Submit dan Skip.

ac79bf1ed6375a27.png

@Composable
fun GameScreen() {
    val mediumPadding = dimensionResource(R.dimen.padding_medium)

    Column(
        modifier = Modifier
            .verticalScroll(rememberScrollState())
            .padding(mediumPadding),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Text(
            text = stringResource(R.string.app_name),
            style = typography.titleLarge,
        )

        GameLayout(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight()
                .padding(mediumPadding)
        )
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(mediumPadding),
            verticalArrangement = Arrangement.spacedBy(mediumPadding),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {

            Button(
                modifier = Modifier.fillMaxWidth(),
                onClick = { }
            ) {
                Text(
                    text = stringResource(R.string.submit),
                    fontSize = 16.sp
                )
            }

            OutlinedButton(
                onClick = { },
                modifier = Modifier.fillMaxWidth()
            ) {
                Text(
                    text = stringResource(R.string.skip),
                    fontSize = 16.sp
                )
            }
        }

        GameStatus(score = 0, modifier = Modifier.padding(20.dp))
    }
}

Peristiwa klik tombol tidak diterapkan di kode awal. Anda akan menerapkan peristiwa ini sebagai bagian dari codelab.

FinalScoreDialog

Composable FinalScoreDialog menampilkan dialog, yaitu jendela kecil yang memberikan permintaan kepada pengguna, dengan opsi untuk Play Again atau Exit game. Nanti dalam codelab ini, Anda akan menerapkan logika untuk menampilkan dialog ini di akhir game.

dba2d9ea62aaa982.png

// No need to copy, this is included in the starter code.

@Composable
private fun FinalScoreDialog(
    score: Int,
    onPlayAgain: () -> Unit,
    modifier: Modifier = Modifier
) {
    val activity = (LocalContext.current as Activity)

    AlertDialog(
        onDismissRequest = {
           // Dismiss the dialog when the user clicks outside the dialog or on the back
           // button. If you want to disable that functionality, simply use an empty
           // onDismissRequest.
        },
        title = { Text(text = stringResource(R.string.congratulations)) },
        text = { Text(text = stringResource(R.string.you_scored, score)) },
        modifier = modifier,
        dismissButton = {
            TextButton(
                onClick = {
                    activity.finish()
                }
            ) {
                Text(text = stringResource(R.string.exit))
            }
        },
        confirmButton = {
            TextButton(onClick = onPlayAgain) {
                Text(text = stringResource(R.string.play_again))
            }
        }
    )
}

4. Pelajari tentang arsitektur aplikasi

Arsitektur aplikasi memberikan panduan untuk membantu Anda mengalokasikan tanggung jawab aplikasi antar-class. Arsitektur aplikasi yang dirancang dengan baik akan membantu Anda meningkatkan skala aplikasi dan menambahkan fitur lain. Arsitektur juga dapat menyederhanakan kolaborasi tim.

Prinsip arsitektur yang paling umum adalah pemisahan fokus dan menjalankan UI dari model.

Memisahkan fokus

Prinsip desain pemisahan fokus menyatakan bahwa aplikasi akan dibagi ke dalam beberapa class fungsi, masing-masing dengan tanggung jawab yang terpisah.

Menjalankan UI dari model

UI drive dari prinsip model menyatakan bahwa Anda harus menjalankan UI dari suatu model, terutama model yang persisten. Model adalah komponen yang bertanggung jawab menangani data untuk sebuah aplikasi. Model tidak terikat dengan elemen UI dan komponen aplikasi dalam aplikasi Anda, sehingga tidak terpengaruh oleh siklus proses aplikasi dan masalah terkaitnya.

Dengan mempertimbangkan prinsip arsitektur umum yang disebutkan di bagian sebelumnya, setiap aplikasi harus memiliki setidaknya dua lapisan:

  • Lapisan UI: lapisan yang menampilkan data aplikasi di layar, tetapi terlepas dari data.
  • Lapisan data: lapisan yang menyimpan, mengambil, dan menampilkan data aplikasi.

Anda dapat menambahkan lapisan lainnya, yang disebut lapisan domain untuk menyederhanakan dan menggunakan kembali interaksi antara lapisan UI dan data. Lapisan ini bersifat opsional dan di luar cakupan kursus ini.

a4da6fa5c1c9fed5.png

Lapisan UI

Peran lapisan UI, atau lapisan presentasi, adalah menampilkan data aplikasi di layar. Setiap kali data berubah karena interaksi pengguna, seperti menekan tombol, UI harus diperbarui untuk mencerminkan perubahan.

Lapisan UI terdiri dari komponen berikut:

  • Elemen UI: komponen yang merender data di layar. Anda membuat elemen ini menggunakan Jetpack Compose.
  • Pemegang status: komponen yang menyimpan data, mengeksposnya ke UI, dan menangani logika aplikasi. Contoh holder status adalah ViewModel.

6eaee5b38ec247ae.png

ViewModel

Komponen ViewModel menyimpan dan menampilkan status yang digunakan UI. Status UI adalah data aplikasi yang ditransformasikan oleh ViewModel. ViewModel memungkinkan aplikasi Anda mengikuti prinsip arsitektur dalam menjalankan UI dari model.

ViewModel menyimpan data terkait aplikasi yang tidak dihancurkan saat aktivitas dihancurkan dan dibuat ulang oleh framework Android. Tidak seperti instance aktivitas, objek ViewModel tidak dihancurkan. Aplikasi secara otomatis mempertahankan objek ViewModel selama perubahan konfigurasi sehingga data yang disimpan segera tersedia setelah rekomposisi.

Untuk mengimplementasikan ViewModel di aplikasi Anda, perluas class ViewModel, yang berasal dari library komponen arsitektur, dan menyimpan data aplikasi dalam class tersebut.

Status UI

UI adalah apa yang dilihat pengguna, dan status UI adalah tampilan berdasarkan apa yang diberitahukan aplikasi. UI adalah representasi visual status UI. Setiap perubahan pada status UI akan segera ditampilkan di UI.

9cfedef1750ddd2c.png

UI adalah hasil dari binding elemen UI di layar dengan status UI.

// Example of UI state definition, do not copy over

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

Ketetapan

Definisi status UI dalam contoh di atas tidak dapat diubah. Objek yang tidak dapat diubah memberikan jaminan bahwa beberapa sumber tidak mengubah status aplikasi secara instan. Perlindungan ini akan mengosongkan UI untuk berfokus pada satu peran: membaca status dan mengubah elemen UI sebagaimana mestinya. Sehingga, Anda tidak boleh memodifikasi status UI secara langsung di UI, kecuali jika UI tersebut adalah satu-satunya sumber datanya. Melanggar prinsip ini menghasilkan beberapa sumber tepercaya untuk informasi yang sama, sehingga mengakibatkan inkonsistensi data dan bug yang tidak jelas.

5. Menambahkan ViewModel

Dalam tugas ini, Anda menambahkan ViewModel ke aplikasi untuk menyimpan status UI game (kata acak, jumlah kata, dan skor). Untuk mengatasi masalah pada kode awal yang Anda lihat di bagian sebelumnya, Anda perlu menyimpan data game di ViewModel.

  1. Buka build.gradle.kts (Module :app), scroll ke blok dependencies, lalu tambahkan dependensi berikut untuk ViewModel. Dependensi ini digunakan untuk menambahkan viewmodel berbasis siklus proses ke aplikasi compose Anda.
dependencies {
// other dependencies

    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
//...
}
  1. Di paket ui, buat class/file Kotlin bernama GameViewModel. Perluas dari class ViewModel.
import androidx.lifecycle.ViewModel

class GameViewModel : ViewModel() {
}
  1. Dalam paket ui, tambahkan class model untuk UI status yang disebut GameUiState. Jadikan class data dan tambahkan variabel untuk kata acak saat ini.
data class GameUiState(
   val currentScrambledWord: String = ""
)

StateFlow

StateFlow adalah alur holder data yang dapat diamati, yang menampilkan pembaruan status saat ini dan yang baru. Properti value mencerminkan nilai status saat ini. Untuk memperbarui status dan mengirimkannya ke alur, setel nilai baru ke properti nilai dari class MutableStateFlow.

Di Android, StateFlow berfungsi baik dengan class yang harus mempertahankan status tetap yang dapat diamati.

StateFlow dapat ditampilkan dari GameUiState agar composable dapat memproses pembaruan status UI dan membuat status layar tetap bertahan saat terjadi perubahan konfigurasi.

Di class GameViewModel, tambahkan properti _uiState berikut.

import kotlinx.coroutines.flow.MutableStateFlow

// Game UI state
private val _uiState = MutableStateFlow(GameUiState())

Properti pendukung

Properti pendukung memungkinkan Anda menampilkan sesuatu dari pengambil selain dari objek yang tepat.

Untuk properti var, framework Kotlin menghasilkan pengambil dan penyetel.

Untuk metode pengambil dan penyetel, Anda dapat mengganti salah satu atau kedua metode tersebut dan memberikan perilaku kustom Anda sendiri. Untuk mengimplementasikan properti pendukung, Anda akan mengganti metode pengambil untuk menampilkan versi hanya-baca data. Contoh berikut menunjukkan properti pendukung:

//Example code, no need to copy over

// Declare private mutable variable that can only be modified
// within the class it is declared.
private var _count = 0 

// Declare another public immutable field and override its getter method. 
// Return the private property's value in the getter method.
// When count is accessed, the get() function is called and
// the value of _count is returned. 
val count: Int
    get() = _count

Sebagai contoh lainnya, misalnya Anda ingin data aplikasi bersifat pribadi untuk ViewModel:

Di dalam class ViewModel:

  • Properti _count bersifat private dan dapat diubah. Oleh karena itu, ini hanya dapat diakses dan diedit dalam class ViewModel.

Di luar class ViewModel:

  • Pengubah visibilitas default di Kotlin adalah public, sehingga count bersifat publik dan dapat diakses dari class lain seperti pengontrol UI. Jenis val tidak boleh memiliki penyetel. Metode ini tidak dapat diubah dan bersifat hanya-baca sehingga Anda hanya dapat mengganti metode get(). Saat class luar mengakses properti ini, properti akan menampilkan nilai _count dan nilainya tidak dapat diubah. Properti pendukung ini melindungi data aplikasi di dalam ViewModel dari perubahan yang tidak diinginkan dan tidak aman oleh class eksternal, tetapi memungkinkan pemanggil eksternal mengakses nilainya dengan aman.
  1. Di file GameViewModel.kt, tambahkan properti pendukung ke uiState yang bernama _uiState. Beri nama properti uiState dan berjenis StateFlow<GameUiState>.

Sekarang _uiState hanya dapat diakses dan diedit dalam GameViewModel. UI dapat membaca nilainya menggunakan properti hanya baca, uiState. Anda dapat memperbaiki error inisialisasi pada langkah berikutnya.

import kotlinx.coroutines.flow.StateFlow

// Game UI state

// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> 
  1. Setel uiState ke _uiState.asStateFlow().

asStateFlow() membuat alur status yang dapat berubah ini menjadi alur status hanya baca.

import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

// Game UI state
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()

Menampilkan kata acak tanpa pola

Dalam tugas ini, Anda menambahkan metode bantuan untuk memilih kata acak dari WordsData.kt dan mengacak kata.

  1. Di GameViewModel, tambahkan properti bernama currentWord dari jenis String untuk menyimpan kata acak saat ini.
private lateinit var currentWord: String
  1. Tambahkan metode bantuan untuk pilih kata acak dari daftar dan acaklah. Beri nama pickRandomWordAndShuffle() tanpa parameter input, lalu buat fungsi tersebut menampilkan String.
import com.example.unscramble.data.allWords

private fun pickRandomWordAndShuffle(): String {
   // Continue picking up a new random word until you get one that hasn't been used before
   currentWord = allWords.random()
   if (usedWords.contains(currentWord)) {
       return pickRandomWordAndShuffle()
   } else {
       usedWords.add(currentWord)
       return shuffleCurrentWord(currentWord)
   }
}

Android Studio menandai error untuk fungsi dan variabel yang tidak ditentukan.

  1. Di GameViewModel, tambahkan properti berikut setelah properti currentWord agar berfungsi sebagai kumpulan yang dapat diubah untuk menyimpan kata yang telah digunakan dalam game.
// Set of words used in the game
private var usedWords: MutableSet<String> = mutableSetOf()
  1. Tambahkan metode helper lain untuk mengacak kata saat ini yang disebut shuffleCurrentWord() yang menggunakan String dan menampilkan String yang diacak.
private fun shuffleCurrentWord(word: String): String {
   val tempWord = word.toCharArray()
   // Scramble the word
   tempWord.shuffle()
   while (String(tempWord).equals(word)) {
       tempWord.shuffle()
   }
   return String(tempWord)
}
  1. Tambahkan fungsi bantuan untuk melakukan inisialisasi game yang disebut resetGame(). Gunakan fungsi ini nanti untuk memulai dan memulai ulang game. Pada fungsi ini, hapus semua kata dalam kumpulan usedWords, lakukan inisialisasi _uiState. Pilih kata baru untuk currentScrambledWord menggunakan pickRandomWordAndShuffle().
fun resetGame() {
   usedWords.clear()
   _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
  1. Tambahkan blok init ke GameViewModel dan panggil resetGame() dari blok tersebut.
init {
   resetGame()
}

Ketika membangun aplikasi sekarang, Anda masih belum melihat perubahan pada UI. Anda tidak meneruskan data dari ViewModel ke composable di GameScreen.

6. Merancang Compose UI Anda

Di Compose, satu-satunya cara untuk mengupdate UI adalah dengan mengubah status aplikasi. Yang dapat Anda kontrol adalah status UI Anda. Setiap kali status UI berubah, Compose membuat ulang bagian hierarki UI yang telah berubah. Composable dapat menerima status dan menampilkan peristiwa. Misalnya, TextField/OutlinedTextField menerima nilai dan menampilkan callback onValueChange yang meminta pengendali callback untuk mengubah nilai.

//Example code no need to copy over

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

Karena composable menerima status dan menampilkan peristiwa, pola aliran data searah akan sesuai dengan Jetpack Compose. Bagian ini berfokus pada cara menerapkan pola aliran data searah di Compose, cara menerapkan peristiwa dan holder status, dan cara menggunakan ViewModel di Compose.

Aliran data searah

Aliran data searah (UDF) adalah pola desain dengan status mengalir ke bawah dan peristiwa mengalir ke atas. Dengan mengikuti alur data searah, Anda dapat memisahkan composable yang menampilkan status di UI dari bagian aplikasi yang menyimpan dan mengubah status.

Loop update UI untuk aplikasi yang menggunakan aliran data searah terlihat seperti berikut:

  • Peristiwa: Bagian UI menghasilkan peristiwa dan meneruskannya ke atas (seperti klik tombol yang diteruskan ke ViewModel untuk ditangani) atau peristiwa diteruskan dari lapisan lain aplikasi Anda (seperti indikasi bahwa sesi pengguna telah berakhir).
  • Status update: Handler peristiwa dapat mengubah status.
  • Status tampilan: Holder status akan menurunkan status, dan UI akan menampilkannya.

61eb7bcdcff42227.png

Penggunaan pola UDF untuk arsitektur aplikasi memiliki implikasi berikut:

  • ViewModel menyimpan dan menampilkan status yang digunakan UI.
  • Status UI adalah data aplikasi yang diubah oleh ViewModel.
  • UI memberi tahu ViewModel tentang peristiwa pengguna.
  • ViewModel menangani tindakan pengguna dan memperbarui status.
  • Status yang diperbarui dimasukkan kembali ke UI untuk dirender.
  • Proses ini berulang untuk setiap peristiwa yang menyebabkan mutasi status.

Meneruskan data

Teruskan instance ViewModel ke UI, yaitu dari GameViewModel ke GameScreen() di file GameScreen.kt. Di GameScreen(), gunakan instance ViewModel untuk mengakses uiState menggunakan collectAsState().

Fungsi collectAsState() mengumpulkan nilai dari StateFlow ini dan mewakili nilai terbarunya melalui State. StateFlow.value digunakan sebagai nilai awal. Setiap saat akan ada nilai baru yang diposting ke StateFlow, pembaruan State yang ditampilkan, yang menyebabkan rekomposisi setiap penggunaan State.value.

  1. Pada fungsi GameScreen, teruskan argumen kedua dari jenis GameViewModel dengan nilai default viewModel().
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun GameScreen(
   gameViewModel: GameViewModel = viewModel()
) {
   // ...
}

de93b81a92416c23.png

  1. Pada fungsi GameScreen(), tambahkan variabel baru bernama gameUiState. Gunakan delegasi by dan panggil collectAsState() pada uiState.

Pendekatan ini memastikan bahwa setiap kali ada perubahan dalam nilai uiState, rekomposisi terjadi untuk composable menggunakan nilai gameUiState.

import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

@Composable
fun GameScreen(
   // ...
) {
   val gameUiState by gameViewModel.uiState.collectAsState()
   // ...
}
  1. Teruskan gameUiState.currentScrambledWord ke composable GameLayout(). Anda menambahkan argumen di langkah selanjutnya, jadi abaikan error untuk saat ini.
GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   modifier = Modifier
       .fillMaxWidth()
       .wrapContentHeight()
       .padding(mediumPadding)
)
  1. Tambahkan currentScrambledWord sebagai parameter lain ke fungsi composable GameLayout().
@Composable
fun GameLayout(
   currentScrambledWord: String,
   modifier: Modifier = Modifier
) {
}
  1. Perbarui fungsi composable GameLayout() untuk menampilkan currentScrambledWord. Tetapkan parameter text kolom teks pertama di kolom ke currentScrambledWord.
@Composable
fun GameLayout(
   // ...
) {
   Column(
       verticalArrangement = Arrangement.spacedBy(24.dp)
   ) {
       Text(
           text = currentScrambledWord,
           fontSize = 45.sp,
           modifier = modifier.align(Alignment.CenterHorizontally)
       )
    //... 
    }
}
  1. Jalankan dan bangun aplikasi. Anda akan melihat kata yang ejaannya diacak.

6d93a8e1ba5dad6f.png

Menampilkan kata tebakan

Dalam composable GameLayout(), memperbarui kata tebakan pengguna adalah salah satu callback peristiwa yang mengalir ke atas dari GameScreen ke ViewModel. Data gameViewModel.userGuess akan mengalir ke bawah dari ViewModel ke GameScreen.

keyboard callback peristiwa selesai menekan tombol dan perubahan tebakan pengguna diteruskan dari UI ke model tampilan

  1. Di file GameScreen.kt, dalam composable GameLayout(), setel onValueChange ke onUserGuessChanged dan onKeyboardDone() ke tindakan keyboard onDone. Anda dapat memperbaiki error tersebut di langkah berikutnya.
OutlinedTextField(
   value = "",
   singleLine = true,
   modifier = Modifier.fillMaxWidth(),
   onValueChange = onUserGuessChanged,
   label = { Text(stringResource(R.string.enter_your_word)) },
   isError = false,
   keyboardOptions = KeyboardOptions.Default.copy(
       imeAction = ImeAction.Done
   ),
   keyboardActions = KeyboardActions(
       onDone = { onKeyboardDone() }
   ),
  1. Pada fungsi composable GameLayout(), tambahkan dua argumen lagi: lambda onUserGuessChanged menggunakan argumen String dan tidak menampilkan apa pun, serta onKeyboardDone tidak mengambil apa pun dan tidak menampilkan apa pun.
@Composable
fun GameLayout(
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   currentScrambledWord: String,
   modifier: Modifier = Modifier,
   ) {
}
  1. Pada panggilan fungsi GameLayout(), tambahkan argumen lambda untuk onUserGuessChanged dan onKeyboardDone.
GameLayout(
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { },
   currentScrambledWord = gameUiState.currentScrambledWord,
)

Anda segera menentukan metode updateUserGuess di GameViewModel.

  1. Di file GameViewModel.kt, tambahkan metode bernama updateUserGuess() yang menggunakan argumen String, kata tebakan pengguna. Di dalam fungsi, perbarui userGuess dengan meneruskan guessedWord.
  fun updateUserGuess(guessedWord: String){
     userGuess = guessedWord
  }

Kemudian, Anda menambahkan userGuess di ViewModel.

  1. Di file GameViewModel.kt, tambahkan properti var yang disebut userGuess. Gunakan mutableStateOf() agar Compose mengamati nilai ini dan menetapkan nilai awal ke "".
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

var userGuess by mutableStateOf("")
   private set
  1. Di file GameScreen.kt, di dalam GameLayout(), tambahkan parameter String lain untuk userGuess. Tetapkan parameter value dari OutlinedTextField ke userGuess.
fun GameLayout(
   currentScrambledWord: String,
   userGuess: String,
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   modifier: Modifier = Modifier
) {
   Column(
       verticalArrangement = Arrangement.spacedBy(24.dp)
   ) {
       //...
       OutlinedTextField(
           value = userGuess,
           //..
       )
   }
}
  1. Pada fungsi GameScreen, update panggilan fungsi GameLayout() untuk menyertakan parameter userGuess.
GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   userGuess = gameViewModel.userGuess,
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { },
   //...
)
  1. Bangun dan jalankan aplikasi Anda.
  2. Coba menebak dan masukkan kata. Kolom teks dapat menampilkan tebakan pengguna.

ed10c7f522495a.png

7. Memverifikasi kata tebakan dan memperbarui skor

Dalam tugas ini, Anda akan mengimplementasikan metode untuk memverifikasi kata yang ditebak pengguna, lalu memperbarui skor game atau menampilkan error. Anda akan memperbarui UI status game dengan skor baru dan kata baru nanti.

  1. Di GameViewModel, tambahkan metode lain bernama checkUserGuess().
  2. Pada fungsi checkUserGuess(), tambahkan blok if else untuk memverifikasi apakah tebakan pengguna sama dengan currentWord. Reset userGuess ke string yang kosong.
fun checkUserGuess() {
   
   if (userGuess.equals(currentWord, ignoreCase = true)) {
   } else {
   }
   // Reset user guess
   updateUserGuess("")
}
  1. Jika tebakan pengguna salah, tetapkan isGuessedWordWrong ke true. MutableStateFlow<T>. update() memperbarui MutableStateFlow.value menggunakan nilai yang ditentukan.
import kotlinx.coroutines.flow.update

   if (userGuess.equals(currentWord, ignoreCase = true)) {
   } else {
       // User's guess is wrong, show an error
       _uiState.update { currentState ->
           currentState.copy(isGuessedWordWrong = true)
       }
   }
  1. Di class GameUiState, tambahkan Boolean yang disebut isGuessedWordWrong dan lakukan inisialisasi ke false.
data class GameUiState(
   val currentScrambledWord: String = "",
   val isGuessedWordWrong: Boolean = false,
)

Selanjutnya, Anda meneruskan callback peristiwa checkUserGuess() ke atas dari GameScreen ke ViewModel saat pengguna mengklik tombol Submit atau tombol selesai di keyboard. Teruskan data, gameUiState.isGuessedWordWrong dari ViewModel ke GameScreen untuk menetapkan error di kolom teks.

7f05d04164aa4646.png

  1. Di file GameScreen.kt, di akhir fungsi composable GameScreen(), panggil gameViewModel.checkUserGuess() di dalam ekspresi lambda onClick dari tombol Submit.
Button(
   modifier = modifier
       .fillMaxWidth()
       .weight(1f)
       .padding(start = 8.dp),
   onClick = { gameViewModel.checkUserGuess() }
) {
   Text(stringResource(R.string.submit))
}
  1. Pada fungsi composable GameScreen(), update panggilan fungsi GameLayout() untuk meneruskan gameViewModel.checkUserGuess() dalam ekspresi lambda onKeyboardDone.
GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   userGuess = gameViewModel.userGuess,
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { gameViewModel.checkUserGuess() }
)
  1. Pada fungsi composable GameLayout(), tambahkan parameter fungsi untuk Boolean, isGuessWrong. Tetapkan parameter isError dari OutlinedTextField ke isGuessWrong untuk menampilkan error di kolom teks jika tebakan pengguna salah.
fun GameLayout(
   currentScrambledWord: String,
   isGuessWrong: Boolean,
   userGuess: String,
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   modifier: Modifier = Modifier
) {
   Column(
       // ,...
       OutlinedTextField(
           // ...
           isError = isGuessWrong,
           keyboardOptions = KeyboardOptions.Default.copy(
               imeAction = ImeAction.Done
           ),
           keyboardActions = KeyboardActions(
               onDone = { onKeyboardDone() }
           ),
       )
}
}
  1. Pada fungsi composable GameScreen(), update panggilan fungsi GameLayout() untuk meneruskan isGuessWrong.
GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   userGuess = gameViewModel.userGuess,
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { gameViewModel.checkUserGuess() },
   isGuessWrong = gameUiState.isGuessedWordWrong,
   // ...
)
  1. Build dan jalankan aplikasi Anda.
  2. Masukkan tebakan yang salah dan klik Submit. Perhatikan bahwa kolom teks berubah menjadi merah, yang menunjukkan error.

a1bc55781d627b38.png

Perhatikan bahwa label kolom teks masih bertuliskan "Enter your word". Agar mudah digunakan, Anda perlu menambahkan beberapa teks error untuk menunjukkan bahwa kata tersebut salah.

  1. Di file GameScreen.kt, dalam composable GameLayout(), perbarui parameter label kolom teks bergantung pada isGuessWrong sebagai berikut:
OutlinedTextField(
   // ...
   label = {
       if (isGuessWrong) {
           Text(stringResource(R.string.wrong_guess))
       } else {
           Text(stringResource(R.string.enter_your_word))
       }
   },
   // ...
)
  1. Di file strings.xml, tambahkan string ke label error.
<string name="wrong_guess">Wrong Guess!</string>
  1. Build dan jalankan aplikasi kembali.
  2. Masukkan tebakan yang salah dan klik Submit. Perhatikan label error.

8c17eb61e9305d49.png

8. Memperbarui skor dan jumlah kata

Dalam tugas ini, Anda akan memperbarui skor dan jumlah kata saat pengguna memainkan game. Skor harus bagian dari _ uiState.

  1. Di GameUiState, tambahkan variabel score dan lakukan inisialisasi ke nol.
data class GameUiState(
   val currentScrambledWord: String = "",
   val isGuessedWordWrong: Boolean = false,
   val score: Int = 0
)
  1. Untuk memperbarui nilai skor, di GameViewModel, pada fungsi checkUserGuess(), di dalam kondisi if untuk saat tebakan pengguna benar, tingkatkan nilai score.
import com.example.unscramble.data.SCORE_INCREASE

fun checkUserGuess() {
   if (userGuess.equals(currentWord, ignoreCase = true)) {
       // User's guess is correct, increase the score
       val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
   } else {
       //...
   }
}
  1. Di GameViewModel, tambahkan metode lain bernama updateGameState untuk memperbarui skor, menambah jumlah kata saat ini, dan memilih kata baru dari file WordsData.kt. Tambahkan Int yang bernama updatedScore sebagai parameter. Update variabel UI status game sebagai berikut:
private fun updateGameState(updatedScore: Int) {
   _uiState.update { currentState ->
       currentState.copy(
           isGuessedWordWrong = false,
           currentScrambledWord = pickRandomWordAndShuffle(),
           score = updatedScore
       )
   }
}
  1. Pada fungsi checkUserGuess(), jika tebakan pengguna benar, lakukan panggilan ke updateGameState dengan skor yang telah diperbarui untuk menyiapkan game untuk putaran berikutnya.
fun checkUserGuess() {
   if (userGuess.equals(currentWord, ignoreCase = true)) {
       // User's guess is correct, increase the score
       // and call updateGameState() to prepare the game for next round
       val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
       updateGameState(updatedScore)
   } else {
       //...
   }
}

checkUserGuess() yang sudah selesai akan terlihat seperti berikut:

fun checkUserGuess() {
   if (userGuess.equals(currentWord, ignoreCase = true)) {
       // User's guess is correct, increase the score
       // and call updateGameState() to prepare the game for next round
       val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
       updateGameState(updatedScore)
   } else {
       // User's guess is wrong, show an error
       _uiState.update { currentState ->
           currentState.copy(isGuessedWordWrong = true)
       }
   }
   // Reset user guess
   updateUserGuess("")
}

Selanjutnya, mirip dengan pembaruan skor, Anda harus memperbarui jumlah kata.

  1. Tambahkan variabel lain untuk jumlah di GameUiState. Panggil currentWordCount dan lakukan inisialisasi ke 1.
data class GameUiState(
   val currentScrambledWord: String = "",
   val currentWordCount: Int = 1,
   val score: Int = 0,
   val isGuessedWordWrong: Boolean = false,
)
  1. Di file GameViewModel.kt, pada fungsi updateGameState(), tingkatkan jumlah kata seperti yang ditunjukkan di bawah. Fungsi updateGameState() dipanggil untuk menyiapkan game untuk putaran berikutnya.
private fun updateGameState(updatedScore: Int) {
   _uiState.update { currentState ->
       currentState.copy(
           //...
           currentWordCount = currentState.currentWordCount.inc(),
           )
   }
}

Skor kelulusan dan jumlah kata

Selesaikan langkah-langkah berikut untuk meneruskan data skor dan jumlah kata dari ViewModel ke GameScreen.

546e101980380f80.png

  1. Di file GameScreen.kt, pada fungsi composable GameLayout(), tambahkan jumlah kata sebagai argumen dan teruskan argumen format wordCount ke elemen teks.
fun GameLayout(
   onUserGuessChanged: (String) -> Unit,
   onKeyboardDone: () -> Unit,
   wordCount: Int,
   //...
) {
   //...

   Card(
       //...
   ) {
       Column(
           // ...
       ) {
           Text(
               //..
               text = stringResource(R.string.word_count, wordCount),
               style = typography.titleMedium,
               color = colorScheme.onPrimary
           )

// ...

}
  1. Perbarui panggilan fungsi GameLayout() untuk menyertakan jumlah kata.
GameLayout(
   userGuess = gameViewModel.userGuess,
   wordCount = gameUiState.currentWordCount,
   //...
)
  1. Pada fungsi composable GameScreen(), perbarui panggilan fungsi GameStatus() untuk menyertakan parameter score. Teruskan skor dari gameUiState.
GameStatus(score = gameUiState.score, modifier = Modifier.padding(20.dp))
  1. Bangun dan jalankan aplikasi.
  2. Masukkan kata tebakan, lalu klik Submit. Perhatikan skor dan jumlah kata yang diperbarui.
  3. Klik Skip, dan perhatikan bahwa tidak ada yang terjadi.

Untuk mengimplementasikan fungsi lewati, Anda harus meneruskan callback peristiwa lewati ke GameViewModel.

  1. Di file GameScreen.kt, pada fungsi composable GameScreen(), lakukan panggilan ke gameViewModel.skipWord() dalam ekspresi lambda onClick.

Android Studio menampilkan error karena Anda belum menerapkan fungsi. Anda dapat memperbaiki error ini di langkah berikutnya dengan menambahkan metode skipWord(). Jika pengguna melewati kata, Anda perlu memperbarui variabel game dan menyiapkan game untuk putaran berikutnya.

OutlinedButton(
   onClick = { gameViewModel.skipWord() },
   modifier = Modifier.fillMaxWidth()
) {
   //...
}
  1. Di GameViewModel, tambahkan metode skipWord().
  2. Di dalam fungsi skipWord(), lakukan panggilan ke updateGameState(), dengan meneruskan skor dan mereset tebakan pengguna.
fun skipWord() {
   updateGameState(_uiState.value.score)
   // Reset user guess
   updateUserGuess("")
}
  1. Jalankan aplikasi Anda dan mainkan game-nya. Sekarang Anda akan dapat melewati kata.

e87bd75ba1269e96.png

Anda tetap dapat memainkan game lebih dari 10 kata. Pada tugas berikutnya, Anda akan menangani ronde terakhir game.

9. Menangani putaran terakhir game

Dalam implementasi saat ini, pengguna dapat melewati atau memainkan lebih dari 10 kata. Dalam tugas ini, Anda menambahkan logika untuk mengakhiri game.

d3fd67d92c5d3c35.png

Untuk mengimplementasikan logika akhir game, Anda harus memeriksa terlebih dahulu apakah pengguna telah mencapai jumlah kata maksimum.

  1. Di GameViewModel, tambahkan blok if-else dan pindahkan isi fungsi yang ada ke dalam blok else.
  2. Tambahkan kondisi if untuk memastikan ukuran usedWords sama dengan MAX_NO_OF_WORDS.
import com.example.android.unscramble.data.MAX_NO_OF_WORDS

private fun updateGameState(updatedScore: Int) {
   if (usedWords.size == MAX_NO_OF_WORDS){
       //Last round in the game
   } else{
       // Normal round in the game
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               currentScrambledWord = pickRandomWordAndShuffle(),
               currentWordCount = currentState.currentWordCount.inc(),
               score = updatedScore
           )
       }
   }
}
  1. Di dalam blok if, tambahkan flag Boolean isGameOver dan setel flag ke true untuk menunjukkan akhir game.
  2. Perbarui score dan reset isGuessedWordWrong di dalam blok if. Kode berikut menunjukkan tampilan fungsi Anda:
private fun updateGameState(updatedScore: Int) {
   if (usedWords.size == MAX_NO_OF_WORDS){
       //Last round in the game, update isGameOver to true, don't pick a new word
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               score = updatedScore,
               isGameOver = true
           )
       }
   } else{
       // Normal round in the game
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               currentScrambledWord = pickRandomWordAndShuffle(),
               currentWordCount = currentState.currentWordCount.inc(),
               score = updatedScore
           )
       }
   }
}
  1. Di GameUiState, tambahkan variabel Boolean isGameOver dan tetapkan ke false.
data class GameUiState(
   val currentScrambledWord: String = "",
   val currentWordCount: Int = 1,
   val score: Int = 0,
   val isGuessedWordWrong: Boolean = false,
   val isGameOver: Boolean = false
)
  1. Jalankan aplikasi Anda dan mainkan game-nya. Anda tidak dapat memainkan lebih dari 10 kata.

ac8a12e66111f071.png

Saat game berakhir, sebaiknya beri tahu pengguna dan tanyakan apakah mereka ingin bermain lagi. Anda akan menerapkan fitur ini di tugas berikutnya.

Menampilkan dialog akhir game

Dalam tugas ini, Anda meneruskan data isGameOver ke GameScreen dari ViewModel dan menggunakannya untuk menampilkan dialog pemberitahuan dengan opsi untuk mengakhiri atau memulai ulang game.

Dialog adalah jendela kecil yang meminta pengguna untuk membuat keputusan atau memasukkan informasi tambahan. Biasanya, dialog tidak mengisi seluruh layar, dan mengharuskan pengguna untuk melakukan suatu tindakan agar bisa melanjutkan. Android menyediakan berbagai jenis dialog. Dalam codelab ini, Anda akan mempelajari Dialog Pemberitahuan.

Anatomi dialog pemberitahuan

eb6edcdd0818b900.png

  1. Penampung
  2. Ikon (opsional)
  3. Judul (opsional)
  4. Teks pendukung
  5. Pembagi (opsional)
  6. Tindakan

File GameScreen.kt dalam kode awal sudah menyediakan fungsi yang menampilkan dialog pemberitahuan dengan opsi untuk keluar atau memulai ulang game.

78d43c7aa01b414d.png

@Composable
private fun FinalScoreDialog(
   onPlayAgain: () -> Unit,
   modifier: Modifier = Modifier
) {
   val activity = (LocalContext.current as Activity)

   AlertDialog(
       onDismissRequest = {
           // Dismiss the dialog when the user clicks outside the dialog or on the back
           // button. If you want to disable that functionality, simply use an empty
           // onDismissRequest.
       },
       title = { Text(stringResource(R.string.congratulations)) },
       text = { Text(stringResource(R.string.you_scored, 0)) },
       modifier = modifier,
       dismissButton = {
           TextButton(
               onClick = {
                   activity.finish()
               }
           ) {
               Text(text = stringResource(R.string.exit))
           }
       },
       confirmButton = {
           TextButton(
               onClick = {
                   onPlayAgain()
               }
           ) {
               Text(text = stringResource(R.string.play_again))
           }
       }
   )
}

Dalam fungsi ini, parameter title dan text menampilkan judul dan teks pendukung dalam dialog pemberitahuan. dismissButton dan confirmButton adalah tombol teks. Di parameter dismissButton, Anda menampilkan teks Exit dan menghentikan aplikasi dengan menyelesaikan aktivitas. Di parameter confirmButton, Anda memulai ulang game dan menampilkan teks Play Again.

a24f59b84a178d9b.png

  1. Di file GameScreen.kt, pada fungsi FinalScoreDialog(), perhatikan parameter skor untuk menampilkan skor game di dialog pemberitahuan.
@Composable
private fun FinalScoreDialog(
   score: Int,
   onPlayAgain: () -> Unit,
   modifier: Modifier = Modifier
) {
  1. Dalam fungsi FinalScoreDialog(), perhatikan penggunaan ekspresi lambda parameter text untuk menggunakan score sebagai argumen format ke teks dialog.
text = { Text(stringResource(R.string.you_scored, score)) }
  1. Di file GameScreen.kt, di akhir fungsi composable GameScreen(), setelah blok Column, tambahkan kondisi if untuk memeriksa gameUiState.isGameOver.
  2. Di blok if, tampilkan dialog pemberitahuan. Lakukan panggilan ke FinalScoreDialog() dengan meneruskan score dan gameViewModel.resetGame() untuk callback peristiwa onPlayAgain.
if (gameUiState.isGameOver) {
   FinalScoreDialog(
       score = gameUiState.score,
       onPlayAgain = { gameViewModel.resetGame() }
   )
}

resetGame() adalah callback peristiwa yang diteruskan dari GameScreen ke ViewModel.

  1. Dalam file GameViewModel.kt, panggil kembali fungsi resetGame(), lakukan inisialisasi _uiState, dan pilih kata baru.
fun resetGame() {
   usedWords.clear()
   _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
  1. Build dan jalankan aplikasi Anda.
  2. Mainkan game hingga selesai, dan amati dialog pemberitahuan dengan opsi untuk Exit dari game atau Play Again. Coba opsi yang ditampilkan di dialog pemberitahuan.

c6727347fe0db265.png

10. Status dalam rotasi perangkat

Pada codelab sebelumnya, Anda telah mempelajari perubahan konfigurasi di Android. Saat terjadi perubahan konfigurasi, Android akan memulai ulang aktivitas dari awal, menjalankan semua callback startup siklus proses.

ViewModel menyimpan data terkait aplikasi yang tidak dihancurkan saat framework Android menghancurkan dan membuat ulang aktivitas. Objek ViewModel secara otomatis dipertahankan dan tidak dihancurkan seperti instance aktivitas selama perubahan konfigurasi. Data yang disimpan segera tersedia setelah rekomposisi.

Dalam tugas ini, Anda akan memeriksa apakah aplikasi mempertahankan UI status selama perubahan konfigurasi.

  1. Jalankan aplikasi dan putar beberapa kata. Ubah konfigurasi perangkat dari potret ke lanskap, atau sebaliknya.
  2. Perhatikan bahwa data yang disimpan di UI status ViewModel dipertahankan selama perubahan konfigurasi.

4a63084643723724.png

4134470d435581dd.png

11. Mendapatkan kode solusi

Untuk mendownload kode codelab yang sudah selesai, Anda dapat menggunakan perintah git berikut:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git
$ cd basic-android-kotlin-compose-training-unscramble
$ git checkout viewmodel

Atau, Anda dapat mendownload repositori sebagai file ZIP, lalu mengekstraknya, dan membukanya di Android Studio.

Jika Anda ingin melihat kode solusi untuk codelab ini, lihat kode tersebut di GitHub.

12. Kesimpulan

Selamat! Anda telah menyelesaikan codelab. Kini Anda memahami bagaimana panduan arsitektur aplikasi Android merekomendasikan untuk memisahkan class yang memiliki tanggung jawab berbeda dan menjalankan UI dari model.

Jangan lupa untuk membagikan karya Anda di media sosial dengan #AndroidBasics.

Pelajari lebih lanjut