Giter VIP home page Giter VIP logo

basic-android-kotlin-compose-training-unscramble's Introduction

Unscramble app

Single player game app that displays scrambled words. To play the game, player has to make a word using all the letters in the displayed scrambled word. This code demonstrates the Android Architecture component- ViewModel and StateFlow.

Pre-requisites

  • Experience with Kotlin syntax.
  • How to create and run a project in Android Studio.
  • How to create composable functions

Getting Started

  1. Install Android Studio, if you don't already have it.
  2. Download the sample.
  3. Import the sample into Android Studio.
  4. Build and run the sample.

basic-android-kotlin-compose-training-unscramble's People

Contributors

android-dev-lxl avatar osuleymanova avatar seanaujong avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

basic-android-kotlin-compose-training-unscramble's Issues

Android Basics: Viewmodel and State in Compose

Step 7 (Verify guess word and update score):
After sub-step 3 there should be another sub-step before sub-step 4 as follows:

Pass the gameUiState.isGuessedWordWrong to the GameLayout() composable.

GameLayout(
   currentScrambledWord = gameUiState.currentScrambledWord,
   userGuess = gameViewModel.userGuess,
   onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
   onKeyboardDone = { gameViewModel.checkUserGuess() },
   isGuessWrong = gameUiState.isGuessedWordWrong
)

Android Basics: Viewmodel and State in Compose

https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-4-pathway-1%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-viewmodel-and-state#7

Task 8: Update score and Word Count
Part 5 gives the following instructions:

Add another variable for the count in the GameUiState. Call it currentWordCount and initialize it to 1.

but the code is shown as:

data class GameUiState( val currentScrambledWord: String = "", val currentWordCount: Int = 0, val score: Int = 1, val isGuessedWordWrong: Boolean = false, )

currentWordCount in the code is initialized as 0, while the score is initialized as 1

Android Basics: Viewmodel and State in Compose

URL of codelab
https://developer.android.com/codelabs/basic-android-kotlin-compose-test-viewmodel?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-4-pathway-1%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-test-viewmodel#5

In which task and step of the codelab can this issue be found?
All of them, Try running code solution.. won't work

Describe the problem
Can't run with coverage

Steps to reproduce?
Download solution and try running it with coverage

Versions
Android Studio version:
Android Studio Dolphin | 2021.3.1 Patch 1
API version of the emulator:

Additional information
Include screenshots if they would be useful in clarifying the problem.
Ussssntitled

Android Basics: Viewmodel and State in Compose

Android Basics: Viewmodel and State in Compose

There is a typo in the article:

Actual

  1. In the GameScreen() composable function, add lambda parameters for onUserGuessChanged and onKeyboardDone.

Expected

  1. In the GameLayout() composable function, add lambda parameters for onUserGuessChanged and onKeyboardDone.

Link

https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-4-pathway-1%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-viewmodel-and-state#5:~:text=In%20the-,GameScreen,-()%20composable%20function

Android Basics: Viewmodel and State in Compose

after adding gameViewModel: GameViewModel = viewModel() to GameScreen Function error occurs

E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.android.unscramble, PID: 29128
java.lang.NullPointerException: Attempt to invoke interface method 'void java.util.Set.clear()' on a null object reference
at com.example.android.unscramble.ui.GameViewModel.resetGame(GameViewModel.kt:41)
at com.example.android.unscramble.ui.GameViewModel.(GameViewModel.kt:11)
at java.lang.Class.newInstance(Native Method)
at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.kt:202)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:322)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:304)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:278)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:128)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.get(ViewModel.kt:215)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.viewModel(ViewModel.kt:156)
at com.example.android.unscramble.ui.GameScreenKt.GameScreen(GameScreen.kt:187)
at com.example.android.unscramble.ComposableSingletons$MainActivityKt$lambda-1$1.invoke(MainActivity.kt:38)
at com.example.android.unscramble.ComposableSingletons$MainActivityKt$lambda-1$1.invoke(MainActivity.kt:37)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.material.SurfaceKt$Surface$1.invoke(Surface.kt:134)
at androidx.compose.material.SurfaceKt$Surface$1.invoke(Surface.kt:117)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.material.SurfaceKt.Surface-F-jzlyU(Surface.kt:114)
at com.example.android.unscramble.ComposableSingletons$MainActivityKt$lambda-2$1.invoke(MainActivity.kt:35)
at com.example.android.unscramble.ComposableSingletons$MainActivityKt$lambda-2$1.invoke(MainActivity.kt:33)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.material.MaterialTheme_androidKt.PlatformMaterialTheme(MaterialTheme.android.kt:23)
at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:82)
at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:81)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.material.TextKt.ProvideTextStyle(Text.kt:265)
at androidx.compose.material.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:81)
at androidx.compose.material.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:80)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.material.MaterialThemeKt.MaterialTheme(MaterialTheme.kt:72)
at com.example.android.unscramble.ui.theme.ThemeKt.UnscrambleTheme(Theme.kt:61)
at com.example.android.unscramble.ComposableSingletons$MainActivityKt$lambda-3$1.invoke(MainActivity.kt:33)
E/AndroidRuntime: at com.example.android.unscramble.ComposableSingletons$MainActivityKt$lambda-3$1.invoke(MainActivity.kt:32)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:404)
at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:250)
at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:249)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:177)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:123)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:122)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:114)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:157)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:156)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:156)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:140)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:78)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3248)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3238)
at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:341)
at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source:1)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3238)
at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3173)
at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:587)
at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:950)
at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:519)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:140)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1060)
at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:131)
at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:182)
E/AndroidRuntime: at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:360)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:202)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:138)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1147)
at android.view.View.dispatchAttachedToWindow(View.java:20626)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3514)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3521)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3521)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3521)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3521)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2702)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2182)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8730)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1352)
at android.view.Choreographer.doCallbacks(Choreographer.java:1149)
at android.view.Choreographer.doFrame(Choreographer.java:1049)
at android.view.Choreographer$FrameHandler.handleMessage(Choreographer.java:1275)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8010)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
W/System: A resource failed to call close.
W/System: A resource failed to call close.

Android Basics: Viewmodel and State in Compose

Serial 3 below is a bit confusion.

  1. In the GameLayout() composable function, add lambda parameters for onUserGuessChanged and onKeyboardDone.

it gives an impression that the composable function GameLayout() needs to be changed, however, it is actually taking about passing the parameters to the function call GameLayout() in the GameScreen composable.

Android Basics: Viewmodel and State in Compose

Create a gameViewModel_Initialization_FirstWordLoaded() method and annotate it with the @test annotation. In the method, create an expectedScore variable and assign 0 to it.

The method name gameViewModel_Initialization_FirstWordLoaded() above should be changed to gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly()

Android Basics: Viewmodel and State in Compose

In section 9, in the fourth step under 'To implement game-end logic' , there's sample code mistake:
With the current code, currentWordCount would be over 10.

I would change it as follows and it should be fine

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,
-              currentWordCount = currentState.currentWordCount.inc(),
               isGameOver = true
           )
       }
   } else{
       // Normal round in the game
       _uiState.update { currentState ->
           currentState.copy(
               isGuessedWordWrong = false,
               currentScrambledWord = pickRandomWordAndShuffle(),
               currentWordCount = currentState.currentWordCount.inc(),
               score = updatedScore
           )
       }
   }
}

Android Basics: Viewmodel and State in Compose - Write Unit Test codelab

The solution code doesn't pass unit test.

  • In gameViewModel_Initialization_FirstWordLoaded test, gameUiState.currentWordCount should be equal to 0.
  • In updateGameState(updatedScore: Int) function in GameViewModel class, we missed a currentWordCount = currentState.currentWordCount.inc(), at the last round of the game, which cause the gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly test to fail.

BUG: gameViewModel_Initialization_FirstWordLoaded() test failing

https://developer.android.com/codelabs/basic-android-kotlin-compose-test-viewmodel?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-4-pathway-1%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-test-viewmodel#3

In the unit test (boundary case) gameViewModel_Initialization_FirstWordLoaded()

The test case fails as currentWordCount is initially 0, but in the test case we are asserting that current word count is set to 1.

Steps to reproduce*

  1. Go to com.example.android.unscramble.ui.test.GameViewModelTest
  2. Run test gameViewModel_Initialization_FirstWordLoaded()
  3. Test fails with error: java.lang.AssertionError at GameViewModelTest.kt:47

As per code labs the initial word count is 0 so in this unit test we should assert that current word count is set to 0.

Android Basics: Viewmodel and State in Compose

isGuessWrong parameter is not being added to the GameLayout call in the example

GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
isGuessWrong = gameUiState.isGuessedWordWrong,
userGuess = gameViewModel.userGuess,
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { gameViewModel.checkUserGuess() },

)

Android Basics: Viewmodel and State in Compose

There is a mistake in one of tests, In "Write unit test for ViewModel" codelab
@test
fun gameViewModel_Initialization_FirstWordLoaded()
assertTrue(gameUiState.currentWordCount == 1)

initial value for currentWordCount must equal 0

Android Basics: Viewmodel and State in Compose

On the 5th step on this pathway link below;

https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-4-pathway-1%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-viewmodel-and-state#7

The game UI state is given as this;

data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 0,
val score: Int = 1,
val isGuessedWordWrong: Boolean = false,
)

but it should be this; just as step 5 says - "Add another variable for the count in the GameUiState. Call it currentWordCount and initialize it to 1"

data class GameUiState(
val currentScrambledWord: String = "",
val isGuessedWordWrong: Boolean = false,
val score: Int = 0,
val currentWordCount: Int = 1
)

Android Basics: Viewmodel and State in Compose

@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)

    // Assert that current word is scrambled.
    assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
    // Assert that current word count is set to 1.
    assertTrue(gameUiState.currentWordCount == 1)
    // Assert that initially the score is 0.
    assertTrue(gameUiState.score == 0)
    // Assert that the wrong word guessed is false.
    assertFalse(gameUiState.isGuessedWordWrong)
    // Assert that game is not over.
    assertFalse(gameUiState.isGameOver)
}

At the first time the app initialized, currentWordCount should be 0 not 1. Am I wrong?

Android Basics: Viewmodel and State in Compose

Error on the test for Boundary case. The assertion for the current word count was set to 1 instead of 0

Here is the snippet that flags an error when the test is run

@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    ...
    // Assert that current word count is set to 1.
    assertTrue(gameUiState.currentWordCount == 1)
   ...
}

What should be

 // Assert that current word count is set to 0.
    assertTrue(gameUiState.currentWordCount == 0)

This is because the currentWordCount property in GameUiState is initially set to 0.

Android Basics: Viewmodel and State in Compose. AllWordsGuessed_UiStateUpdatedCorrectly. PLEASE FIND CORRECT Version of the test function.

Dear @osuleymanova

Please consider updating the solution in the codelab to the one below. It works, and the test passes provided that, the GameUiState data class is initialized with the property currentWordCount:Int = 1, not zero, as other commenters also noted.

@test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
repeat(MAX_NO_OF_WORDS) {
val currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
//Assert that after each correct answer, score is updated correctly
assertEquals(expectedScore, viewModel.uiState.value.score)
}
val currentGameUiState = viewModel.uiState.value
//Assert that after all questions are answered, the current word count is up-to-date
assertEquals(MAX_NO_OF_WORDS, currentGameUiState.currentWordCount)
// Assert that after 10 questions are answered, the game is over.
assertTrue(currentGameUiState.isGameOver)
}

in the GameViewModel.kt the function updateGameState should be like in the main brach of your code, not like in the viewmodel branch.

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
_uiState.update { currentState ->
currentState.copy(
isGuessedWordWrong = false,
currentScrambledWord = pickRandomAndShuffle(),
currentWordCount = currentState.currentWordCount.inc(),
score = updatedScore
)
}
}
}

Mix androidx.compose imports in view model

I'm not sure if is it OK to mix compose features like mutableStateOf into view model.
Why is it better to use mutableState insted of StateFlow as you show below

// Game UI state
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()
var userGuess by mutableStateOf("")
private set

Is this code wrong?

    // ViewModel
    private var _userGuess = MutableStateFlow("")
    val userGuess : StateFlow<String> = _userGuess.asStateFlow()

    // GameScreen
    val userGuessState by gameViewModel.userGuess.collectAsState()
// ....
    GameLayout(
            onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
            userGuess = userGuessState,
// ...

Android Basics: Viewmodel and State in Compose

Hello,
I have found three issues in the codelab [Write unit tests for ViewModel]
(https://developer.android.com/codelabs/basic-android-kotlin-compose-test-viewmodel?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-4-pathway-1%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-test-viewmodel)

The first one is in the "Test strategy" tab, in the second boundary case with method "gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly()" in the description it is announced as "gameViewModel_Initialization_FirstWordLoaded()", which is not correct, as we have already applied this method earlier. I attached the screenshot for better understanding what i've meant.
Screenshot 2022-09-18 at 20 28 42

The second one is in the "Introduction to the code coverage" tab. When we add "gameViewModel_WordSkipped_ScoreUnchangedAndWordCountIncreased()" method, the companion object of SCORE_AFTER_FIRST_CORRECT_ANSWER is missed (you used it in the solution code tab, but skipped it in the codelab). Of course it is underlined as an error in the Android Studio.
Here is the screenshot
Screenshot 2022-09-18 at 20 54 33

The third issue is in the solution code (and in the codelab accordingly).
Two test can't be passed even in the solution code according to the screenshot below.
Screenshot 2022-09-18 at 20 08 51

And actually it is an issue of the previous codelab where we did Unscrambled app. To end the game we had this condition: (usedWords.size == MAX_NO_OF_WORDS), which equals to 10 == 10. But in the test we compare GameUiState.currentWordCount with MAX_NO_OF_WORDS. And as initial value of currentWordCount equals to 0, the game stops when currentWordCount equals to 9 (so the first word is counted as zero word, the second one as the first one and so on). You can see it when you open the app, it will be written "0 of 10 words". I think it is illogical as when we start the game the first one of 10 words is available for guessing. Further more, when we end the game the latest count shown is "9 of 10 words", which can be confusing for user, as he or she will think that there is one more word to guess before the end of the game.
I think that we should reassign the initial value of GameUiState.currentWordCount to 1 on both codelabs to solve this issue.

Thanks so much for your codelabs.

Android Basics: Viewmodel and State in Compose

The default values for score and currentWordCount are flip-flopped in this snippet.

"""
Next, similar to the updates for the score, you need to update the word count.

Add another variable for the count in the GameUiState. Call it currentWordCount and initialize it to 1.
"""

data class GameUiState(
   val currentScrambledWord: String = "",
   val currentWordCount: Int = 0, // should be 1
   val score: Int = 1, // should be 0
   val isGuessedWordWrong: Boolean = false,
)

Android Basics: Viewmodel and State in Compose

It seems to be an error on Step 8 Update Score and Count

There is a check done in here:
fun checkUserGuess() { if (_uiState.value.userGuess.equals(currentWord, true)) { // User's guess is correct, increase the score val updatedScore = _uiState.value.score.plus(SCORE_INCREASE) } else { //... } }

However on this step properties of uiState are:
data class GameUiState( val currentScrambledWord: String = "", val isGuessedWordWrong: Boolean = false, val score: Int = 0 )

There is no property called userGuess

is it an error or read something wrong?

Android Basics: Viewmodel and State in Compose

https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-4-pathway-1%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-viewmodel-and-state#7

On the topic 8 and step 5, the values of "score" and "currentWordCount" are inverted in the code.

data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 0,
val score: Int = 1,
val isGuessedWordWrong: Boolean = false,
)

score should be = 0 and currentWordCount should be = 1.

Android Basics: Viewmodel and State in Compose

Second block of code in codelab step 7 references property that does not exist in state

7. Verify guess word and update score

import kotlinx.coroutines.flow.update

   if (_uiState.value.userGuess.equals(currentWord, true)) {
   } else {
       // User's guess is wrong, show an error
       _uiState.update { currentState ->
           currentState.copy(isGuessedWordWrong = true)
       }
   }

Here is the code in the solution:

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

Viewmodels and compose issue with imports

Ok so I cant tell if I screwed up or something, but when I insert

import androidx.lifecycle.viewmodel.compose.viewModel
it tells me that compose is not a folder despite it being in the solution code and the tutorial telling me that is the import. I was wondering if I messed up or the tutorial was outdated. I also tried to see any other usages of something called just viewModel and nothing came up so does anyone know whats happening?

Android Basics: Viewmodel and State in Compose

The viewmodel is initialized wrong, as revealed by gameViewModel_Initialization_FirstWordLoaded (BTW gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly also fails for the same reason). currentWordCount is only set (or incremented) by updateGameState, but neither viewModel.init nor resetGame() calls that.
One idea how to fix that is reset game has to be changed from
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
to
_uiState.value = GameUiState()
updateGameState(0)

Android Basics: Viewmodel and State in Compose

This codelabs has some errors

  • the variable currentGameUiState in fun gameViewModel_Initialization_FirstWordLoaded are not consistent

  • gameViewModel_Initialization_FirstWordLoaded test won't pass

    • maybe assertTrue should == 0

Android Basics: Viewmodel and State in Compose

In section 6, in the first step under 'Display the guess word', there's a typo:
...set onUserGuessChanged to onValueChange and onKeyboardDone() to onDone keyboard action.
Correction:
...set onValueChange to onUserGuessChanged and onDone to onKeyboardDone() keyboard action.

Android Basics: Viewmodel and State in Compose

Android Basics: Viewmodel and State in Compose

En el paso 7 del codelab, dentro de la condición if de la función checkUserGuess(). En codelab se escribe esto if (_uiState.value.userGuess.equals(currentWord, true)), y en realidad debería quedar así if (userGuess.equals(currentWord, true)).
Mi función al final del Codelab queda así, para también incrementar palabra intentada a la hora de un fallo:

 /*
     * Checks if the user's guess is correct.
     * Increases the score accordingly.
     */
    fun checkUserGuess() {
        if (userGuess != "") {
            if (userGuess.equals(currentWord, ignoreCase = true)) {
                val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
                updateGameState(updatedScore)
            } else {
                // User's guess is wrong, show an error
                if (usedWords.size < MAX_NO_OF_WORDS) {
                    _uiState.update { currentGameUiState ->
                        currentGameUiState.copy(
                            isGuessedWordWrong = true,
                            currentWordCount = currentGameUiState.currentWordCount.inc()
                        )
                    }
                    usedWords.add(userGuess)
                } else {
                    lastRound()
                }
            }
            // Reset user guess
            updateUserGuess("")
        }
    }

Edito. También hay que actualizar dentro del else el estado de la actualización del juego. updateGameState(_uiState.value.score)
Edito2. Solucionado. Subido proyecto también a Github. Rama newfun - Unscramble Github

Android Basics: Viewmodel and State in Compose

The gameViewModel_Initialization_FirstWordLoaded() test function will fail as the currentWordCount variable value will be 0, not 1 as expected in the assertion.

The currentWordCount property in the gameUiState data class is initialized with the value of 0.
Value is updated only when the user guesses the word correctly.

Solution(?): initialize the currentWordCount property with the value of 1 instead.

Android Basics: Viewmodel and State in Compose

In the boundary case testing of the unscramble app, the currentWordCount variable should be
"assertTrue(currentGameUiState.currentWordCount==0)" since we are still on the first word. Only the updateGameState() function increments the word count.

Android Basics: Viewmodel and State in Compose

@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val currentGameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)

}

To verify the state is correct, add the assertTrue() functions to assert that the currentWordCount property is set to 1, and the score property is set to 0.
Add assertFalse() functions to verify that the isGuessedWordWrong property is false and that the isGameOver property is set to false.

@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)

    // Assert that current word is scrambled.
    assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
    // Assert that current word count is set to 1.
    assertTrue(gameUiState.currentWordCount == 1)
    // Assert that initially the score is 0.
    assertTrue(gameUiState.score == 0)
    // Assert that the wrong word guessed is false.
    assertFalse(gameUiState.isGuessedWordWrong)
    // Assert that game is not over.
    assertFalse(gameUiState.isGameOver)
}

This part, I see the variable name suddenly changed from currentGameUiState to gameUiState.

Android Basics: Viewmodel and State in Compose ... unresolved compose

I'm working through the Android Training and hit a wall with the import of viewModel. In the GameScreen.kt file, the line

import androidx.lifecycle.viewmodel.compose.viewModel shows compose in RED as an unresolved reference.
build.gradle has:
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"

As a result of the import not working, fun GameScreen(
modifier: Modifier = Modifier,
gameViewModel: GameViewModel = viewModel()
)

viewModel() is also not resolved.

To ensure I didn't miss something I changed to the viewmodel branch and a Rebuild does the same problem I was having on the Starter branch.

My install is:
Android Studio Dolphin | 2021.3.1 Patch 1
Build #AI-213.7172.25.2113.9123335, built on September 29, 2022
Runtime version: 11.0.13+0-b1751.21-8125866 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Linux 5.15.0-56-generic
GC: G1 Young Generation, G1 Old Generation
Memory: 2048M
Cores: 12
Registry:
external.system.auto.import.disabled=true
ide.text.editor.with.preview.show.floating.toolbar=false

Current Desktop: X-Cinnamon

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.