Hard Prerequisites |
|
In this project, 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.
In this task, you locate and run your starter code for the previous project. You can use the GuessTheWord app that you built in previous project as your starter code, or you can download a starter app.
In a previous project, 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:
game_fragment.xml
layout file.The Button view and the GameViewModel don’t communicate directly—they need the click listener that’s in the GameFragment.
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.
In this step, you associate GameViewModel with the corresponding layout file, game_fragment.xml.
<layout ...>
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
<androidx.constraintlayout...
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
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.
<Button
android:id="@+id/skip_button"
...
android:onClick="@{() -> gameViewModel.onSkip()}"
... />
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()}"
... />
<Button
android:id="@+id/end_game_button"
...
android:onClick="@{() -> gameViewModel.onGameFinish()}"
... />
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()
}
In this step, you associate ScoreViewModel with the corresponding layout file, score_fragment.xml.
<layout ...>
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
<Button
android:id="@+id/play_again_button"
...
android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
... />
viewModel = ...
binding.scoreViewModel = viewModel
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 in ScoreFragment.
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 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 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.
In this step, you bind the current word text view directly to the LiveData object in the ViewModel.
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.
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
Code to remove:
/** Setting up LiveData observation relationship **/
viewModel.word.observe(viewLifecycleOwner, Observer { newWord ->
binding.wordText.text = newWord
})
In this step, you bind the LiveData score to the score text view in the score fragment.
<TextView
android:id="@+id/score_text"
...
android:text="@{String.valueOf(scoreViewModel.score)}"
... />
In ScoreFragment, after initializing the scoreViewModel, set the current activity as the lifecycle owner of the binding 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
Code to remove:
// Add observer for score
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
<TextView
android:id="@+id/word_text"
...
android:text="@{@string/quote_format(gameViewModel.word)}"
... />
<TextView
android:id="@+id/score_text"
...
android:text="@{@string/score_format(gameViewModel.score)}"
... />
Code to remove:
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})