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
ViewModelobjects in your app. - How to create
ViewModelobjects using theViewModelProvider.Factoryinterface.
What you'll learn
- What makes
LiveDataobjects useful. - How to add
LiveDatato the data stored in aViewModel. - When and how to use
MutableLiveData. - How to add observer methods to observe changes in the
LiveData. - How to encapsulate
LiveDatausing a backing property. - How to communicate between a UI controller and its corresponding
ViewModel.
What you'll do
- Use
LiveDatafor 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
LiveDataobserver 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.
- (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.
- Run the app and play the game.
- 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:
LiveDatais observable, which means that an observer is notified when the data held by theLiveDataobject changes.LiveDataholds data;LiveDatais a wrapper that can be used with any dataLiveDatais lifecycle-aware, meaning that it only updates observers that are in an active lifecycle state such asSTARTEDorRESUMED.
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
- Under the
screens/gamepackage, open theGameViewModelfile. - Change the type of the variables
scoreandwordtoMutableLiveData.MutableLiveDatais aLiveDatawhose value can be changed.MutableLiveDatais 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>()- In
GameViewModel, inside theinitblock, initializescoreandword. To change the value of aLiveDatavariable, you use thesetValue()method on the variable. In Kotlin, you can callsetValue()using thevalueproperty.
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.
- In
GameViewModel, in theonSkip()method, changescoretoscore.value. Notice the error aboutscorepossibly beingnull. You fix this error next. - To resolve the error, add a
nullcheck toscore.valueinonSkip(). Then call theminus()function onscore, which performs the subtraction withnull-safety.
fun onSkip() {
if (!wordList.isEmpty()) {
score.value = (score.value)?.minus(1)
}
nextWord()
}- Update the
onCorrect()method in the same way: add anullcheck to thescorevariable and use theplus()function.
fun onCorrect() {
if (!wordList.isEmpty()) {
score.value = (score.value)?.plus(1)
}
nextWord()
}- In
GameViewModel, inside thenextWord()method, change thewordreference toword.value.
private fun nextWord() {
if (!wordList.isEmpty()) {
//Select and remove a word from the list
word.value = wordList.removeAt(0)
}
}- In
GameFragment, inside theupdateWordText()method, change the reference toviewModel.wordtoviewModel.word.value.
/** Methods for updating the UI **/
private fun updateWordText() {
binding.wordText.text = viewModel.word.value
}- In
GameFragment, insideupdateScoreText()method, change the reference to theviewModel.scoretoviewModel.score.value.
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.value.toString()
}- In
GameFragment, inside thegameFinished()method, change the reference toviewModel.scoretoviewModel.score.value. Add the requirednull-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)
}- 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.
- In
GameFragment,inside theonCreateView()method, attach anObserverobject to theLiveDataobject for the current score,viewModel.score. Use theobserve()method, and put the code after the initialization of theviewModel. 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.
- The observer that you just created receives an event when the data held by the observed
LiveDataobject changes. Inside the observer, update the scoreTextViewwith the new score.
/** Setting up LiveData observation relationship **/
viewModel.score.observe(this, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})- Attach an
Observerobject to the current wordLiveDataobject. Do it the same way you attached anObserverobject 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.
- In
GameFragment, delete the methodsupdateWordText()andupdateScoreText(), and all references to them. You don't need them anymore, because the text views are updated by theLiveDataobserver methods. - Run your app. Your game app should work exactly as before, but now it uses
LiveDataandLiveDataobservers.
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
MutableLiveDataobject can be changed, as the name implies. Inside theViewModel, the data should be editable, so it usesMutableLiveData. - Data in a
LiveDataobject can be read, but not changed. From outside theViewModel, data should be readable, but not editable, so the data should be exposed asLiveData.
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
- In
GameViewModel, make the currentscoreobjectprivate. - To follow the naming convention used in backing properties, change
scoreto_score. The_scoreproperty is now the mutable version of the game score, to be used internally. - Create a public version of the
LiveDatatype, calledscore.
// The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>- You see an initialization error. This error happens because inside the
GameFragment, thescoreis aLiveDatareference, andscorecan no longer access its setter. To learn more about getters and setters in Kotlin, see Getters and Setters.
To resolve the error, override theget()method for thescoreobject inGameViewModeland return the backing property,_score.
val score: LiveData<Int>
get() = _score- In the
GameViewModel, change the references ofscoreto 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)
}
...
}- Rename the
wordobject to_wordand add a backing property for it, as you did for thescoreobject.
// 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.
- In
GameViewModel, create aBooleanMutableLiveDataobject called_eventGameFinish. This object will hold the game-finished event. - After initializing the
_eventGameFinishobject, create and initialize a backing property calledeventGameFinish.
// Event which triggers the end of the game
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
get() = _eventGameFinish- In
GameViewModel, add anonGameFinish()method. In the method, set the game-finished event,eventGameFinish, totrue.
/** Method for the game completed event **/
fun onGameFinish() {
_eventGameFinish.value = true
}- In
GameViewModel, inside thenextWord()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)
}
}- In
GameFragment, insideonCreateView(), after initializing theviewModel, attach an observer toeventGameFinish. Use theobserve()method. Inside the lambda function, call thegameFinished()method.
// Observer for the Game finished event
viewModel.eventGameFinish.observe(this, Observer<Boolean> { hasFinished ->
if (hasFinished) gameFinished()
})- 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,eventGameFinishis set, the associated observer method in the game fragment is called, and the app navigates to the screen fragment. - The code you added has introduced a lifecycle issue. To understand the issue, in the
GameFragmentclass, comment out the navigation code in thegameFinished()method. Make sure to keep theToastmessage 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)
}
- 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.
- In
GameViewModel, add anonGameFinishComplete()method to reset the game finished event,_eventGameFinish.
/** Method for the game completed event **/
fun onGameFinishComplete() {
_eventGameFinish.value = false
}- In
GameFragment, at the end ofgameFinished(), callonGameFinishComplete()on theviewModelobject. (Leave the navigation code ingameFinished()commented out for now.)
private fun gameFinished() {
...
viewModel.onGameFinishComplete()
}- 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.
- In
GameFragment, inside thegameFinished()method, uncomment the navigation code.
To uncomment in Android Studio, select the lines that are commented out and pressControl+/(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.
- 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.
- In
ScoreViewModel, change thescorevariable type toMutableLiveData. Rename it by convention to_scoreand add a backing property.
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _score- In
ScoreViewModel, inside theinitblock, initialize_score. You can remove or leave the log in theinitblock as you like.
init {
_score.value = finalScore
}- In
ScoreFragment, insideonCreateView(), after initializing theviewModel, attach an observer for the scoreLiveDataobject. 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 theViewModel.
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.
- Run your app and play the game. The app should work as before, but now it uses
LiveDataand 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.
- In
res/layout/score_fragment.xml, for theplay_again_buttonbutton, change thevisibilityattribute's value tovisible.
<Button
android:id="@+id/play_again_button"
...
android:visibility="visible"
/>- In
ScoreViewModel, add aLiveDataobject to hold aBooleancalled_eventPlayAgain. This object is used to save theLiveDataevent to navigate from the score screen to the game screen.
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
get() = _eventPlayAgain- In
ScoreViewModel, define methods to set and reset the event,_eventPlayAgain.
fun onPlayAgain() {
_eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
_eventPlayAgain.value = false
}- In
ScoreFragment, add an observer foreventPlayAgain. Put the code at the end ofonCreateView(), before thereturnstatement. Inside the lambda expression, navigate back to the game screen and reseteventPlayAgain.
// 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.
- In
ScoreFragment, insideonCreateView(), add a click listener to the PlayAgain button and callviewModel.onPlayAgain().
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }- 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
LiveDatais an observable data holder class that is lifecycle-aware, one of the Android Architecture Components.- You can use
LiveDatato enable your UI to update automatically when the data updates. LiveDatais observable, which means that an observer like an activity or an fragment can be notified when the data held by theLiveDataobject changes.LiveDataholds data; it is a wrapper that can be used with any data.LiveDatais lifecycle-aware, meaning that it only updates observers that are in an active lifecycle state such asSTARTEDorRESUMED.
To add LiveData
- Change the type of the data variables in
ViewModeltoLiveDataorMutableLiveData.
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 thesetValue()method on theLiveDatavariable.
To encapsulate LiveData
- The
LiveDatainside theViewModelshould be editable. Outside theViewModel, theLiveDatashould 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, useprivateMutableLiveDatainside theViewModeland return aLiveDatabacking property outside theViewModel.
Observable LiveData
LiveDatafollows an observer pattern. The "observable" is theLiveDataobject, and the observers are the methods in the UI controllers, like fragments. Whenever the data wrapped insideLiveDatachanges, the observer methods in the UI controllers are notified.- To make the
LiveDataobservable, attach an observer object to theLiveDatareference in the observers (such as activities and fragments) using theobserve()method. - This
LiveDataobserver pattern can be used to communicate from theViewModelto the UI controllers.
Udacity course:
Android developer documentation:
Other:
- Backing property in Kotlin
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
ViewModelobject, change the data type of the data toprivateLiveData. Use a backing property to expose read-only data of the typeMutableLiveData. - Inside the
ViewModelobject, change the data type of the data toprivateMutableLiveData. Use a backing property to expose read-only data of the typeLiveData. - Inside the UI controller, change the data type of the data to
privateMutableLiveData. Use a backing property to expose read-only data of the typeLiveData. - Inside the
ViewModelobject, change the data type of the data toLiveData. Use a backing property to expose read-only data of the typeLiveData.
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
LiveDataobject - The UI controller
- The
ViewModelobject
Start the next lesson:
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.




