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
- Pengantar arsitektur aplikasi Android
- Cara menggunakan class
ViewModel
di aplikasi Anda - Cara mempertahankan data UI melalui perubahan konfigurasi perangkat menggunakan
ViewModel
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:
- Buka project dengan kode awal di Android Studio.
- Jalankan aplikasi di perangkat Android atau di emulator.
- 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
.
// 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.
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
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.
@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.
// 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.
Arsitektur aplikasi yang direkomendasikan
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.
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.
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.
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
.
- Buka
build.gradle.kts (Module :app)
, scroll ke blokdependencies
, lalu tambahkan dependensi berikut untukViewModel
. 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")
//...
}
- Di paket
ui
, buat class/file Kotlin bernamaGameViewModel
. Perluas dari classViewModel
.
import androidx.lifecycle.ViewModel
class GameViewModel : ViewModel() {
}
- Dalam paket
ui
, tambahkan class model untuk UI status yang disebutGameUiState
. 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
bersifatprivate
dan dapat diubah. Oleh karena itu, ini hanya dapat diakses dan diedit dalam classViewModel
.
Di luar class ViewModel
:
- Pengubah visibilitas default di Kotlin adalah
public
, sehinggacount
bersifat publik dan dapat diakses dari class lain seperti pengontrol UI. Jenisval
tidak boleh memiliki penyetel. Metode ini tidak dapat diubah dan bersifat hanya-baca sehingga Anda hanya dapat mengganti metodeget()
. Saat class luar mengakses properti ini, properti akan menampilkan nilai_count
dan nilainya tidak dapat diubah. Properti pendukung ini melindungi data aplikasi di dalamViewModel
dari perubahan yang tidak diinginkan dan tidak aman oleh class eksternal, tetapi memungkinkan pemanggil eksternal mengakses nilainya dengan aman.
- Di file
GameViewModel.kt
, tambahkan properti pendukung keuiState
yang bernama_uiState
. Beri nama propertiuiState
dan berjenisStateFlow<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>
- 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.
- Di
GameViewModel
, tambahkan properti bernamacurrentWord
dari jenisString
untuk menyimpan kata acak saat ini.
private lateinit var currentWord: String
- Tambahkan metode bantuan untuk pilih kata acak dari daftar dan acaklah. Beri nama
pickRandomWordAndShuffle()
tanpa parameter input, lalu buat fungsi tersebut menampilkanString
.
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.
- Di
GameViewModel
, tambahkan properti berikut setelah properticurrentWord
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()
- Tambahkan metode helper lain untuk mengacak kata saat ini yang disebut
shuffleCurrentWord()
yang menggunakanString
dan menampilkanString
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)
}
- 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 kumpulanusedWords
, lakukan inisialisasi_uiState
. Pilih kata baru untukcurrentScrambledWord
menggunakanpickRandomWordAndShuffle()
.
fun resetGame() {
usedWords.clear()
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
- Tambahkan blok
init
keGameViewModel
dan panggilresetGame()
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.
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
.
- Pada fungsi
GameScreen
, teruskan argumen kedua dari jenisGameViewModel
dengan nilai defaultviewModel()
.
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun GameScreen(
gameViewModel: GameViewModel = viewModel()
) {
// ...
}
- Pada fungsi
GameScreen()
, tambahkan variabel baru bernamagameUiState
. Gunakan delegasiby
dan panggilcollectAsState()
padauiState
.
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()
// ...
}
- Teruskan
gameUiState.currentScrambledWord
ke composableGameLayout()
. Anda menambahkan argumen di langkah selanjutnya, jadi abaikan error untuk saat ini.
GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(mediumPadding)
)
- Tambahkan
currentScrambledWord
sebagai parameter lain ke fungsi composableGameLayout()
.
@Composable
fun GameLayout(
currentScrambledWord: String,
modifier: Modifier = Modifier
) {
}
- Perbarui fungsi composable
GameLayout()
untuk menampilkancurrentScrambledWord
. Tetapkan parametertext
kolom teks pertama di kolom kecurrentScrambledWord
.
@Composable
fun GameLayout(
// ...
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
text = currentScrambledWord,
fontSize = 45.sp,
modifier = modifier.align(Alignment.CenterHorizontally)
)
//...
}
}
- Jalankan dan bangun aplikasi. Anda akan melihat kata yang ejaannya diacak.
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
.
- Di file
GameScreen.kt
, dalam composableGameLayout()
, setelonValueChange
keonUserGuessChanged
danonKeyboardDone()
ke tindakan keyboardonDone
. 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() }
),
- Pada fungsi composable
GameLayout()
, tambahkan dua argumen lagi: lambdaonUserGuessChanged
menggunakan argumenString
dan tidak menampilkan apa pun, sertaonKeyboardDone
tidak mengambil apa pun dan tidak menampilkan apa pun.
@Composable
fun GameLayout(
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
currentScrambledWord: String,
modifier: Modifier = Modifier,
) {
}
- Pada panggilan fungsi
GameLayout()
, tambahkan argumen lambda untukonUserGuessChanged
danonKeyboardDone
.
GameLayout(
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { },
currentScrambledWord = gameUiState.currentScrambledWord,
)
Anda segera menentukan metode updateUserGuess
di GameViewModel
.
- Di file
GameViewModel.kt
, tambahkan metode bernamaupdateUserGuess()
yang menggunakan argumenString
, kata tebakan pengguna. Di dalam fungsi, perbaruiuserGuess
dengan meneruskanguessedWord
.
fun updateUserGuess(guessedWord: String){
userGuess = guessedWord
}
Kemudian, Anda menambahkan userGuess
di ViewModel.
- Di file
GameViewModel.kt
, tambahkan properti var yang disebutuserGuess
. GunakanmutableStateOf()
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
- Di file
GameScreen.kt
, di dalamGameLayout()
, tambahkan parameterString
lain untukuserGuess
. Tetapkan parametervalue
dariOutlinedTextField
keuserGuess
.
fun GameLayout(
currentScrambledWord: String,
userGuess: String,
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
//...
OutlinedTextField(
value = userGuess,
//..
)
}
}
- Pada fungsi
GameScreen
, update panggilan fungsiGameLayout()
untuk menyertakan parameteruserGuess
.
GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
userGuess = gameViewModel.userGuess,
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { },
//...
)
- Bangun dan jalankan aplikasi Anda.
- Coba menebak dan masukkan kata. Kolom teks dapat menampilkan tebakan pengguna.
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.
- Di
GameViewModel
, tambahkan metode lain bernamacheckUserGuess()
. - Pada fungsi
checkUserGuess()
, tambahkan blokif else
untuk memverifikasi apakah tebakan pengguna sama dengancurrentWord
. ResetuserGuess
ke string yang kosong.
fun checkUserGuess() {
if (userGuess.equals(currentWord, ignoreCase = true)) {
} else {
}
// Reset user guess
updateUserGuess("")
}
- Jika tebakan pengguna salah, tetapkan
isGuessedWordWrong
ketrue
.MutableStateFlow<T>.
update()
memperbaruiMutableStateFlow.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)
}
}
- Di class
GameUiState
, tambahkanBoolean
yang disebutisGuessedWordWrong
dan lakukan inisialisasi kefalse
.
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.
- Di file
GameScreen.kt
, di akhir fungsi composableGameScreen()
, panggilgameViewModel.checkUserGuess()
di dalam ekspresi lambdaonClick
dari tombol Submit.
Button(
modifier = modifier
.fillMaxWidth()
.weight(1f)
.padding(start = 8.dp),
onClick = { gameViewModel.checkUserGuess() }
) {
Text(stringResource(R.string.submit))
}
- Pada fungsi composable
GameScreen()
, update panggilan fungsiGameLayout()
untuk meneruskangameViewModel.checkUserGuess()
dalam ekspresi lambdaonKeyboardDone
.
GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
userGuess = gameViewModel.userGuess,
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { gameViewModel.checkUserGuess() }
)
- Pada fungsi composable
GameLayout()
, tambahkan parameter fungsi untukBoolean
,isGuessWrong
. Tetapkan parameterisError
dariOutlinedTextField
keisGuessWrong
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() }
),
)
}
}
- Pada fungsi composable
GameScreen()
, update panggilan fungsiGameLayout()
untuk meneruskanisGuessWrong
.
GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
userGuess = gameViewModel.userGuess,
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { gameViewModel.checkUserGuess() },
isGuessWrong = gameUiState.isGuessedWordWrong,
// ...
)
- Build dan jalankan aplikasi Anda.
- Masukkan tebakan yang salah dan klik Submit. Perhatikan bahwa kolom teks berubah menjadi merah, yang menunjukkan error.
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.
- Di file
GameScreen.kt
, dalam composableGameLayout()
, perbarui parameter label kolom teks bergantung padaisGuessWrong
sebagai berikut:
OutlinedTextField(
// ...
label = {
if (isGuessWrong) {
Text(stringResource(R.string.wrong_guess))
} else {
Text(stringResource(R.string.enter_your_word))
}
},
// ...
)
- Di file
strings.xml
, tambahkan string ke label error.
<string name="wrong_guess">Wrong Guess!</string>
- Build dan jalankan aplikasi kembali.
- Masukkan tebakan yang salah dan klik Submit. Perhatikan label error.
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
.
- Di
GameUiState
, tambahkan variabelscore
dan lakukan inisialisasi ke nol.
data class GameUiState(
val currentScrambledWord: String = "",
val isGuessedWordWrong: Boolean = false,
val score: Int = 0
)
- Untuk memperbarui nilai skor, di
GameViewModel
, pada fungsicheckUserGuess()
, di dalam kondisiif
untuk saat tebakan pengguna benar, tingkatkan nilaiscore
.
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 {
//...
}
}
- Di
GameViewModel
, tambahkan metode lain bernamaupdateGameState
untuk memperbarui skor, menambah jumlah kata saat ini, dan memilih kata baru dari fileWordsData.kt
. TambahkanInt
yang bernamaupdatedScore
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
)
}
}
- Pada fungsi
checkUserGuess()
, jika tebakan pengguna benar, lakukan panggilan keupdateGameState
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.
- Tambahkan variabel lain untuk jumlah di
GameUiState
. PanggilcurrentWordCount
dan lakukan inisialisasi ke1
.
data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 1,
val score: Int = 0,
val isGuessedWordWrong: Boolean = false,
)
- Di file
GameViewModel.kt
, pada fungsiupdateGameState()
, tingkatkan jumlah kata seperti yang ditunjukkan di bawah. FungsiupdateGameState()
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
.
- Di file
GameScreen.kt
, pada fungsi composableGameLayout()
, tambahkan jumlah kata sebagai argumen dan teruskan argumen formatwordCount
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
)
// ...
}
- Perbarui panggilan fungsi
GameLayout()
untuk menyertakan jumlah kata.
GameLayout(
userGuess = gameViewModel.userGuess,
wordCount = gameUiState.currentWordCount,
//...
)
- Pada fungsi composable
GameScreen()
, perbarui panggilan fungsiGameStatus()
untuk menyertakan parameterscore
. Teruskan skor darigameUiState
.
GameStatus(score = gameUiState.score, modifier = Modifier.padding(20.dp))
- Bangun dan jalankan aplikasi.
- Masukkan kata tebakan, lalu klik Submit. Perhatikan skor dan jumlah kata yang diperbarui.
- Klik Skip, dan perhatikan bahwa tidak ada yang terjadi.
Untuk mengimplementasikan fungsi lewati, Anda harus meneruskan callback peristiwa lewati ke GameViewModel
.
- Di file
GameScreen.kt
, pada fungsi composableGameScreen()
, lakukan panggilan kegameViewModel.skipWord()
dalam ekspresi lambdaonClick
.
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()
) {
//...
}
- Di
GameViewModel
, tambahkan metodeskipWord()
. - Di dalam fungsi
skipWord()
, lakukan panggilan keupdateGameState()
, dengan meneruskan skor dan mereset tebakan pengguna.
fun skipWord() {
updateGameState(_uiState.value.score)
// Reset user guess
updateUserGuess("")
}
- Jalankan aplikasi Anda dan mainkan game-nya. Sekarang Anda akan dapat melewati kata.
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.
Untuk mengimplementasikan logika akhir game, Anda harus memeriksa terlebih dahulu apakah pengguna telah mencapai jumlah kata maksimum.
- Di
GameViewModel
, tambahkan blokif-else
dan pindahkan isi fungsi yang ada ke dalam blokelse
. - Tambahkan kondisi
if
untuk memastikan ukuranusedWords
sama denganMAX_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
)
}
}
}
- Di dalam blok
if
, tambahkan flagBoolean
isGameOver
dan setel flag ketrue
untuk menunjukkan akhir game. - Perbarui
score
dan resetisGuessedWordWrong
di dalam blokif
. 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
)
}
}
}
- Di
GameUiState
, tambahkan variabelBoolean
isGameOver
dan tetapkan kefalse
.
data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 1,
val score: Int = 0,
val isGuessedWordWrong: Boolean = false,
val isGameOver: Boolean = false
)
- Jalankan aplikasi Anda dan mainkan game-nya. Anda tidak dapat memainkan lebih dari 10 kata.
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
- Penampung
- Ikon (opsional)
- Judul (opsional)
- Teks pendukung
- Pembagi (opsional)
- Tindakan
File GameScreen.kt
dalam kode awal sudah menyediakan fungsi yang menampilkan dialog pemberitahuan dengan opsi untuk keluar atau memulai ulang game.
@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.
- Di file
GameScreen.kt
, pada fungsiFinalScoreDialog()
, perhatikan parameter skor untuk menampilkan skor game di dialog pemberitahuan.
@Composable
private fun FinalScoreDialog(
score: Int,
onPlayAgain: () -> Unit,
modifier: Modifier = Modifier
) {
- Dalam fungsi
FinalScoreDialog()
, perhatikan penggunaan ekspresi lambda parametertext
untuk menggunakanscore
sebagai argumen format ke teks dialog.
text = { Text(stringResource(R.string.you_scored, score)) }
- Di file
GameScreen.kt
, di akhir fungsi composableGameScreen()
, setelah blokColumn
, tambahkan kondisiif
untuk memeriksagameUiState.isGameOver
. - Di blok
if
, tampilkan dialog pemberitahuan. Lakukan panggilan keFinalScoreDialog()
dengan meneruskanscore
dangameViewModel.resetGame()
untuk callback peristiwaonPlayAgain
.
if (gameUiState.isGameOver) {
FinalScoreDialog(
score = gameUiState.score,
onPlayAgain = { gameViewModel.resetGame() }
)
}
resetGame()
adalah callback peristiwa yang diteruskan dari GameScreen
ke ViewModel
.
- Dalam file
GameViewModel.kt
, panggil kembali fungsiresetGame()
, lakukan inisialisasi_uiState
, dan pilih kata baru.
fun resetGame() {
usedWords.clear()
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
- Build dan jalankan aplikasi Anda.
- Mainkan game hingga selesai, dan amati dialog pemberitahuan dengan opsi untuk Exit dari game atau Play Again. Coba opsi yang ditampilkan di dialog pemberitahuan.
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.
- Jalankan aplikasi dan putar beberapa kata. Ubah konfigurasi perangkat dari potret ke lanskap, atau sebaliknya.
- Perhatikan bahwa data yang disimpan di UI status
ViewModel
dipertahankan selama perubahan konfigurasi.
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.