Android Kotlin Fundamentals 05.2: LiveData and LiveData observers

This codelab is part of the Android Kotlin Fundamentals course. You'll get the most value out of this course if you work through the codelabs in sequence. All the course codelabs are listed on the Android Kotlin Fundamentals codelabs landing page.

Introduction

In the previous codelab, you used a ViewModel in the GuessTheWord app to allow the app's data to survive device-configuration changes. In this codelab, you learn how to integrate LiveData with the data in the ViewModel classes. LiveData, which is one of the Android Architecture Components, lets you build data objects that notify views when the underlying database changes.

To use the LiveData class, you set up "observers" (for example, activities or fragments) that observe changes in the app's data. LiveData is lifecycle-aware, so it only updates app-component observers that are in an active lifecycle state.

What you should already know

  • How to create basic Android apps in Kotlin.
  • How to navigate between your app's destinations.
  • Activity and fragment lifecycle.
  • How to use ViewModel objects in your app.
  • How to create ViewModel objects using the ViewModelProvider.Factory interface.

What you'll learn

  • What makes LiveData objects useful.
  • How to add LiveData to the data stored in a ViewModel.
  • When and how to use MutableLiveData.
  • How to add observer methods to observe changes in the LiveData.
  • How to encapsulate LiveData using a backing property.
  • How to communicate between a UI controller and its corresponding ViewModel.

What you'll do

  • Use LiveData for the word and the score in the GuessTheWord app.
  • Add observers that notice when the word or the score changes.
  • Update the text views that display changed values.
  • Use the LiveData observer pattern to add a game-finished event.
  • Implement the Play Again button.

In the Lesson 5 codelabs, you develop the GuessTheWord app, beginning with starter code. GuessTheWord is a two-player charades-style game, where the players collaborate to achieve the highest score possible.

The first player looks at the words in the app and acts each one out in turn, making sure not to show the word to the second player. The second player tries to guess the word.

To play the game, the first player opens the app on the device and sees a word, for example "guitar," as shown in the screenshot below.

The first player acts out the word, being careful not to actually say the word itself.

  • When the second player guesses the word correctly, the first player presses the Got It button, which increases the count by one and shows the next word.
  • If the second player can't guess the word, the first player presses the Skip button, which decreases the count by one and skips to the next word.
  • To end the game, press the End Game button. (This functionality isn't in the starter code for the first codelab in the series.)

In this codelab, you improve the GuessTheWord app by adding an event to end the game when the user cycles through all the words in the app. You also add a Play Again button in the score fragment, so the user can play the game again.

Title screen

Game screen

Score screen

In this task, you locate and run your starter code for this codelab. You can use the GuessTheWord app that you built in previous codelab as your starter code, or you can download a starter app.

  1. (Optional) If you're not using your code from the previous codelab, download the starter code for this codelab. Unzip the code, and open the project in Android Studio.
  2. Run the app and play the game.
  3. Notice that the Skip button displays the next word and decreases the score by one, and the Got It button shows the next word and increases the score by one. The End Game button ends the game.

LiveData is an observable data holder class that is lifecycle-aware. For example, you can wrap a LiveData around the current score in the GuessTheWord app. In this codelab, you learn about several characteristics of LiveData:

  • LiveData is observable, which means that an observer is notified when the data held by the LiveData object changes.
  • LiveData holds data; LiveData is a wrapper that can be used with any data
  • LiveData is lifecycle-aware, meaning that it only updates observers that are in an active lifecycle state such as STARTED or RESUMED.

In this task, you learn how to wrap any data type into LiveData objects by converting the current score and current word data in the GameViewModel to LiveData. In a later task, you add an observer to these LiveData objects and learn how to observe the LiveData.

Step 1: Change the score and word to use LiveData

  1. Under the screens/game package, open the GameViewModel file.
  2. Change the type of the variables score and word to MutableLiveData.

    MutableLiveData is a LiveData whose value can be changed. MutableLiveData is a generic class, so you need to specify the type of data that it holds.
// The current word
val word = MutableLiveData<String>()
// The current score
val score = MutableLiveData<Int>()
  1. In GameViewModel, inside the init block, initialize score and word. To change the value of a LiveData variable, you use the setValue() method on the variable. In Kotlin, you can call setValue() using the value property.
init {

   word.value = ""
   score.value = 0
  ...
}

Step 2: Update the LiveData object reference

The score and word variables are now of the type LiveData. In this step, you change the references to these variables, using the value property.

  1. In GameViewModel, in the onSkip() method, change score to score.value. Notice the error about score possibly being null. You fix this error next.
  2. To resolve the error, add a null check to score.value in onSkip(). Then call the minus() function on score, which performs the subtraction with null-safety.
fun onSkip() {
   if (!wordList.isEmpty()) {
       score.value = (score.value)?.minus(1)
   }
   nextWord()
}
  1. Update the onCorrect() method in the same way: add a null check to the score variable and use the plus() function.
fun onCorrect() {
   if (!wordList.isEmpty()) {
       score.value = (score.value)?.plus(1)
   }
   nextWord()
}
  1. In GameViewModel, inside the nextWord() method, change the word reference to word.value.
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       word.value = wordList.removeAt(0)
   }
}
  1. In GameFragment, inside the updateWordText() method, change the reference to viewModel.word to viewModel.word.value.
/** Methods for updating the UI **/
private fun updateWordText() {
   binding.wordText.text = viewModel.word.value
}
  1. In GameFragment, inside updateScoreText() method, change the reference to the viewModel.score to viewModel.score.value.
private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.value.toString()
}
  1. In GameFragment, inside the gameFinished() method, change the reference to viewModel.score to viewModel.score.value. Add the required null-safety check.
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   NavHostFragment.findNavController(this).navigate(action)
}
  1. Make sure there are no errors in your code. Compile and run your app. The app's functionality should be the same as it was before.

This task is closely related to the previous task, where you converted the score and word data into LiveData objects. In this task, you attach Observer objects to those LiveData objects.

  1. In GameFragment, inside the onCreateView() method, attach an Observer object to the LiveData object for the current score, viewModel.score. Use the observe() method, and put the code after the initialization of the viewModel. Use a lambda expression to simplify the code. (A lambda expression is an anonymous function that isn't declared, but is passed immediately as an expression.)
viewModel.score.observe(this, Observer { newScore ->
})

Resolve the reference to Observer. To do this, click on Observer, press Alt+Enter (Option+Enter on a Mac), and import androidx.lifecycle.Observer.

  1. The observer that you just created receives an event when the data held by the observed LiveData object changes. Inside the observer, update the score TextView with the new score.
/** Setting up LiveData observation relationship **/
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Attach an Observer object to the current word LiveData object. Do it the same way you attached an Observer object to the current score.
/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})

When the value of score or the word changes, the score or word displayed on the screen now updates automatically.

  1. In GameFragment, delete the methods updateWordText() and updateScoreText(), and all references to them. You don't need them anymore, because the text views are updated by the LiveData observer methods.
  2. Run your app. Your game app should work exactly as before, but now it uses LiveData and LiveData observers.

Encapsulation is a way to restrict direct access to some of an object's fields. When you encapsulate an object, you expose a set of public methods that modify the private internal fields. Using encapsulation, you control how other classes manipulate these internal fields.

In your current code, any external class can modify the score and word variables using the value property, for example using viewModel.score.value. It might not matter in the app you're developing in this codelab, but in a production app, you want control over the data in the ViewModel objects.

Only the ViewModel should edit the data in your app. But UI controllers need to read the data, so the data fields can't be completely private. To encapsulate your app's data, you use both MutableLiveData and LiveData objects.

MutableLiveData vs. LiveData:

  • Data in a MutableLiveData object can be changed, as the name implies. Inside the ViewModel, the data should be editable, so it uses MutableLiveData.
  • Data in a LiveData object can be read, but not changed. From outside the ViewModel, data should be readable, but not editable, so the data should be exposed as LiveData.

To carry out this strategy, you use a Kotlin backing property. A backing property allows you to return something from a getter other than the exact object. In this task, you implement a backing property for the score and word objects in the GuessTheWord app.

Add a backing property to score and word

  1. In GameViewModel, make the current score object private.
  2. To follow the naming convention used in backing properties, change score to _score. The _score property is now the mutable version of the game score, to be used internally.
  3. Create a public version of the LiveData type, called score.
// The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
  1. You see an initialization error. This error happens because inside the GameFragment, the score is a LiveData reference, and score can no longer access its setter. To learn more about getters and setters in Kotlin, see Getters and Setters.

    To resolve the error, override the get() method for the score object in GameViewModel and return the backing property, _score.
val score: LiveData<Int>
   get() = _score
  1. In the GameViewModel, change the references of score to its internal mutable version, _score.
init {
   ...
   _score.value = 0
   ...
}

...
fun onSkip() {
   if (!wordList.isEmpty()) {
       _score.value = (score.value)?.minus(1)
   }
  ...
}

fun onCorrect() {
   if (!wordList.isEmpty()) {
       _score.value = (score.value)?.plus(1)
   }
   ...
}
  1. Rename the word object to _word and add a backing property for it, as you did for the score object.
// The current word
private val _word = MutableLiveData<String>()
val word: LiveData<String>
   get() = _word
...
init {
   _word.value = ""
   ...
}
...
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       _word.value = wordList.removeAt(0)
   }
}

Great job, you've encapsulated the LiveData objects word and score.

Your current app navigates to the score screen when the user taps the End Game button. You also want the app to navigate to the score screen when the players have cycled through all the words. After the players finish with the last word, you want the game to end automatically so the user doesn't have to tap the button.

To implement this functionality, you need an event to be triggered and communicated to the fragment from the ViewModel when all the words have been shown. To do this, you use the LiveData observer pattern to model a game-finished event.

The observer pattern

The observer pattern is a software design pattern. It specifies communication between objects: an observable (the "subject" of observation) and observers. An observable is an object that notifies observers about the changes in its state.

In the case of LiveData in this app, the observable (subject) is the LiveData object, and the observers are the methods in the UI controllers, such as fragments. A state change happens whenever the data wrapped inside LiveData changes. The LiveData classes are crucial in communicating from the ViewModel to the fragment.

Step 1: Use LiveData to detect a game-finished event

In this task, you use the LiveData observer pattern to model a game-finished event.

  1. In GameViewModel, create a Boolean MutableLiveData object called _eventGameFinish. This object will hold the game-finished event.
  2. After initializing the _eventGameFinish object, create and initialize a backing property called eventGameFinish.
// Event which triggers the end of the game
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
   get() = _eventGameFinish
  1. In GameViewModel, add an onGameFinish() method. In the method, set the game-finished event, eventGameFinish, to true.
/** Method for the game completed event **/
fun onGameFinish() {
   _eventGameFinish.value = true
}
  1. In GameViewModel, inside the nextWord() method, end the game if the word list is empty.
private fun nextWord() {
   if (wordList.isEmpty()) {
       onGameFinish()
   } else {
       //Select and remove a _word from the list
       _word.value = wordList.removeAt(0)
   }
}
  1. In GameFragment, inside onCreateView(), after initializing the viewModel, attach an observer to eventGameFinish. Use the observe() method. Inside the lambda function, call the gameFinished() method.
// Observer for the Game finished event
viewModel.eventGameFinish.observe(this, Observer<Boolean> { hasFinished ->
   if (hasFinished) gameFinished()
})
  1. Run your app, play the game, and go through all the words. The app navigates to the score screen automatically, instead of staying in the game fragment until you tap End Game.

    After the word list is empty, eventGameFinish is set, the associated observer method in the game fragment is called, and the app navigates to the screen fragment.
  2. The code you added has introduced a lifecycle issue. To understand the issue, in the GameFragment class, comment out the navigation code in the gameFinished() method. Make sure to keep the Toast message in the method.
private fun gameFinished() {
       Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
//        val action = GameFragmentDirections.actionGameToScore()
//        action.score = viewModel.score.value?:0
//        NavHostFragment.findNavController(this).navigate(action)
   }
  1. Run your app, play the game, and go through all the words. A toast message that says "Game has just finished" appears briefly at the bottom of the game screen, which is the expected behavior.

Now rotate the device or emulator. The toast displays again! Rotate the device a few more times, and you will probably see the toast every time. This is a bug, because the toast should only display once, when the game is finished. The toast shouldn't display every time the fragment is re-created. You resolve this issue in the next task.

Step 2: Reset the game-finished event

Usually, LiveData delivers updates to the observers only when data changes. An exception to this behavior is that observers also receive updates when the observer changes from an inactive to an active state.

This is why the game-finished toast is triggered repeatedly in your app. When the game fragment is re-created after a screen rotation, it moves from an inactive to an active state. The observer in the fragment is re-connected to the existing ViewModel and receives the current data. The gameFinished() method is re-triggered, and the toast displays.

In this task, you fix this issue and display the toast only once, by resetting the eventGameFinish flag in the GameViewModel.

  1. In GameViewModel, add an onGameFinishComplete() method to reset the game finished event, _eventGameFinish.
/** Method for the game completed event **/

fun onGameFinishComplete() {
   _eventGameFinish.value = false
}
  1. In GameFragment, at the end of gameFinished(), call onGameFinishComplete() on the viewModel object. (Leave the navigation code in gameFinished() commented out for now.)
private fun gameFinished() {
   ...
   viewModel.onGameFinishComplete()
}
  1. Run the app and play the game. Go through all the words, then change the screen orientation of the device. The toast is displayed only once.
  2. In GameFragment, inside the gameFinished() method, uncomment the navigation code.

    To uncomment in Android Studio, select the lines that are commented out and press Control+/ (Command+/ on a Mac).
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   findNavController(this).navigate(action)
   viewModel.onGameFinishComplete()
}

If prompted by Android Studio, import androidx.navigation.fragment.NavHostFragment.findNavController.

  1. Run the app and play the game. Make sure that the app navigates automatically to the final score screen after you go through all the words.

Great Job! Your app uses LiveData to trigger a game-finished event to communicate from the GameViewModel to the game fragment that the word list is empty. The game fragment then navigates to the score fragment.

In this task, you change the score to a LiveData object in the ScoreViewModel and attach an observer to it. This task is similar to what you did when you added LiveData to the GameViewModel.

You make these changes to ScoreViewModel for completeness, so that all the data in your app uses LiveData.

  1. In ScoreViewModel, change the score variable type to MutableLiveData. Rename it by convention to _score and add a backing property.
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
   get() = _score
  1. In ScoreViewModel, inside the init block, initialize _score. You can remove or leave the log in the init block as you like.
init {
   _score.value = finalScore
}
  1. In ScoreFragment, inside onCreateView(), after initializing the viewModel, attach an observer for the score LiveData object. Inside the lambda expression, set the score value to the score text view. Remove the code that directly assigns the text view with the score value from the ViewModel.

Code to add:

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})

Code to remove:

binding.scoreText.text = viewModel.score.toString()

When prompted by Android Studio, import androidx.lifecycle.Observer.

  1. Run your app and play the game. The app should work as before, but now it uses LiveData and an observer to update the score.

In this task, you add a Play Again button to the score screen and implement its click listener using a LiveData event. The button triggers an event to navigate from the score screen to the game screen.

The starter code for the app includes the Play Again button, but the button is hidden.

  1. In res/layout/score_fragment.xml, for the play_again_button button, change the visibility attribute's value to visible.
<Button
   android:id="@+id/play_again_button"
...
   android:visibility="visible"
 />
  1. In ScoreViewModel, add a LiveData object to hold a Boolean called _eventPlayAgain. This object is used to save the LiveData event to navigate from the score screen to the game screen.
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
   get() = _eventPlayAgain
  1. In ScoreViewModel, define methods to set and reset the event, _eventPlayAgain.
fun onPlayAgain() {
   _eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
   _eventPlayAgain.value = false
}
  1. In ScoreFragment, add an observer for eventPlayAgain. Put the code at the end of onCreateView(), before the return statement. Inside the lambda expression, navigate back to the game screen and reset eventPlayAgain.
// Navigates back to game when button is pressed
viewModel.eventPlayAgain.observe(this, Observer { playAgain ->
   if (playAgain) {
      findNavController().navigate(ScoreFragmentDirections.actionRestart())
       viewModel.onPlayAgainComplete()
   }
})

Import androidx.navigation.fragment.findNavController, when prompted by Android Studio.

  1. In ScoreFragment, inside onCreateView(), add a click listener to the PlayAgain button and call viewModel.onPlayAgain().
binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. Run your app and play the game. When the game is finished, the score screen shows the final score and the Play Again button. Tap the PlayAgain button, and the app navigates to the game screen so that you can play the game again.

Good work! You changed the architecture of your app to use LiveDataobjects in the ViewModel, and you attached observers to the LiveData objects. LiveData notifies observer objects when the value held by the LiveData changes.

Android Studio project: GuessTheWord

LiveData

  • LiveData is an observable data holder class that is lifecycle-aware, one of the Android Architecture Components.
  • You can use LiveData to enable your UI to update automatically when the data updates.
  • LiveData is observable, which means that an observer like an activity or an fragment can be notified when the data held by the LiveData object changes.
  • LiveData holds data; it is a wrapper that can be used with any data.
  • LiveData is lifecycle-aware, meaning that it only updates observers that are in an active lifecycle state such as STARTED or RESUMED.

To add LiveData

  • Change the type of the data variables in ViewModel to LiveData or MutableLiveData.

MutableLiveData is a LiveData object whose value can be changed. MutableLiveData is a generic class, so you need to specify the type of data that it holds.

  • To change the value of the data held by the LiveData, use the setValue() method on the LiveData variable.

To encapsulate LiveData

  • The LiveData inside the ViewModel should be editable. Outside the ViewModel, the LiveData should be readable. This can be implemented using a Kotlin backing property.
  • A Kotlin backing property allows you to return something from a getter other than the exact object.
  • To encapsulate the LiveData, use private MutableLiveData inside the ViewModel and return a LiveData backing property outside the ViewModel.

Observable LiveData

  • LiveData follows an observer pattern. The "observable" is the LiveData object, and the observers are the methods in the UI controllers, like fragments. Whenever the data wrapped inside LiveData changes, the observer methods in the UI controllers are notified.
  • To make the LiveData observable, attach an observer object to the LiveData reference in the observers (such as activities and fragments) using the observe() method.
  • This LiveData observer pattern can be used to communicate from the ViewModel to the UI controllers.

Udacity course:

Android developer documentation:

Other:

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

How do you encapsulate the LiveData stored in a ViewModel so that external objects can read data without being able to update it?

  • Inside the ViewModel object, change the data type of the data to private LiveData. Use a backing property to expose read-only data of the type MutableLiveData.
  • Inside the ViewModel object, change the data type of the data to private MutableLiveData. Use a backing property to expose read-only data of the type LiveData.
  • Inside the UI controller, change the data type of the data to private MutableLiveData. Use a backing property to expose read-only data of the type LiveData.
  • Inside the ViewModel object, change the data type of the data to LiveData. Use a backing property to expose read-only data of the type LiveData.

Question 2

LiveData updates a UI controller (such as a fragment) if the UI controller is in which of the following states?

  • Resumed
  • In the background
  • Paused
  • Stopped

Question 3

In the LiveData observer pattern, what's the observable item (what is observed)?

  • The observer method
  • The data in a LiveData object
  • The UI controller
  • The ViewModel object

Start the next lesson: 5.3: Data binding with ViewModel and LiveData

For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.