Tugas 11 PPB
TUGAS 11 PPB
UNSCRAMBLE APPLICATION
Nama: Muhammad Lintang Panjerino
NRP: 5025201045
Kelas: PPB I
Tahun: 2024
Link Github: Tugas 11 - Unscramble Application (Viewmodel)
Pada Tugas 11 PPB kali ini, diberikan tugas untuk membuat proyek Unscramble Application (Viewmodel) dengan menggunakan Jetpack Compose pada Android Studio. Secara garis besar, proyek Unscramble Application ini adalah sebuah aplikasi game yang menampilkan kata acak dan kemudian tujuan game ini adalah menebak kata yang benar dan mendapatkan skor setiap kali berhasil menebak kata acak. Aplikasi ini menampilkan kata acak, nomor soal, dan jumlah skor yang diperoleh. Pada tiap nomor soal, terdapat kata acak yang harus ditebak, dan ada 2 pilihan, yaitu "Submit" untuk menjawab dan "Skip" untuk melewati soal. Ketika benar menajwab, skor akan bertambah, ketika melewati soal, skor akan tetap, dan pada akhir permainan akan ditampilkan skor total dari pemain. Proyek ini bertujuan untuk mempelajari lebih dalam tentang konsep Viewmodel, state, UI Layer, dan Data Layer pada Jetpack Compose.
Untuk memulai pengerjaan proyek, langkah awal yang dilakukan adalah mengunduh file zip dari repository github berikut https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble dan pilih branch dengan nama "starter". Kemudian ekstrak file zip tersebut pada folder lokal PC/laptop dan ubah nama folder hasil ekstrak menjadi "Unscramble Application". Pada Android Studio, pilih "File" > pilih "Open" > pilih file tempat folder projek diekstrak > pilih "OK".
Setelah folder proyek terbuka, masih ada proses gradle build. Proses ini akan memakan waktu beberapa menit. Berikut adalah tampilan awal pada file MainActivity.kt. File ini merupakan file utama dari aplikasi Unscramble Application. Setelah terbuka, klik "Run app" untuk langsung melihat tampilan awal aplikasi.
Aplikasi ini terbagi menjadi 6 bagian besar, yaitu .
1. Menambahkan ViewModel
Menambahkan dependency ViewModel
a. Buka build.gradle.kts (Module :app), scroll ke blok dependencies, lalu tambahkan dependensi untuk ViewModel -> implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"). Dependensi ini digunakan untuk menambahkan viewmodel berbasis siklus proses ke aplikasi.
b. Di package ui, buat class/file Kotlin bernama GameViewModel. Perluas dari class ViewModel
c. Dalam package ui, tambahkan class model untuk UI status dengan nama GameUiState. Jadikan class data dan tambahkan variabel untuk kata acak saat ini.
Stateflow dan properti pendukung
a. Di class GameViewModel, tambahkan properti _uiState
b. Di file GameViewModel.kt, tambahkan properti pendukung ke uiState yang bernama _uiState. Beri nama properti uiState dan berjenis StateFlow<GameUiState>
c. Setel uiState ke _uiState.asStateFlow()
Menampilkan kata acak tanpa pola
a. Di GameViewModel, tambahkan properti bernama currentWord dari jenis String untuk menyimpan kata acak saat ini
b. Tambahkan method helper untuk pilih kata acak dari daftar dan acaklah. Beri nama pickRandomWordAndShuffle() tanpa parameter input, lalu buat fungsi tersebut menampilkan String
c. Di GameViewModel, tambahkan properti berikut setelah properti currentWord agar berfungsi sebagai kumpulan yang dapat diubah untuk menyimpan kata yang telah digunakan dalam game
d. Tambahkan method helper lain untuk mengacak kata saat ini yang disebut shuffleCurrentWord() yang menggunakan String dan menampilkan String yang diacak
e. Tambahkan fungsi helper 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()
f. Tambahkan blok init ke GameViewModel dan panggil resetGame() dari blok tersebut
2. Merancang UI Compose
Meneruskan data
a. Pada fungsi GameScreen, teruskan argumen kedua dari jenis GameViewModel dengan nilai default viewModel()
b. Pada fungsi GameScreen(), tambahkan variabel baru bernama gameUiState. Gunakan delegasi by dan panggil collectAsState() pada uiState
c. Teruskan gameUiState.currentScrambledWord ke composable GameLayout(). Abaikan error untuk saat ini karena akan ditambahkan argumen di langkah selanjutnya
d. Tambahkan currentScrambledWord sebagai parameter lain ke fungsi composable GameLayout()
e. Perbarui fungsi composable GameLayout() untuk menampilkan currentScrambledWord. Tetapkan parameter text kolom teks pertama di kolom ke currentScrambledWord
f. Lakukan 'Run app' dan sekarang akan terlihat kata yang ejaannya diacak
Menampilkan kata tebakan
a. Di file GameScreen.kt, dalam composable GameLayout(), setel onValueChange ke onUserGuessChanged dan onKeyboardDone() ke tindakan keyboard onDone
b. 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
c. Pada panggilan fungsi GameLayout(), tambahkan argumen lambda untuk onUserGuessChanged dan onKeyboardDone
d. Di file GameViewModel.kt, tambahkan method bernama updateUserGuess() yang menggunakan argumen String, kata tebakan pengguna. Di dalam fungsi, perbarui userGuess dengan meneruskan guessedWord
e. Di file GameViewModel.kt, tambahkan properti var yang disebut userGuess. Gunakan mutableStateOf() agar Compose mengamati nilai ini dan menetapkan nilai awal ke ""
f. Di file GameScreen.kt, di dalam GameLayout(), tambahkan parameter String lain untuk userGuess. Tetapkan parameter value dari OutlinedTextField ke userGuess
g. Pada fungsi GameScreen, update panggilan fungsi GameLayout() untuk menyertakan parameter userGuess
h. Lakukan 'Run app', ketika user menebak dan masukkan kata, kolom teks dapat menampilkan tebakan pengguna
3. Verifikasi Kata Tebakan
Memverifikasi kata
a. Di class GameViewModel, tambahkan method lain bernama checkUserGuess()
b. Pada fungsi checkUserGuess(), tambahkan blok if-else untuk memverifikasi apakah tebakan user sama dengan currentWord. Reset userGuess ke string yang kosong
c. Jika tebakan user salah, tetapkan isGuessedWordWrong ke true. MutableStateFlow<T>. update() memperbarui MutableStateFlow.value menggunakan nilai yang ditentukan
d. Di class GameUiState, tambahkan Boolean yang disebut isGuessedWordWrong dan lakukan inisialisasi ke false
Error handling
a. Di file GameScreen.kt, di akhir fungsi composable GameScreen(), panggil gameViewModel.checkUserGuess() di dalam ekspresi lambda onClick dari tombol Submit
b. Pada fungsi composable GameScreen(), update panggilan fungsi GameLayout() untuk meneruskan gameViewModel.checkUserGuess() dalam ekspresi lambda onKeyboardDone
c. 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
d. Pada fungsi composable GameScreen(), update panggilan fungsi GameLayout() untuk meneruskan isGuessWrong
e. Lakukan 'Run app', masukkan tebakan yang salah dan klik Submit. Perhatikan bahwa kolom teks berubah menjadi merah, yang menunjukkan error
f. Di file GameScreen.kt, dalam composable GameLayout(), perbarui parameter label kolom teks bergantung pada isGuessWrong
g. Di file strings.xml, tambahkan string ke label error
h. Lakukan 'Run app', masukkan tebakan yang salah dan klik Submit. Label error sudah menyesuaikan jika user salah menebak kata
4. Memperbarui Skor dan Jumlah Kata
Memperbarui skor
a. Di GameUiState, tambahkan variabel score dan lakukan inisialisasi ke nol
b. Untuk memperbarui nilai skor, di GameViewModel, pada fungsi checkUserGuess(), di dalam kondisi if untuk saat tebakan pengguna benar, tingkatkan nilai score
c. Di GameViewModel, tambahkan method 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
d. Pada fungsi checkUserGuess(), jika tebakan pengguna benar, lakukan panggilan ke updateGameState dengan skor yang telah diperbarui untuk menyiapkan game untuk putaran berikutnya
e. Tambahkan variabel lain untuk jumlah di GameUiState. Panggil currentWordCount dan lakukan inisialisasi ke 1
f. Di file GameViewModel.kt, pada fungsi updateGameState(), tingkatkan jumlah kata. Fungsi updateGameState() dipanggil untuk menyiapkan game untuk putaran berikutnya
Melewati soal
a. Di file GameScreen.kt, pada fungsi composable GameLayout(), tambahkan jumlah kata sebagai argumen dan teruskan argumen format wordCount ke elemen teks
b. Perbarui panggilan fungsi GameLayout() untuk menyertakan jumlah kata
c. Pada fungsi composable GameScreen(), perbarui panggilan fungsi GameStatus() untuk menyertakan parameter score. Teruskan skor dari gameUiState
d. Di file GameScreen.kt, pada fungsi composable GameScreen(), lakukan panggilan ke gameViewModel.skipWord() dalam ekspresi lambda onClick
e. Di GameViewModel, tambahkan method skipWord()
f. Di dalam fungsi skipWord(), lakukan panggilan ke updateGameState(), dengan meneruskan skor dan mereset tebakan pengguna
g. Lakukan 'Run app', game sudah dapat memperbarui skor dan jumlah kata, serta dapat melewati soal (skip)
5. Menangani Putaran Terakhir Game
Menentukan putaran terakhir game
a. Di GameViewModel, tambahkan blok if-else dan pindahkan isi fungsi yang ada sebelumnya ke dalam blok else
b. Tambahkan kondisi if untuk memastikan ukuran usedWords sama dengan MAX_NO_OF_WORDS
c. Di dalam blok if, tambahkan flag Boolean isGameOver dan setel flag ke true untuk menunjukkan akhir game
d. Perbarui score dan reset isGuessedWordWrong di dalam blok if
e. Di GameUiState, tambahkan variabel Boolean isGameOver dan tetapkan ke false
f. Lakukan 'Run app', sekarang game tidak bisa dimainkan lebih dari 10 kata
Menampilkan dialog akhir game
a. Di file GameScreen.kt, pada fungsi FinalScoreDialog(), perhatikan parameter skor untuk menampilkan skor game di dialog pemberitahuan
b. Dalam fungsi FinalScoreDialog(), perhatikan penggunaan ekspresi lambda parameter text untuk menggunakan score sebagai argumen format ke teks dialog
c. Di file GameScreen.kt, di akhir fungsi composable GameScreen(), setelah blok Column, tambahkan kondisi if untuk memeriksa gameUiState.isGameOver
d. Di blok if, tampilkan dialog pemberitahuan. Lakukan panggilan ke FinalScoreDialog() dengan meneruskan score dan gameViewModel.resetGame() untuk callback onPlayAgain
e. Dalam file GameViewModel.kt, panggil kembali fungsi resetGame(), lakukan inisialisasi _uiState, dan pilih kata baru
f. Lakukan 'Run app', ketika game dimainkan sampai selesai, akan muncul dialog pemberitahuan dengan opsi untuk Exit dari game atau Play Again
6. Status Dalam Rotasi Perangkat
Saat terjadi perubahan konfigurasi, Android akan memulai ulang aktivitas dari awal, kemudian 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.
Di bawah ini adalah cara untuk memastikan bahwa aplikasi yang menggunakan ViewModel akan mempertahankan status UI selama perubahan konfigurasi.
a. Jalankan aplikasi dan putar beberapa kata. Ubah konfigurasi perangkat dari potrait ke landscape, atau sebaliknya
b. Perhatikan bahwa data yang disimpan di UI status ViewModel dipertahankan selama perubahan konfigurasi
Hasil Akhir
Berikut adalah tampilan hasil akhir dari aplikasi Unscramble Application.
Berikut adalah kode sumber Unscramble Application
GameScreen.kt
/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.unscramble.ui import android.app.Activity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.shapes import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import com.example.unscramble.R import com.example.unscramble.ui.theme.UnscrambleTheme @Composable fun GameScreen( gameViewModel: GameViewModel = viewModel() ) { val gameUiState by gameViewModel.uiState.collectAsState() val mediumPadding = dimensionResource(R.dimen.padding_medium) Column( modifier = Modifier .statusBarsPadding() .verticalScroll(rememberScrollState()) .safeDrawingPadding() .padding(mediumPadding), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = stringResource(R.string.app_name), style = typography.titleLarge, ) GameLayout( onUserGuessChanged = { gameViewModel.updateUserGuess(it) }, userGuess = gameViewModel.userGuess, isGuessWrong = gameUiState.isGuessedWordWrong, onKeyboardDone = { gameViewModel.checkUserGuess() }, wordCount = gameUiState.currentWordCount, currentScrambledWord = gameUiState.currentScrambledWord, modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(mediumPadding) ) Column( modifier = Modifier .fillMaxWidth() .padding(mediumPadding), verticalArrangement = Arrangement.spacedBy(mediumPadding), horizontalAlignment = Alignment.CenterHorizontally ) { Button( modifier = Modifier.fillMaxWidth(), onClick = { gameViewModel.checkUserGuess() } ) { Text( text = stringResource(R.string.submit), fontSize = 16.sp ) } OutlinedButton( onClick = { gameViewModel.skipWord() }, modifier = Modifier.fillMaxWidth() ) { Text( text = stringResource(R.string.skip), fontSize = 16.sp ) } } GameStatus(score = gameUiState.score, modifier = Modifier.padding(20.dp)) if (gameUiState.isGameOver) { FinalScoreDialog( score = gameUiState.score, onPlayAgain = { gameViewModel.resetGame() } ) } } } @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) ) } } @Composable fun GameLayout( onUserGuessChanged: (String) -> Unit, userGuess: String, isGuessWrong: Boolean, onKeyboardDone: () -> Unit, wordCount: Int, currentScrambledWord: String, 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, wordCount), style = typography.titleMedium, color = colorScheme.onPrimary ) Text( text = currentScrambledWord, style = typography.displayMedium ) Text( text = stringResource(R.string.instructions), textAlign = TextAlign.Center, style = typography.titleMedium ) OutlinedTextField( value = userGuess, singleLine = true, shape = shapes.large, modifier = Modifier.fillMaxWidth(), colors = TextFieldDefaults.colors( focusedContainerColor = colorScheme.surface, unfocusedContainerColor = colorScheme.surface, disabledContainerColor = colorScheme.surface, ), onValueChange = onUserGuessChanged, label = { if (isGuessWrong) { Text(text = stringResource(R.string.wrong_guess)) } else { Text(text = stringResource(R.string.enter_your_word)) } }, isError = isGuessWrong, keyboardOptions = KeyboardOptions.Default.copy( imeAction = ImeAction.Done ), keyboardActions = KeyboardActions( onDone = { onKeyboardDone() } ) ) } } } /* * Creates and shows an AlertDialog with final score. */ @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 // onCloseRequest. }, 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)) } } ) } @Preview(showBackground = true) @Composable fun GameScreenPreview() { UnscrambleTheme { GameScreen() } }
GameViewModel.kt
package com.example.unscramble.ui import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.example.unscramble.data.MAX_NO_OF_WORDS import com.example.unscramble.data.SCORE_INCREASE import com.example.unscramble.data.allWords import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update class GameViewModel : ViewModel() { // Game UI State private val _uiState = MutableStateFlow(GameUiState()) val uiState: StateFlow<GameUiState> = _uiState.asStateFlow() var userGuess by mutableStateOf("") private set // Set of words used in the game private var usedWords: MutableSet<String> = mutableSetOf() private lateinit var currentWord: String init { resetGame() } fun resetGame() { usedWords.clear() _uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle()) } fun updateUserGuess(guessedWord: String){ userGuess = guessedWord } 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("") } fun skipWord() { updateGameState(_uiState.value.score) // Reset user guess updateUserGuess("") } private fun updateGameState(updatedScore: Int) { if (usedWords.size == MAX_NO_OF_WORDS) { // Last round in the game _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(), score = updatedScore, currentWordCount = currentState.currentWordCount.inc() ) } } } 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) } 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) } } }
GameUiState.kt
package com.example.unscramble.ui data class GameUiState( val currentScrambledWord: String = "", val isGuessedWordWrong: Boolean = false, val score: Int = 0, val currentWordCount: Int = 1, val isGameOver: Boolean = false )
Berikut adalah link video demo Unscramble Application
Referensi
https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state?hl=id#0
Komentar
Posting Komentar