Android Kotlin Fundamentals 05.3: Data binding with ViewModel and LiveData

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 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 a ViewModel.
  • 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 to ViewModel objects so that the views communicate directly with the ViewModel objects.
  • You change the app to use LiveData as the data-binding source. After this change, the LiveData objects notify the UI about changes in the data, and the LiveData observer methods are no longer needed.

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

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

  1. (Optional) If you're not using your code from the previous codelab, download the starter code for this codelab. Unzip the code, and open the project in Android Studio.
  2. Run the app and play the game.
  3. Notice that the 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.
  4. Cycle through all the words, and notice that the app navigates automatically to the score screen.

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 the game_fragment.xml layout file.
  • When the user taps the Got It button, a click listener in the GameFragment fragment calls the corresponding click listener in GameViewModel.
  • 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.

  1. In the game_fragment.xml file, add a data-binding variable of the type GameViewModel. 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...
  1. In the GameFragment file, pass the GameViewModel 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.

  1. In game_fragment.xml, add the onClick attribute to the skip_button. Define a binding expression and call the onSkip() method in the GameViewModel. This binding expression is called a listener binding.
<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />
  1. Similarly, bind the click event of the correct_button to the onCorrect() method in the GameViewModel.
<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onCorrect()}"
   ... />
  1. Bind the click event of the end_game_button to the onGameFinish() method in the GameViewModel.
<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />
  1. 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.

  1. In the score_fragment.xml file, add a binding variable of the type ScoreViewModel. This step is similar to what you did for GameViewModel above.
<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
  1. In score_fragment.xml, add the onClick attribute to the play_again_button. Define a listener binding and call the onPlayAgain() method in the ScoreViewModel.
<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />
  1. In ScoreFragment, inside onCreateView(), initialize the viewModel. Then initialize the binding.scoreViewModel binding variable.
viewModel = ...
binding.scoreViewModel = viewModel
  1. In ScoreFragment, remove the code that sets the click listener for the playAgainButton. If Android Studio shows an error, clean and rebuild the project.

Code to remove:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. 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 in ScoreFragment.

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:

  1. 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.
  2. 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.
  3. 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

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.

  1. In game_fragment.xml, add android:text attribute to the word_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.

  1. In the GameFragment, in onCreateView(), after initializing the gameViewModel, set the current activity as the lifecycle owner of the binding variable. This defines the scope of the LiveData object above, allowing the object to automatically update the views in the layout, game_fragment.xml.
binding.gameViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. In GameFragment, remove the observer for the LiveData word.

Code to remove:

/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})
  1. 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.

  1. In score_fragment.xml, add the android:text attribute to the score text view. Assign scoreViewModel.score to the text attribute. Because the score is an integer, convert it to a string using String.valueOf().
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{String.valueOf(scoreViewModel.score)}"
   ... />
  1. In ScoreFragment, after initializing the scoreViewModel, set the current activity as the lifecycle owner of the binding variable.
binding.scoreViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. In ScoreFragment, remove the observer for the score object.

Code to remove:

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. 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.

  1. In string.xml, add the following strings, which you will use to format the word and score 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>
  1. In game_fragment.xml, update the text attribute of the word_text text view to use the quote_format string resource. Pass in gameViewModel.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)}"
   ... />
  1. Format the score text view similar to the word_text. In the game_fragment.xml, add the text attribute to the score_text text view. Use the string resource score_format, which takes one numerical argument, represented by the %d placeholder. Pass in the LiveData object, score, as an argument to this formatting string.
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{@string/score_format(gameViewModel.score)}"
   ... />
  1. In GameFragment class, inside the onCreateView() method, remove the score observer code.

Code to remove:

viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Clean, rebuild, and run your app, then play the game. Notice that the current word and the score are formatted in the game screen.

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

  • The Data Binding Library works seamlessly with Android Architecture Components like ViewModel and LiveData.
  • 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 passing ViewModel objects into the data binding, you can automate some of the communication between the views and the ViewModel 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 the GameViewModel 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 the ViewModel. When the LiveData in the ViewModel 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 the binding 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 the LiveData object as an argument to the formatting string.
 android:text="@{@string/quote_format(gameViewModel.word)}"

Udacity course:

Android developer documentation:

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

Start the next lesson: 5.4: LiveData transformations

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