Wednesday, June 5, 2024

Tugas 11 - ViewModel dan State Pada Compose Dengan Aplikasi Unscramble

Nama     : Florentino Benedictus

NRP       : 5025201222

Tahun    : 2024

Kelas      : Pemrograman Perangkat Bergerak I

Link Implementasi      :  GameScreen.kt, GameUiState.kt, GameViewModel.kt


Tugas 11 - ViewModel dan State Pada Compose Dengan Aplikasi Unscramble

Pada tugas ini, digunakan aplikasi awal Unscramble yaitu aplikasi permainan dimana aplikasi akan menampilkan kata yang hurufnya teracak lalu user harus menebak kata yang tersusun atas huruf-huruf tersebut. Jika kata ditebak dengan benar maka user akan mendapat poin dan aplikasi juga memiliki fitur skip untuk mengubah soal. Pengerjaan tugas menggunakan referensi Google Codelab - ViewModel and State in Compose.

1. Inisiasi Proyek
Pertama-tama buka link https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble/tree/starter yang merupakan kode awal aplikasi, kemudian download ZIP dari branch starter. Ekstrak file .zip yang telah terdownload, pindahkan isi folder hasil ekstrak ke lokasi yang diinginkan, lalu buka Android Studio.
Klik Open pada bagian kanan atas lalu pilih folder project yang sudah di-extract lalu pilih OK. Kemudian tunggu hingga building model Gradle selesai.
Selanjutnya run aplikasi, maka akan terlihat tampilan awal aplikasi Unscramble. Aplikasi Unscramble memiliki beberapa bug yang perlu diperbaiki dan fitur yang perlu ditambahkan.

2. Menambahkan ViewModel
Untuk menggunakan ViewModel maka hal pertama yang perlu dilakukan adalah menambahkan dependensi ViewModel pada build.gradle.app projek.
Kemudian buat file GameViewModel pada package ui yang diextend dari class ViewModel.
Tambahkan juga file data class GameUiState pada package ui.
Langkah selanjutnya yaitu menambahkan StateFlow pada GameViewModel yaitu data holder yang akan menampilkan current state dan update state. 
Selanjutnya buat beberapa fungsi dan variabel tambahan pada GameViewModel untuk menampilkan kata scramble meliputi variabel currentWord yaitu kata scramble saat ini, fungsi pickRandomWordAndShuffle() untuk mendapatkan kata random, kata usedWords untuk menyimpan kata yang sudah terpakai, fungsi shuffleCurrentWord() untuk mengacak kata, fungsi resetGame() untuk inisialisasi permainan, dan init yang akan memanggil resetGame().


3. Passing ViewModel ke UI
Buka GameScreen() pada GameScreen.kt, kemudian passing argumen GameViewModel dan tambahkan gameUiState yang akan melakukan komposisi ulang ketika uiState mengalami perubahan.
Passing currentScrambledWord pada GameLayout() composable pada GameScreen().
Tambahkan currentScrambledWord sebagai parameter pada fungsi composable GameLayout dan ganti hardcoded string "scrambleun" dengan currentScrambledWord.
Ketika dirun ulang, terlihat bahwa kata yang posisi hurufnya teracak sudah dapat dilihat.
Kemudian agar textfield dapat menampilkan kata yang ditebak maka ubah value onValueChange menjadi onUserGuessChanged, tambahkan onKeyboardDone pada onDone KeyboardActions, dan ubah value pada OutlinedTextField menjadi userGuess. Tambahkan juga ketiga variabel tersebut sebagai parameter pada fungsi composable GameLayout. Update juga GameLayout yang dipanggil pada GameScreen.
Selanjutnya tambahkan variabel userGuess dan fungsi updateUserGuess pada GameViewModel.kt.
Terlihat textfield sudah dapat menampilkan kata tebakan user.


4. Verifikasi Kata Tebakan
Tambahkan fungsi checkUserGuess() pada GameViewModel, fungsi ini akan berfungsi untuk melakukan pengecekan tebakan user terhadap kunci jawaban. Tambahkan juga isGuessedWordWrong pada class GameUiState.
Kembali ke GameScreen.kt, ubah isi onClick pada tombol submit dan onKeyboardDone menjadi gameViewModel.checkUserGuess(). Ubah juga isi composable GameLayout pada GameScreen() dan fungsi composable GameLayout sehingga menggunakan variabel isGuessWrong.
Tambahkan kondisional pesan yang ditampilkan pada GameLayout sehingga ketika jawaban salah akan menggunakan resource string wrong_guess pada strings.xml (sudah ada dari template project).
Terlihat bahwa sekarang pesan error akan ditampilkan ketika jawaban salah.


5. Update Score dan Word Count
Tambahkan variabel score dan currentWordCount pada GameUiState. Tambahkan juga isi conditional if dari checkUserGuess di dalam GameViewModel.
Tambahkan fungsi updateGameState pada GameViewModel yang akan digunakan untuk mengupdate skor, menambahkan jumlah currentWordCount, dan mengganti scrambled word dengan kata baru.
Kemudian passing variabel wordCount pada fungsi composable GameLayout dan ganti juga "0" yang ter-hardcode pada kode template sehingga menggunakan wordCount. Pada GameLayout yang dipanggil pada GameScreen argumen wordCount akan didapat dari gameUiState.currentWordCount.
Ubah juga fungsi GameStatus pada GameScreen.kt sehingga score akan disesuaikan dengan nilai score pada gameUiState.

6. Tambahkan Fitur Skip
Tambahkan fungsi skipWord pada GameViewModel.kt, kemudian panggil gameViewModel.skipWord ketika tombol skip ditekan fungsi GameScreen di GameScreen.kt.

7. Handle Last Round Permainan
Agar permainan selesai ketika 10 pertanyaan telah dijawab/diskip, maka perlu ditambahkan kondisi untuk menyelesaikan permainan.
Update fungsi updateGameState pada GameViewModel.kt sehingga ketika MAX_NO_OF_WORDS tercapai maka nilai flag isGameOver akan diubah ke true. Tambahkan juga flag tersebut pada GameUiState.kt.
Selanjutnya FinalScoreDialog yang berfungsi sebagai alert dari template dapat digunakan sebagai display dialog ketika permainan selesai. Tambahkan pengecekan kondisi flag isGameOver pada akhir fungsi GameScreen() pada GameScreen.kt.

8. Hasil Akhir Aplikasi
Berikut adalah hasil uji coba aplikasi yang dibuat, terlihat bahwa tiap jawaban benar akan memberi skor 20, jawaban yang diskip tidak mendapatkan poin, dan ketika 10 pertanyaan telah diberikan maka terdapat pilihan untuk main lagi/keluar dari aplikasi.


Berikut adalah isi code implementasi program:
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.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 com.example.unscramble.R
import com.example.unscramble.ui.theme.UnscrambleTheme
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@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(
userGuess = gameViewModel.userGuess,
wordCount = gameUiState.currentWordCount,
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { gameViewModel.checkUserGuess() },
isGuessWrong = gameUiState.isGuessedWordWrong,
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(
isGuessWrong: Boolean,
userGuess: String,
onUserGuessChanged: (String) -> Unit,
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(stringResource(R.string.wrong_guess))
} else {
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()
}
}
view raw GameScreen.kt hosted with ❤ by GitHub

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,
)
view raw GameUiState.kt hosted with ❤ by GitHub

GameViewModel.kt:
package com.example.unscramble.ui
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import com.example.unscramble.data.allWords
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.example.unscramble.data.MAX_NO_OF_WORDS
import com.example.unscramble.data.SCORE_INCREASE
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
private lateinit var currentWord: String
// Set of words used in the game
private var usedWords: MutableSet<String> = mutableSetOf()
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, 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
)
}
}
}
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 updateUserGuess(guessedWord: String){
userGuess = guessedWord
}
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)
}
}
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)
}
fun resetGame() {
usedWords.clear()
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
init {
resetGame()
}
}

Referensi

https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state#0

No comments:

Post a Comment

EAS PPB I - Aplikasi Alfamind

Nama       : Florentino Benedictus NRP          : 5025201222 Tahun     : 2024 Kelas        : Pemrograman Perangkat Bergerak I Link Desain An...