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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
GameScreen.kt
Pada file ini berisi composable utama yang menampilkan UI game.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
GameUiState.kt
Mendefinisikan keadaan UI game.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) |
GameViewModel.kt
Berisi logika game dan data.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
) |
Logika Game
1. Reset Game:
resetGame() di ViewModel akan menginisialisasi ulang data game.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun resetGame() { | |
usedWords.clear() | |
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle()) | |
} |
2. Update Tebakan
updateUserGuess() untuk memperbarui tebakan pengguna.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun updateUserGuess(guessedWord: String) { | |
userGuess = guessedWord | |
} |
3. Memeriksa Tebakan
checkUserGuess() untuk memeriksa apakah tebakan
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun skipWord() { | |
updateGameState(_uiState.value.score) | |
updateUserGuess("") | |
} |
5. Memperbarui Keadaan Game:
Fungsi updateGameState() untuk mengupdate/memperbarui keadaan kondisi game.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private fun pickRandomWordAndShuffle(): String { | |
currentWord = allWords.random() | |
return if (usedWords.contains(currentWord)) { | |
pickRandomWordAndShuffle() | |
} else { | |
usedWords.add(currentWord) | |
shuffleCurrentWord(currentWord) | |
} | |
} |
Hasil :
Komentar
Posting Komentar