1. Welcome
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 theViewModelProvider.Factory
interface.
What you'll learn
- What makes
LiveData
objects useful. - How to add
LiveData
to 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
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.
2. App overview
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
3. Task: Get started
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.
4. Task: Add LiveData to the GameViewModel
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 theLiveData
object changes.LiveData
holds data;LiveData
is a wrapper that can be used with any dataLiveData
is lifecycle-aware. When you attach an observer to theLiveData
, the observer is associated with aLifecycleOwner
(usually an Activity or Fragment). The LiveData only updates observers that are in an active lifecycle state such asSTARTED
orRESUMED
. You can read more aboutLiveData
and observation here.
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/game
package, open theGameViewModel
file. - Change the type of the variables
score
andword
toMutableLiveData
.
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>()
- In
GameViewModel
, inside theinit
block, initializescore
andword
. To change the value of aLiveData
variable, you use thesetValue()
method on the variable. In Kotlin, you can callsetValue()
using thevalue
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.
- In
GameViewModel
, in theonSkip()
method, changescore
toscore.value
. Notice the error aboutscore
possibly beingnull
. You fix this error next. - To resolve the error, add a
null
check toscore.value
inonSkip()
. Then call theminus()
function onscore
, which performs the subtraction withnull
-safety.
fun onSkip() {
score.value = (score.value)?.minus(1)
nextWord()
}
- Update the
onCorrect()
method in the same way: add anull
check to thescore
variable and use theplus()
function.
fun onCorrect() {
score.value = (score.value)?.plus(1)
nextWord()
}
- In
GameViewModel
, inside thenextWord()
method, change theword
reference 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
.word
toviewModel
.
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
.score
toviewModel
.
score
.
value.
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.value.toString()
}
- In
GameFragment
, inside thegameFinished()
method, change the reference toviewModel
.score
toviewModel
.
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.
5. Task: Attach observers to the LiveData objects
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. You'll use the fragment view ( viewLifecycleOwner
) as the LifecycleOwner
.
- In
GameFragment,
inside theonCreateView()
method, attach anObserver
object to theLiveData
object 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(viewLifecycleOwner, 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
LiveData
object changes. Inside the observer, update the scoreTextView
with the new score.
/** Setting up LiveData observation relationship **/
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- Attach an
Observer
object to the current wordLiveData
object. Do it the same way you attached anObserver
object to the current score.
/** Setting up LiveData observation relationship **/
viewModel.word.observe(viewLifecycleOwner, 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 theLiveData
observer methods. - Run your app. Your game app should work exactly as before, but now it uses
LiveData
andLiveData
observers.
6. Task: Encapsulate the LiveData
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 theViewModel
, the data should be editable, so it usesMutableLiveData
. - Data in a
LiveData
object 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 currentscore
objectprivate
. - 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. - Create a public version of the
LiveData
type, 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
, thescore
is aLiveData
reference, andscore
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
- In the
GameViewModel
, change the references ofscore
to its internal mutable version,_score
.
init {
...
_score.value = 0
...
}
...
fun onSkip() {
_score.value = (score.value)?.minus(1)
...
}
fun onCorrect() {
_score.value = (score.value)?.plus(1)
...
}
- Rename the
word
object to_word
and add a backing property for it, as you did for thescore
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
.
7. Task: Add a game-finished event
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 aBoolean
MutableLiveData
object called_eventGameFinish
. This object will hold the game-finished event. - After initializing the
_eventGameFinish
object, 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(viewLifecycleOwner, 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, eventGameFinish
is set, the associated observer method in the game fragment is called, and the app navigates to the screen fragment.
7. 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)
}
- 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 theviewModel
object. (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 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
.
- 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.
8. Task: Add LiveData to the ScoreViewModel
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 thescore
variable type toMutableLiveData
. Rename it by convention to_score
and add a backing property.
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _score
- In
ScoreViewModel
, inside theinit
block, initialize_score
. You can remove or leave the log in theinit
block as you like.
init {
_score.value = finalScore
}
- In
ScoreFragment
, insideonCreateView()
, after initializing theviewModel
, attach an observer for the scoreLiveData
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 theViewModel
.
Code to add:
// Add observer for score
viewModel.score.observe(viewLifecycleOwner, 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
LiveData
and an observer to update the score.
9. Task: Add the Play Again button
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_button
button, change thevisibility
attribute's value tovisible
.
<Button
android:id="@+id/play_again_button"
...
android:visibility="visible"
/>
- In
ScoreViewModel
, add aLiveData
object to hold aBoolean
called_eventPlayAgain
. This object is used to save theLiveData
event 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 thereturn
statement. Inside the lambda expression, navigate back to the game screen and reseteventPlayAgain
.
// Navigates back to game when button is pressed
viewModel.eventPlayAgain.observe(viewLifecycleOwner, 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.
10. Solution code
Good work! You changed the architecture of your app to use LiveData
objects 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
11. Summary
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 theLiveData
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 asSTARTED
orRESUMED
.
To add LiveData
- Change the type of the data variables in
ViewModel
toLiveData
orMutableLiveData
.
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 theLiveData
variable.
To encapsulate LiveData
- The
LiveData
inside theViewModel
should be editable. Outside theViewModel
, theLiveData
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
, useprivate
MutableLiveData
inside theViewModel
and return aLiveData
backing property outside theViewModel
.
Observable LiveData
LiveData
follows an observer pattern. The "observable" is theLiveData
object, and the observers are the methods in the UI controllers, like fragments. Whenever the data wrapped insideLiveData
changes, the observer methods in the UI controllers are notified.- To make the
LiveData
observable, attach an observer object to theLiveData
reference in the observers (such as activities and fragments) using theobserve()
method. - This
LiveData
observer pattern can be used to communicate from theViewModel
to the UI controllers.
12. Learn more
Udacity course:
Android developer documentation:
Other:
- Backing property in Kotlin
13. Homework
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 toprivate
LiveData
. Use a backing property to expose read-only data of the typeMutableLiveData
. - Inside the
ViewModel
object, change the data type of the data toprivate
MutableLiveData
. Use a backing property to expose read-only data of the typeLiveData
. - 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 typeLiveData
. - Inside the
ViewModel
object, 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
LiveData
object - The UI controller
- The
ViewModel
object
14. Next codelab
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.