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.
  1. 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")
    //...
    }
  2. Di paket ui, buat file/class Kotlin bernama GameViewModel. Perluas dari class ViewModel.
    import androidx.lifecycle.ViewModel
    
    class GameViewModel : ViewModel() {
    }
  3. 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 = ""
    )
  4. 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.
  5. 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

  1. Di GameViewModel, tambahkan properti bernama currentWord dari jenis String untuk menyimpan kata acak saat ini.
    private lateinit var currentWord: String
  2. 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)
       }
    }
  3. 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()
  4. 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)
    }
  5. 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())
    }
  6. Tambahkan blok init ke GameViewModel dan panggil resetGame() dari blok tersebut.
    init {
       resetGame()
    }

Meneruskan ViewModel ke UI

  1. Pada fungsi GameScreen, teruskan argumen kedua dari jenis GameViewModel dengan nilai default viewModel().
  2. 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()
    ) {
       // ...
    }
  3. Teruskan gameUiState.currentScrambledWord ke composable GameLayout().
    GameLayout(
       currentScrambledWord = gameUiState.currentScrambledWord,
       modifier = Modifier
           .fillMaxWidth()
           .wrapContentHeight()
           .padding(mediumPadding)
    )
  4. Tambahkan currentScrambledWord sebagai parameter lain ke fungsi composable GameLayout().
    @Composable
    fun GameLayout(
       currentScrambledWord: String,
       modifier: Modifier = Modifier
    ) {
    }
  5. 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

  1. 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("")
    }
    
       
  2. 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
  1. 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))
    }
  2. Pada fungsi composable GameScreen(), update panggilan fungsi GameLayout() untuk meneruskan gameViewModel.checkUserGuess() dalam ekspresi lambda onKeyboardDone.
    GameLayout(
       // ...
       onKeyboardDone = { gameViewModel.checkUserGuess() }
    )
  3. 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,
               // ...
           )
    }
    }
  4. Pada fungsi composable GameScreen(), update panggilan fungsi GameLayout() untuk meneruskan isGuessWrong.
    GameLayout(
       isGuessWrong = gameUiState.isGuessedWordWrong,
       // ...
    )
  5. 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.
  1. Di GameUiState, tambahkan variabel score dan lakukan inisialisasi ke nol.
    data class GameUiState(
       // ...
       val score: Int = 0
    )
  2. Untuk memperbarui nilai skor, di GameViewModel, pada fungsi checkUserGuess(), di dalam kondisi if untuk saat tebakan pengguna benar, tingkatkan nilai score.
  3. 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 ) } }
  4. 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 {
           //...
       }
    }
  5. Tambahkan variabel lain untuk jumlah di GameUiState. Panggil currentWordCount dan lakukan inisialisasi ke 1.
    data class GameUiState(
       // ...
       val isGuessedWordWrong: Boolean = false,
    )
  6. 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

  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),
                   //..
               )
    
    // ...
    
    }
  2. Perbarui panggilan fungsi GameLayout() untuk menyertakan jumlah kata.
    GameLayout(
       userGuess = gameViewModel.userGuess,
       wordCount = gameUiState.currentWordCount,
       //...
    )
  3. 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))
  4. Di file GameScreen.kt, pada fungsi composable GameScreen(), lakukan panggilan ke gameViewModel.skipWord() dalam ekspresi lambda onClick.
    OutlinedButton(
       onClick = { gameViewModel.skipWord() },
       modifier = Modifier.fillMaxWidth()
    ) {
       //...
    }
  5. 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.
    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.
    3. Di dalam blok if, tambahkan flag Boolean isGameOver dan setel flag ke true untuk menunjukkan akhir game.
    4. 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
                 )
             }
         }
      }
    5. 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
    1. Penampung
    2. Ikon (opsional)
    3. Judul (opsional)
    4. Teks pendukung
    5. Pembagi (opsional)
    6. 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.
    Alur Pengiriman Callback onPlayAgain() ke ViewModel
    1. Di file GameScreen.kt, pada fungsi FinalScoreDialog(), tambahkan parameter skor untuk menampilkan skor game di dialog pemberitahuan.
      @Composable
      private fun FinalScoreDialog(
         score: Int,
         // ...
      ) {
    2. 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)) }
    3. Di file GameScreen.kt, di akhir fungsi composable GameScreen(), setelah blok Column, tambahkan kondisi if untuk memeriksa gameUiState.isGameOver..
    4. 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() }
         )
      }
    5. 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

          Postingan Populer