Tugas 11: ViewModel & State dalam Compose dengan Aplikasi Unscramble
Nama : Helsa Nesta Dhaifullah
NRP : 5025201005
Kelas : PPB I - 2024
Apa itu ViewModel?
ViewModel adalah salah satu komponen arsitektur dari pustaka Android
Jetpack yang dapat menyimpan data aplikasi. Data yang disimpan oleh ViewModel
tidak akan hilang jika aplikasi mengalami perubahan konfigurasi, seperti
rotasi layar. Namun, jika aplikasi benar-benar dimatikan dan dijalankan ulang,
data akan hilang. ViewModel dirancang untuk menyimpan data hanya selama
aplikasi aktif atau saat perubahan cepat terjadi.
Gambar Arsitektur Android dengan ViewModel |
Gambar tersebut menunjukkan arsitektur aplikasi Android dengan dua lapisan
utama: Data Layer dan UI Layer. Data Layer menyimpan dan
mengelola data aplikasi, sedangkan UI Layer terdiri dari ViewModel dan
elemen-elemen UI.
ViewModel di dalam UI Layer bertugas mengelola status UI dan
mengambil data dari Data Layer. Ini memastikan data tetap ada meskipun
terjadi perubahan konfigurasi. Elemen UI berinteraksi dengan pengguna dan
mengirimkan event ke ViewModel. ViewModel memproses event ini
dan memperbarui status UI, yang kemudian ditampilkan kembali
oleh elemen UI. Arsitektur ini memisahkan logika bisnis dari logika UI,
membuat aplikasi lebih terstruktur dan mudah dikelola.
Mengenal Aplikasi Unscramble
Unscramble adalah permainan ejaan kata untuk satu pemain. Pemain menebak kata
dari huruf-huruf acak yang ditampilkan. Poin diberikan untuk tebakan yang
benar, dan pemain bisa mencoba lagi jika salah. Ada opsi untuk melewati kata.
Di pojok kanan atas, ditampilkan jumlah kata yang telah dimainkan. Setiap
permainan terdiri dari 10 kata acak.
Pratinjau Aplikasi Unscramble |
Dari aplikasi ini, kita akan berlatih untuk menerapkan ViewModel ke dalam
aplikasi. Kode awal dari aplikasi Unscramble dapat diunduh di link Github
(cabang starter) berikut.
Menambahkan ViewModel
Setelah kalian berhasil melakukan import project Aplikasi Unscramble di
Android Studio, tugas pertama adalah menambahkan ViewModel ke aplikasi
untuk menyimpan status UI game (kata acak, jumlah kata, dan skor).
Untuk mengatasi masalah pada kode awal yang kalian lihat di bagian sebelumnya,
kalian perlu menyimpan data game di ViewModel.
-
Buka build.gradle.kts (Module :app), gulir ke blok
dependencies, dan tambahkan dependensi berikut untuk
ViewModel guna menambahkan viewmodel berbasis siklus proses ke
aplikasi Compose kalian.
dependencies { // other dependencies implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") //... }
-
Di paket ui, buat file/class Kotlin bernama GameViewModel.
Perluas dari class ViewModel.
import androidx.lifecycle.ViewModel class GameViewModel : ViewModel() { }
-
Di paket ui, buat juga 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 = "" )
-
Di class GameViewModel, tambahkan properti _uiState berikut.
import kotlinx.coroutines.flow.MutableStateFlow // Game UI state private val _uiState = MutableStateFlow(GameUiState())
StateFlow adalah sebuah aliran data yang dapat diamati, yang menampilkan pembaruan status saat ini dan yang baru. Properti value menunjukkan nilai status yang sedang aktif. Untuk memperbarui status dan mengirimkannya ke aliran, cukup atur nilai baru ke properti value dari class MutableStateFlow. -
Di file GameViewModel.kt, tambahkan properti pendukung ke uiState yang bernama _uiState. Beri nama properti uiState dan berjenis StateFlow<GameUiState>. Kemudian setel uiState ke _uiState.asStateFlow().
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> = _uiState.asStateFlow()
Sekarang, _uiState hanya bisa diakses dan diubah di dalam GameViewModel. UI dapat membaca nilainya melalui properti hanya baca uiState. Fungsi asStateFlow() mengubah aliran status yang dapat diubah ini menjadi aliran status hanya baca.
Menampilkan kata acak tanpa pola
-
Di GameViewModel, tambahkan properti bernama
currentWord dari jenis String untuk menyimpan kata acak
saat ini.
private lateinit var currentWord: String
-
Tambahkan fungsi untuk pilih kata acak dari WordsData.kt dan
acaklah. Beri nama pickRandomWordAndShuffle() tanpa parameter
input dengan kembalian 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) } }
-
Di GameViewModel, tambahkan properti usedWords setelah
properti sebelumnya. Berfungsi sebagai kumpulan dari kata-kata yang
telah digunakan dalam game.
private var usedWords: MutableSet<String> = mutableSetOf()
-
Tambahkan fungsi untuk mengacak kata saat ini yang disebut
shuffleCurrentWord() dengan parameter String dan
mengembalikan 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) }
-
Tambahkan juga fungsi resetGame() untuk menghapus semua kata
dalam kumpulan usedWords, menginisialisasi _uiState, dan memilih
kata baru untuk currentScrambledWord menggunakan
pickRandomWordAndShuffle().
fun resetGame() { usedWords.clear() _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle()) }
-
Tambahkan blok init ke GameViewModel dan panggil
resetGame() dari blok tersebut.
init { resetGame() }
Meneruskan ViewModel ke UI
- Pada fungsi GameScreen, teruskan argumen kedua dari jenis GameViewModel dengan nilai default viewModel().
-
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.
@Composable fun GameScreen( gameViewModel: GameViewModel = viewModel() ) { // ... }
-
Teruskan gameUiState.currentScrambledWord ke composable
GameLayout().
GameLayout( currentScrambledWord = gameUiState.currentScrambledWord, modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(mediumPadding) )
-
Tambahkan currentScrambledWord sebagai parameter lain ke fungsi
composable GameLayout().
@Composable fun GameLayout( currentScrambledWord: String, modifier: Modifier = Modifier ) { }
-
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, //... } }
Verifikasi Kata Tebakan dan Update Skor
-
Tambahkan checkUserGuess() di GameViewModel. Periksa
apakah tebakan pengguna sama dengan currentWord. Jika ya,
reset userGuess menjadi string kosong. Jika tidak, set
isGuessedWordWrong menjadi true dan perbarui
MutableStateFlow.value.
import kotlinx.coroutines.flow.update fun checkUserGuess() { if (userGuess.equals(currentWord, ignoreCase = true)) { } else { // User's guess is wrong, show an error _uiState.update { currentState -> currentState.copy(isGuessedWordWrong = true) } } // Reset user guess updateUserGuess("") }
-
Di class GameUiState, tambahkan Boolean yang disebut
isGuessedWordWrong dan lakukan inisialisasi ke
false.
data class GameUiState( val currentScrambledWord: String = "", val isGuessedWordWrong: Boolean = false, )
Teruskan callback checkUserGuess() dari GameScreen ke
ViewModel saat pengguna klik tombol Submit atau tombol
selesai di keyboard. Teruskan data
gameUiState.isGuessedWordWrong dari ViewModel ke
GameScreen untuk menetapkan error di kolom teks.
Alur Pengiriman callback dari GameScreen ke ViewModel |
-
Di file GameScreen.kt, di akhir fungsi composable
GameScreen(), panggil
gameViewModel.checkUserGuess() di dalam ekspresi lambda
onClick dari tombol Submit.
Button( // ... onClick = { gameViewModel.checkUserGuess() } ) { Text(stringResource(R.string.submit)) }
-
Pada fungsi composable GameScreen(), update panggilan fungsi
GameLayout() untuk meneruskan
gameViewModel.checkUserGuess() dalam ekspresi lambda
onKeyboardDone.
GameLayout( // ... onKeyboardDone = { gameViewModel.checkUserGuess() } )
-
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, // ... ) { Column( // ,... OutlinedTextField( // ... isError = isGuessWrong, // ... ) } }
-
Pada fungsi composable GameScreen(), update panggilan fungsi
GameLayout() untuk meneruskan isGuessWrong.
GameLayout( isGuessWrong = gameUiState.isGuessedWordWrong, // ... )
-
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)) } }, // ... )
Memperbarui Skor dan Jumlah Kata
Dalam tugas ini, kalian akan memperbarui skor dan jumlah kata saat
pengguna memainkan game. Skor harus bagian dari _ uiState.
-
Di GameUiState, tambahkan variabel score dan lakukan
inisialisasi ke nol.
data class GameUiState( // ... val score: Int = 0 )
- Untuk memperbarui nilai skor, di GameViewModel, pada fungsi checkUserGuess(), di dalam kondisi if untuk saat tebakan pengguna benar, tingkatkan nilai score.
- Di GameViewModel, tambahkan fungsi updateGameState untuk memperbarui skor, menambah jumlah kata saat ini, dan memilih kata baru dari file WordsData.kt. Gunakan parameter updatedScore bertipe Int. Kemudian, perbarui variabel status UI 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 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 { //... } }
-
Tambahkan variabel lain untuk jumlah di GameUiState. Panggil
currentWordCount dan lakukan inisialisasi ke 1.
data class GameUiState( // ... val isGuessedWordWrong: Boolean = false, )
- 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 & Jumlah Kata
-
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), //.. ) // ... }
-
Perbarui panggilan fungsi GameLayout() untuk menyertakan
jumlah kata.
GameLayout( userGuess = gameViewModel.userGuess, wordCount = gameUiState.currentWordCount, //... )
- 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))
- Di file GameScreen.kt, pada fungsi composable GameScreen(), lakukan panggilan ke gameViewModel.skipWord() dalam ekspresi lambda onClick.
OutlinedButton( onClick = { gameViewModel.skipWord() }, modifier = Modifier.fillMaxWidth() ) { //... }
- Di GameViewModel, tambahkan metode skipWord(). Di dalamnya lakukan panggilan ke updateGameState(), dengan meneruskan skor dan mereset tebakan pengguna.
fun skipWord() { updateGameState(_uiState.value.score) // Reset user guess updateUserGuess("") }
Handle Putaran Terakhir Game
Untuk mengimplementasikan logika akhir game, kalian harus memeriksa
terlebih dahulu apakah pengguna telah mencapai jumlah kata maksimum.
- Di GameViewModel, tambahkan blok if-else dan pindahkan isi fungsi yang ada ke dalam blok else.
- Tambahkan kondisi if untuk memastikan ukuran usedWords sama dengan MAX_NO_OF_WORDS.
- Di dalam blok if, tambahkan flag Boolean isGameOver dan setel flag ke true untuk menunjukkan akhir game.
-
Perbarui score dan reset isGuessedWordWrong di dalam
blok if. Kode berikut menunjukkan tampilan fungsi kalian:
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 variabel
Boolean isGameOver dan tetapkan ke false.
data class GameUiState( // ... val isGameOver: Boolean = false )
Menampilkan Dialog Akhir Game
Saat game berakhir, sebaiknya beri tahu pengguna dan tanyakan apakah
mereka ingin bermain lagi. Untuk lebih paham isi/anatomi dari dialog
pemberitahuan adalah sebagai berikut.
Struktur Dialog Pemberitahuan |
- Penampung
- Ikon (opsional)
- Judul (opsional)
- Teks pendukung
- Pembagi (opsional)
- Tindakan
Lakukan sedikit perubahan pada fungsi yang telah disediakan untuk
menampilkan dialog pemberitahuan di file GameScreen.kt agar bisa meneruskan state maupun event dengan
GameViewModel.
- Di file GameScreen.kt, pada fungsi FinalScoreDialog(), tambahkan parameter skor untuk menampilkan skor game di dialog pemberitahuan.
@Composable private fun FinalScoreDialog( score: Int, // ... ) {
-
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)) }
- Di file GameScreen.kt, di akhir fungsi composable GameScreen(), setelah blok Column, tambahkan kondisi if untuk memeriksa gameUiState.isGameOver..
- Di blok if, tampilkan dialog pemberitahuan. Lakukan panggilan ke FinalScoreDialog() dengan meneruskan score dan gameViewModel.resetGame() untuk callback event onPlayAgain.
if (gameUiState.isGameOver) { FinalScoreDialog( score = gameUiState.score, onPlayAgain = { gameViewModel.resetGame() } ) }
- Dalam file GameViewModel.kt, panggil kembali fungsi resetGame(), lakukan inisialisasi _uiState, dan pilih kata baru. fungsi resetGame() adalah callback event yang diteruskan dari GameScreen ke ViewModel.
fun resetGame() { usedWords.clear() _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle()) }
Kode Solusi
Kode lengkap dari tutorial latihan penerapan ViewModel & State dalam Compose dengan Aplikasi Unscramble dapat kalian akses di Github saya. Link nya sebagai berikut.
Komentar
Posting Komentar