Tugas 11 - ViewModel and State in Compose

Nama : Afiq Akram
NRP : 50252012070
Kelas : PPB-I
Tahun : 2024


ViewModel and State in Compose

Tugas kali ini yaitu membuat aplikasi game Unscramble menggunakan Jetpack Compose dan ViewModel dari library Android Jetpack. Tugas ini mengikuti panduan dari tutorial ViewModel and State in Compose pada Website Android Developer. Tugas ini mengikuti panduan dari tutorial ViewModel and State in Compose. Panduan tersebut akan membantu kita dalam mengatur proyek, memahami arsitektur, dan mengimplementasikan fungsionalitas game.

Deskripsi Proyek

Aplikasi Unscramble adalah permainan kata tunggal dimana pemain harus menebak kata yang diacak. Aplikasi ini menampilkan kata yang diacak, dan pemain harus menebak kata yang benar menggunakan semua huruf yang tersedua. Poin diberikan untuk setiap tebakan yang benar, dan aplikasi akan melacak jumlah kata yang berhasil ditebak. Setiap permainan terdiri dari 10 kata.

MainActivity.kt
Menginisialiasi aplikasi dan mengatur konten menggunakan Jetpack Compose.
package com.example.unscramble
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.unscramble.ui.GameScreen
import com.example.unscramble.ui.theme.UnscrambleTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
UnscrambleTheme {
Surface(
modifier = Modifier.fillMaxSize(),
) {
GameScreen()
}
}
}
}
}
view raw
view raw MainActivity.kt hosted with ❤ by GitHub

GameScreen.kt
Pada file ini berisi composable utama yang menampilkan UI game.
package com.example.unscramble.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
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.res.dimensionResource
import androidx.compose.ui.res.stringResource
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) },
wordCount = gameUiState.currentWordCount,
userGuess = gameViewModel.userGuess,
onKeyboardDone = { gameViewModel.checkUserGuess() },
currentScrambledWord = gameUiState.currentScrambledWord,
isGuessWrong = gameUiState.isGuessedWordWrong,
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(
currentScrambledWord: String,
wordCount: Int,
isGuessWrong: Boolean,
userGuess: String,
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,
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() }
)
)
}
}
}
@Composable
private fun FinalScoreDialog(
score: Int,
onPlayAgain: () -> Unit,
modifier: Modifier = Modifier
) {
val activity = (LocalContext.current as Activity)
AlertDialog(
onDismissRequest = {},
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
Mendefinisikan keadaan UI game.
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
)
view raw GameUiState.kt hosted with ❤ by GitHub

GameViewModel.kt
Berisi logika game dan data.
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() {
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()
var userGuess by mutableStateOf("")
private set
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)) {
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("")
}
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(
isGuessedWordWrong = false,
currentScrambledWord = pickRandomWordAndShuffle(),
currentWordCount = currentState.currentWordCount.inc(),
score = updatedScore
)
}
}
}
private fun shuffleCurrentWord(word: String): String {
val tempWord = word.toCharArray()
tempWord.shuffle()
while (String(tempWord) == word) {
tempWord.shuffle()
}
return String(tempWord)
}
private fun pickRandomWordAndShuffle(): String {
currentWord = allWords.random()
return if (usedWords.contains(currentWord)) {
pickRandomWordAndShuffle()
} else {
usedWords.add(currentWord)
shuffleCurrentWord(currentWord)
}
}
}

WordsData.kt
Berisi daftar kata-kata yang benar dalam game.
package com.example.unscramble.data
const val MAX_NO_OF_WORDS = 10
const val SCORE_INCREASE = 20
val allWords: Set<String> = setOf(
"animal", "auto", "anecdote", "alphabet", "all", "awesome", "arise", "balloon",
"basket", "bench", "best", "birthday", "book", "briefcase", "camera", "camping",
"candle", "cat", "cauliflower", "chat", "children", "class", "classic", "classroom",
"coffee", "colorful", "cookie", "creative", "cruise", "dance", "daytime", "dinosaur",
"doorknob", "dine", "dream", "dusk", "eating", "elephant", "emerald", "eerie",
"electric", "finish", "flowers", "follow", "fox", "frame", "free", "frequent",
"funnel", "green", "guitar", "grocery", "glass", "great", "giggle", "haircut",
"half", "homemade", "happen", "honey", "hurry", "hundred", "ice", "igloo", "invest",
"invite", "icon", "introduce", "joke", "jovial", "journal", "jump", "join", "kangaroo",
"keyboard", "kitchen", "koala", "kind", "kaleidoscope", "landscape", "late", "laugh",
"learning", "lemon", "letter", "lily", "magazine", "marine", "marshmallow", "maze",
"meditate", "melody", "minute", "monument", "moon", "motorcycle", "mountain", "music",
"north", "nose", "night", "name", "never", "negotiate", "number", "opposite", "octopus",
"oak", "order", "open", "polar", "pack", "painting", "person", "picnic", "pillow", "pizza",
"podcast", "presentation", "puppy", "puzzle", "recipe", "release", "restaurant", "revolve",
"rewind", "room", "run", "secret", "seed", "ship", "shirt", "should", "small", "spaceship",
"stargazing", "skill", "street", "style", "sunrise", "taxi", "tidy", "timer", "together",
"tooth", "tourist", "travel", "truck", "under", "useful", "unicorn", "unique", "uplift",
"uniform", "vase", "violin", "visitor", "vision", "volume", "view", "walrus", "wander",
"world", "winter", "well", "whirlwind", "x-ray", "xylophone", "yoga", "yogurt", "yoyo",
"you", "year", "yummy", "zebra", "zigzag", "zoology", "zone", "zeal"
)
view raw WordsData.kt hosted with ❤ by GitHub


Logika Game
1. Reset Game:
resetGame() di ViewModel akan menginisialisasi  ulang data game.
fun resetGame() {
usedWords.clear()
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
view raw resetGame.kt hosted with ❤ by GitHub

2. Update Tebakan
updateUserGuess() untuk memperbarui tebakan pengguna.
fun updateUserGuess(guessedWord: String) {
userGuess = guessedWord
}

3. Memeriksa Tebakan 
checkUserGuess() untuk memeriksa apakah tebakan
fun checkUserGuess() {
if (userGuess.equals(currentWord, ignoreCase = true)) {
val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
updateGameState(updatedScore)
} else {
_uiState.update { currentState ->
currentState.copy(isGuessedWordWrong = true)
}
}
updateUserGuess("")
}

4. Melewatkan Kata:
skipWord() untuk skip kata saat ini.
fun skipWord() {
updateGameState(_uiState.value.score)
updateUserGuess("")
}
view raw skipWord.kt hosted with ❤ by GitHub

5. Memperbarui Keadaan Game:
Fungsi updateGameState() untuk mengupdate/memperbarui keadaan kondisi game.
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(
isGuessedWordWrong = false,
currentScrambledWord = pickRandomWordAndShuffle(),
currentWordCount = currentState.currentWordCount.inc(),
score = updatedScore
)
}
}
}

6. Mengacak Kata:
shuffleCurrentWord() berguna untuk mengacak saat ini.
private fun shuffleCurrentWord(word: String): String {
val tempWord = word.toCharArray()
tempWord.shuffle()
while (String(tempWord) == word) {
tempWord.shuffle()
}
return String(tempWord)
}

7. Memilih Kata Acak dan Mengacak:
pickRandomWordAndShuffle() untuk memilih kata acak dan mengacaknya.
private fun pickRandomWordAndShuffle(): String {
currentWord = allWords.random()
return if (usedWords.contains(currentWord)) {
pickRandomWordAndShuffle()
} else {
usedWords.add(currentWord)
shuffleCurrentWord(currentWord)
}
}

Hasil :


Komentar

Postingan populer dari blog ini

Rekursif - Tower of Hanoi