Tugas 8 ViewModel dan State
Tugas 8 ViewModel dan State
Nama: Akbar Putra Asenti Priyanto
NRP: 5025211004
Pada tugas ke-8 ini, diberikan sebuah game unScramble dimana user akan diberikan sebuah kata yang diacak (shuffle), kemudian user harus menebak kata tersebut.
Berikut adalah link codelabs yang digunakan:
Tujuan dari codelabs ini adalah untuk mempelajari tentang viewmodel dan state dalam jetpack compose yang digunakan untuk menyimpan data game yang nantinya akan ditampilkan pada UI.
Struktur dari project ini adalah sebagai berikut:
WordsData.kt
Menyimpan data kata dan point yang digunakan dalam game unscramble.
/*
* 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.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",
"best",
"birthday",
"book",
"briefcase",
...
"x-ray",
"xylophone",
"yoga",
"yogurt",
"yoyo",
"you",
"year",
"yummy",
"zebra",
"zigzag",
"zoology",
"zone",
"zeal"
)
GameViewModel.kt
package com.example.unscramble.ui
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import com.example.unscramble.data.allWords
import com.example.unscramble.data.SCORE_INCREASE
import kotlinx.coroutines.flow.StateFlow
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 kotlinx.coroutines.flow.update
class GameViewModel : ViewModel() {
var userGuess by mutableStateOf("")
private set
private val _uiState = MutableStateFlow(GameUiState())
private lateinit var currentWord: String
private var usedWords: MutableSet<String> = mutableSetOf()
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()
private fun pickRandomWordsAndShuffle(): String {
currentWord = allWords.random()
if (usedWords.contains(currentWord)){
return pickRandomWordsAndShuffle()
} else {
usedWords.add(currentWord)
return shuffleCurrentWord(currentWord)
}
}
fun updateUserGuess(guessedWord: String){
userGuess = guessedWord
}
private fun shuffleCurrentWord(word: String) : String {
val tempWord = word.toCharArray()
tempWord.shuffle()
while (String(tempWord) == word){
tempWord.shuffle()
}
return String(tempWord)
}
private fun updateGameState(updatedScore: Int) {
if (usedWords.size == MAX_NO_OF_WORDS){
_uiState.update { currentState ->
currentState.copy(
isGuessedWordWrong = false,
score = updatedScore,
isGameOver = true
)
}
} else {
_uiState.update { currentState ->
currentState.copy(
currentWordCount = currentState.currentWordCount.inc(),
isGuessedWordWrong = false,
currentScrambledWord = pickRandomWordsAndShuffle(),
score = updatedScore
)
}
}
}
fun resetGame() {
usedWords.clear()
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordsAndShuffle())
}
fun checkUserGuess(){
if (userGuess.equals(currentWord, ignoreCase = true)) {
// User's guess is correct, increase the score
val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
updateGameState(updatedScore)
} else {
_uiState.update { currentState ->
currentState.copy(isGuessedWordWrong = true)
}
}
updateUserGuess("")
}
fun skipWord() {
updateGameState(_uiState.value.score)
updateUserGuess("")
}
init {
resetGame()
}
}
Merupakan class yang menyimpan logic yang digunakan dalam game, seperti fungsi untuk melakukan pengacakan, pengecekan tebakan user, mereset ulang game, dan update game state.
GameUiState.kt
package com.example.unscramble.ui
data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 1,
val score: Int = 0,
val isGuessedWordWrong: Boolean = false,
val isGameOver: Boolean = false
)
Merupakan data class untuk menyimpan nilai-nilai yang akan diupdate dan ditampilkan di UI.
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 androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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) },
onKeyboardDone = { gameViewModel.checkUserGuess() },
userGuess = gameViewModel.userGuess,
wordCount = gameUiState.currentWordCount,
currentScrambledWord = gameUiState.currentScrambledWord,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(mediumPadding),
isGuessWrong = gameUiState.isGuessedWordWrong,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(mediumPadding),
verticalArrangement = Arrangement.spacedBy(mediumPadding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (gameUiState.isGameOver) {
FinalScoreDialog(
score = gameUiState.score,
onPlayAgain = { gameViewModel.resetGame() }
)
}
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))
}
}
@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(
currentScrambledWord: String,
userGuess: String,
isGuessWrong: Boolean,
wordCount: Int,
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
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,
fontSize = 45.sp,
modifier = modifier.align(Alignment.CenterHorizontally)
)
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()
}
}
Merupakan main activity yang dipanggil, berisi implementasi game itu sendiri.
Dokumentasi:
Comments
Post a Comment