ViewModel and State in Compose

 Alif Adrian Anzary - 5025201274

Tugas kali ini adalah membuat aplikasi game Unscramble dengan menggunakan Jetpack Compose dan ViewModel dari library Android Jetpack. Kita akan mengikuti panduan dari tutorial "ViewModel and State in Compose" yang tersedia di Website Android Developer. Panduan ini akan membantu kita dalam mengatur proyek, memahami arsitektur, serta mengimplementasikan fungsionalitas game. Berikut adalah code yang saya gunakan:

MainActivity.kt

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

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()

    }

}

GameUiSkate.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

)

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() {


    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

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"

)


Comments

Popular posts from this blog

program conversion untuk composite database