1. Welcome
Introduction
In the previous codelabs in this lesson, you improved the code for the GuessTheWord app. The app now uses ViewModel
objects, so app data survives device-configuration changes such as screen rotations and changes to keyboard availability. You also added observable LiveData
, so views are notified automatically when observed data changes.
In this codelab, you continue to work with the GuessTheWord app. You bind views to the ViewModel
classes in the app so that the views in your layout communicate directly with the ViewModel
objects. (Up until now in your app, views have communicated indirectly with the ViewModel
, by way of the app's fragments.) After you integrate data binding with the ViewModel
objects, you no longer need click handlers in the app's fragments, so you remove them.
You also change the GuessTheWord app to use LiveData
as the data-binding source to notify the UI about changes in the data, without using LiveData
observer methods.
What you should already know
- How to create basic Android apps in Kotlin.
- How activity and fragment lifecycles work.
- How to use
ViewModel
objects in your app. - How to store data using
LiveData
in aViewModel
. - How to add observer methods to observe the changes in
LiveData
data.
What you'll learn
- How to use elements of the Data Binding Library.
- How to integrate
ViewModel
with data binding. - How to integrate
LiveData
with data binding. - How to use listener bindings to replace the click listeners in a fragment.
- How to add string formatting to data-binding expressions.
What you'll do
- The views in the GuessTheWord layouts communicate indirectly with
ViewModel
objects, using UI controllers (fragments) to relay information. In this codelab, you bind the app's views toViewModel
objects so that the views communicate directly with theViewModel
objects. - You change the app to use
LiveData
as the data-binding source. After this change, theLiveData
objects notify the UI about changes in the data, and theLiveData
observer methods are no longer needed.
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 integrating data binding with LiveData
in ViewModel
objects. This automates the communication between the views in the layout and the ViewModel
objects, and it lets you simplify your code by using LiveData
.
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 Got It button shows the next word and increases the score by one while the Skip button displays the next word and decreases the score by one. The End Game button ends the game.
- Cycle through all the words, and notice that the app navigates automatically to the score screen.
4. Task: Add ViewModel data binding
In a previous codelab, you used data binding as a type-safe way to access the views in the GuessTheWord app. But the real power of data binding is in doing what the name suggests: binding data directly to the view objects in your app.
Current app architecture
In your app, the views are defined in the XML layout, and the data for those views is held in ViewModel
objects. Between each view and its corresponding ViewModel
is a UI controller, which acts as a relay between them.
For example:
- The Got It button is defined as a
Button
view in thegame_fragment.xml
layout file. - When the user taps the Got It button, a click listener in the
GameFragment
fragment calls the corresponding click listener inGameViewModel
. - The score is updated in the
GameViewModel
.
The Button
view and the GameViewModel
don't communicate directly—they need the click listener that's in the GameFragment
.
ViewModel passed into the data binding
It would be simpler if the views in the layout communicated directly with the data in the ViewModel
objects, without relying on UI controllers as intermediaries.
ViewModel
objects hold all the UI data in the GuessTheWord app. By passing ViewModel
objects into the data binding, you can automate some of the communication between the views and the ViewModel
objects.
In this task, you associate the GameViewModel
and ScoreViewModel
classes with their corresponding XML layouts. You also set up listener bindings to handle click events.
Step 1: Add data binding for the GameViewModel
In this step, you associate GameViewModel
with the corresponding layout file, game_fragment.xml
.
- In the
game_fragment.xml
file, add a data-binding variable of the typeGameViewModel
. If you have errors in Android Studio, clean and rebuild the project.
<layout ...>
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
<androidx.constraintlayout...
- In the
GameFragment
file, pass theGameViewModel
into the data binding.
To do this, assign viewModel
to the binding.gameViewModel
variable, which you declared in the previous step. Put this code inside onCreateView()
, after the viewModel
is initialized. If you have errors in Android Studio, clean and rebuild the project.
// Set the viewmodel for databinding - this allows the bound layout access
// to all the data in the ViewModel
binding.gameViewModel = viewModel
Step 2: Use listener bindings for event handling
Listener bindings are binding expressions that run when events such as onClick()
, onZoomIn()
, or onZoomOut()
are triggered. Listener bindings are written as lambda expressions.
Data binding creates a listener and sets the listener on the view. When the listened-for event happens, the listener evaluates the lambda expression. Listener bindings work with the Android Gradle Plugin version 2.0 or higher. To learn more, read Layouts and binding expressions.
In this step, you replace the click listeners in the GameFragment
with listener bindings in the game_fragment.xml
file.
- In
game_fragment.xml
, add theonClick
attribute to theskip_button
. Define a binding expression and call theonSkip()
method in theGameViewModel
. This binding expression is called a listener binding.
<Button
android:id="@+id/skip_button"
...
android:onClick="@{() -> gameViewModel.onSkip()}"
... />
- Similarly, bind the click event of the
correct_button
to theonCorrect
()
method in theGameViewModel
.
<Button
android:id="@+id/correct_button"
...
android:onClick="@{() -> gameViewModel.onCorrect()}"
... />
- Bind the click event of the
end_game_button
to theonGameFinish
()
method in theGameViewModel
.
<Button
android:id="@+id/end_game_button"
...
android:onClick="@{() -> gameViewModel.onGameFinish()}"
... />
- In
GameFragment
, remove the statements that set the click listeners, and remove the functions that the click listeners call. You no longer need them.
Code to remove:
binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
binding.endGameButton.setOnClickListener { onEndGame() }
/** Methods for buttons presses **/
private fun onSkip() {
viewModel.onSkip()
}
private fun onCorrect() {
viewModel.onCorrect()
}
private fun onEndGame() {
gameFinished()
}
Step 3: Add data binding for the ScoreViewModel
In this step, you associate ScoreViewModel
with the corresponding layout file, score_fragment.xml
.
- In the
score_fragment.xml
file, add a binding variable of the typeScoreViewModel
. This step is similar to what you did forGameViewModel
above.
<layout ...>
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
- In
score_fragment.xml
, add theonClick
attribute to theplay_again_button
. Define a listener binding and call theonPlayAgain()
method in theScoreViewModel
.
<Button
android:id="@+id/play_again_button"
...
android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
... />
- In
ScoreFragment
, insideonCreateView()
, initialize theviewModel
. Then initialize thebinding.scoreViewModel
binding variable.
viewModel = ...
binding.scoreViewModel = viewModel
- In
ScoreFragment
, remove the code that sets the click listener for theplayAgainButton
. If Android Studio shows an error, clean and rebuild the project.Code to remove:
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }
- Run your app. The app should work as before, but now the button views communicate directly with the
ViewModel
objects. The views no longer communicate via the button click handlers inScoreFragment
.
Troubleshooting data-binding error messages
When an app uses data binding, the compilation process generates intermediate classes that are used for the data binding. An app can have errors that Android Studio doesn't detect until you try to compile the app, so you don't see warnings or red code while you're writing the code. But at compile time, you get cryptic errors that come from the generated intermediate classes.
If you get a cryptic error message:
- Look carefully at the message in the Android Studio Build pane. If you see a location that ends in
databinding
, there's an error with data binding. - In the layout XML file, check for errors in
onClick
attributes that use data binding. Look for the function that the lambda expression calls, and make sure that it exists. - In the
<data>
section of the XML, check the spelling of the data-binding variable.
For example, note the misspelling of the function name onCorrect()
in the following attribute value:
android:onClick="@{() -> gameViewModel.onCorrectx()}"
Also note the misspelling of gameViewModel
in the <data>
section of the XML file:
<data>
<variable
name="gameViewModelx"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
Android Studio doesn't detect errors like these until you compile the app, and then the compiler shows an error message such as the following:
error: cannot find symbol import com.example.android.guesstheword.databinding.GameFragmentBindingImpl" symbol: class GameFragmentBindingImpl location: package com.example.android.guesstheword.databinding
5. Task: Add LiveData to data binding
Data binding works well with LiveData
that's used with ViewModel
objects. Now that you've added data binding to the ViewModel
objects, you're ready to incorporate LiveData
.
In this task, you change the GuessTheWord app to use LiveData
as the data-binding source to notify the UI about changes in the data, without using the LiveData
observer methods.
Step 1: Add word LiveData to the game_fragment.xml file
In this step, you bind the current word text view directly to the LiveData
object in the ViewModel
.
- In
game_fragment.xml
, addandroid:text
attribute to theword_text
text view.
Set it to the LiveData
object, word
from the GameViewModel
, using the binding variable, gameViewModel
.
<TextView
android:id="@+id/word_text"
...
android:text="@{gameViewModel.word}"
... />
Notice that you don't have to use word.value
. Instead, you can use the actual LiveData
object. The LiveData
object displays the current value of the word
. If the value of word
is null, the LiveData
object displays an empty string.
- In the
GameFragment
, inonCreateView()
, after initializing thegameViewModel
, set the fragment view as the lifecycle owner of thebinding
variable. This defines the scope of theLiveData
object above, allowing the object to automatically update the views in the layout,game_fragment.xml
.
binding.gameViewModel = ...
// Specify the fragment view as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = viewLifecycleOwner
- In
GameFragment
, remove the observer for theLiveData
word
.
Code to remove:
/** Setting up LiveData observation relationship **/
viewModel.word.observe(viewLifecycleOwner, Observer { newWord ->
binding.wordText.text = newWord
})
- Run your app and play the game. Now the current word is being updated without an observer method in the UI controller.
Step 2: Add score LiveData to the score_fragment.xml file
In this step, you bind the LiveData
score
to the score text view in the score fragment.
- In
score_fragment.xml
, add theandroid:text
attribute to the score text view. AssignscoreViewModel.score
to thetext
attribute. Because thescore
is an integer, convert it to a string usingString.valueOf()
.
<TextView
android:id="@+id/score_text"
...
android:text="@{String.valueOf(scoreViewModel.score)}"
... />
- In
ScoreFragment
, after initializing thescoreViewModel
, set the current activity as the lifecycle owner of thebinding
variable.
binding.scoreViewModel = ...
// Specify the fragment view as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = viewLifecycleOwner
- In
ScoreFragment
, remove the observer for thescore
object.
Code to remove:
// Add observer for score
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- Run your app and play the game. Notice that the score in the score fragment is displayed correctly, without an observer in the score fragment.
Step 3: Add string formatting with data binding
In the layout, you can add string formatting along with data binding. In this task, you format the current word to add quotes around it. You also format the score string to prefix Current Score to it, as shown in the following image.
- In
string.xml
, add the following strings, which you will use to format theword
andscore
text views. The%s
and%d
are the placeholders for the current word and current score.
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
- In
game_fragment.xml
, update thetext
attribute of theword_text
text view to use thequote_format
string resource. Pass ingameViewModel.word
. This passes the current word as an argument to the formatting string.
<TextView
android:id="@+id/word_text"
...
android:text="@{@string/quote_format(gameViewModel.word)}"
... />
- Format the
score
text view similar to theword_text
. In thegame_fragment.xml
, add thetext
attribute to thescore_text
text view. Use the string resourcescore_format
, which takes one numerical argument, represented by the%d
placeholder. Pass in theLiveData
object,score
, as an argument to this formatting string.
<TextView
android:id="@+id/score_text"
...
android:text="@{@string/score_format(gameViewModel.score)}"
... />
- In
GameFragment
class, inside theonCreateView()
method, remove thescore
observer code.
Code to remove:
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- Clean, rebuild, and run your app, then play the game. Notice that the current word and the score are formatted in the game screen.
6. Solution code
Congratulations! You have integrated LiveData
and ViewModel
with data binding in your app. This enables the views in your layout to directly communicate with the ViewModel
, without using click handlers in the fragment. You have also used LiveData
objects as the data binding source to automatically notify the UI about changes in the data, without the LiveData
observer methods.
Android Studio project: GuessTheWord
7. Summary
- The Data Binding Library works seamlessly with Android Architecture Components like
ViewModel
andLiveData
. - The layouts in your app can bind to the data in the Architecture Components, which already help you manage the UI controller's lifecycle and notify about changes in the data.
ViewModel data binding
- You can associate a
ViewModel
with a layout by using data binding. ViewModel
objects hold the UI data. By passingViewModel
objects into the data binding, you can automate some of the communication between the views and theViewModel
objects.
How to associate a ViewModel
with a layout:
- In the layout file, add a data-binding variable of the type
ViewModel
.
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
- In the
GameFragment
file, pass theGameViewModel
into the data binding.
binding.gameViewModel = viewModel
Listener bindings
- Listener bindings are binding expressions in the layout that run when click events such as
onClick()
are triggered. - Listener bindings are written as lambda expressions.
- Using listener bindings, you replace the click listeners in the UI controllers with listener bindings in the layout file.
- Data binding creates a listener and sets the listener on the view.
android:onClick="@{() -> gameViewModel.onSkip()}"
Adding LiveData to data binding
LiveData
objects can be used as a data-binding source to automatically notify the UI about changes in the data.- You can bind the view directly to the
LiveData
object in theViewModel
. When theLiveData
in theViewModel
changes, the views in the layout can be automatically updated, without the observer methods in the UI controllers.
android:text="@{gameViewModel.word}"
- To make the
LiveData
data binding work, set the current activity (the UI controller) as the lifecycle owner of thebinding
variable in the UI controller.
binding.lifecycleOwner = this
String formatting with data binding
- Using data binding, you can format a string resource with placeholders like
%s
for strings and%d
for integers. - To update the
text
attribute of the view, pass in theLiveData
object as an argument to the formatting string.
android:text="@{@string/quote_format(gameViewModel.word)}"
8. Learn more
Udacity course:
Android developer documentation:
9. 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
Which of the following statements is not true about listener bindings?
- Listener bindings are binding expressions that run when an event happens.
- Listener bindings work with all versions of the Android Gradle plugin.
- Listener bindings are written as lambda expressions.
- Listener bindings are similar to method references, but they let you run arbitrary data-binding expressions.
Question 2
Assume your app includes this string resource: <string name="generic_name">Hello %s</string>
Which of the following is the correct syntax for formatting the string, using the data-binding expression?
android:text= "@{@string/generic_name(user.name)}"
android:text= "@{string/generic_name(user.name)}"
android:text= "@{@generic_name(user.name)}"
android:text= "@{@string/generic_name,user.name}"
Question 3
When is a listener-binding expression evaluated and run?
- When the data held by the
LiveData
is changed - When an activity is re-created by a configuration change
- When an event such as
onClick()
occurs - When the activity goes into the background
10. Next codelab
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.