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.
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.
ViewModel
objects in your app.ViewModel
objects using the ViewModelProvider.Factory
interface.LiveData
objects useful.LiveData
to the data stored in a ViewModel
.MutableLiveData
.LiveData.
LiveData
using a backing property.ViewModel
.LiveData
for the word and the score in the GuessTheWord app.LiveData
observer pattern to add a game-finished event.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.
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.
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 dataLiveData
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
.
screens/game
package, open the GameViewModel
file.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>()
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
...
}
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.
GameViewModel
, in the onSkip()
method, change score
to score.value
. Notice the error about score
possibly being null
. You fix this error next.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()
}
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()
}
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)
}
}
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
}
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()
}
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)
}
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.
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
.
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()
})
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.
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. 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
:
MutableLiveData
object can be changed, as the name implies. Inside the ViewModel
, the data should be editable, so it uses MutableLiveData
. 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.
GameViewModel
, make the current score
object private
.score
to _score
. The _score
property is now the mutable version of the game score, to be used internally.LiveData
type, called score
. // The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
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.get()
method for the score
object in GameViewModel
and return the backing property, _score
. val score: LiveData<Int>
get() = _score
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)
}
...
}
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 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.
In this task, you use the LiveData
observer pattern to model a game-finished event.
GameViewModel
, create a Boolean
MutableLiveData
object called _eventGameFinish
. This object will hold the game-finished event. _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
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
}
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)
}
}
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()
})
eventGameFinish
is set, the associated observer method in the game fragment is called, and the app navigates to the screen fragment. 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)
}
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.
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
.
GameViewModel
, add an onGameFinishComplete()
method to reset the game finished event, _eventGameFinish
./** Method for the game completed event **/
fun onGameFinishComplete() {
_eventGameFinish.value = false
}
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()
}
GameFragment
, inside the gameFinished()
method, uncomment the navigation code.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
.
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
.
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
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
}
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
.
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.
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"
/>
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
ScoreViewModel
, define methods to set and reset the event, _eventPlayAgain
. fun onPlayAgain() {
_eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
_eventPlayAgain.value = false
}
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.
ScoreFragment
, inside onCreateView()
, add a click listener to the PlayAgain button and call viewModel
.onPlayAgain()
.binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }
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
LiveData
is an observable data holder class that is lifecycle-aware, one of the Android Architecture Components.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
.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.
LiveData
, use the setValue()
method on the LiveData
variable.LiveData
inside the ViewModel
should be editable. Outside the ViewModel
, the LiveData
should be readable. This can be implemented using a Kotlin backing property.LiveData
, use private
MutableLiveData
inside the ViewModel
and return a LiveData
backing property outside the ViewModel
. 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. LiveData
observable, attach an observer object to the LiveData
reference in the observers (such as activities and fragments) using the observe()
method. 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:
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.
How do you encapsulate the LiveData
stored in a ViewModel
so that external objects can read data without being able to update it?
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
.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
.private
MutableLiveData
. Use a backing property to expose read-only data of the type LiveData
.ViewModel
object, change the data type of the data to LiveData
. Use a backing property to expose read-only data of the type LiveData
.LiveData
updates a UI controller (such as a fragment) if the UI controller is in which of the following states?
In the LiveData
observer pattern, what's the observable item (what is observed)?
LiveData
objectViewModel
objectStart the next lesson:
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.