11.
MVVM Sample with Data Binding
Written by Aldo Olivares
In the last chapter, you learned how to implement the MVVM architecture by rebuilding WeWatch using Google’s Architecture Components such as LiveData and ViewModel. In this chapter, you’ll learn how to further improve your app’s architecture by using the Data Binding library to decouple the XML layouts from the activities.
Along the way you’ll learn:
- How to use data binding with XML layouts.
- How to use data binding with RecyclerView adapters.
- How to implement one-way data binding.
- How to implement two-way data binding.
- How to create observable fields.
- How to use data binding to observe ViewModels.
What is data binding?
Before implementing data binding in your Android projects you first need to understand what data binding can do for you. According to the official documentation developer.android.com/topic/libraries/data-binding:
The Data Binding Library is a support library that allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically.
Simply put, data binding lets you display the values of variables or properties inside your XML layouts such as ConstraintLayouts or RecyclerViews.
Typically (and without data binding), when you want to change or display a value inside your XML layout, you first need to get a reference to the View by using findViewById(). Once you have that, you can apply your changes:
findViewById<TextView>(R.id.text_view).apply {
text = viewModel.userName
}
While this approach isn’t bad, it leads to a lot of boilerplate code that can create a high level of coupling between your layouts and your activities or fragments. Developers are usually forced to create many set up methods that get called immediately after the initial creation of their Views. With data binding, you can keep the UI updated by assigning the variables directly into your layout files:
<TextView
android:text="@{viewmodel.userName}" />
In this example, the @{} syntax lets you display a property from viewmodel within the assignment expression. We’ll refer to this syntax as the One-way data binding syntax. This approach helps you remove boilerplate code from your activities and fragments. And because you can assign default values, it also prevents memory leaks and NullPointerExceptions.
In the next few sections, you’ll learn how to use data binding in the WeWatch app from the previous chapter.
Getting Started
Start by opening the starter project for this chapter. You can also use your own project from the previous chapter.
If you haven’t done so already, take some time to familiarize yourself with the code, paying special attention to the classes inside viewmodel and view.
Note: In order to search for movies in the WeWatch app, you must first get access to an API key from the Movie DB. To get your API own key, sign up for an account at www.themoviedb.org. Then, navigate to your account settings on the website, view your settings for the API, and register for a developer API key. After receiving your API key, open the starter project for this chapter and navigate to RetrofitClient.kt. There, you can replace the existing value for
API_KEYwith your own.
Build and run the app to see it in action.
Great! All is well. Now it’s time to implement data binding!
Implementing data binding
By default, data binding is not enabled. To use the Data Binding library, open build.gradle and add the following lines inside the Android block:
dataBinding {
enabled = true
}
Click Sync Now, and wait until Android Studio finishes syncing your project.
Note: Because the Data Binding library is a relatively new library, you should download the latest Android Plugin for Gradle and make sure you’re using Android Studio 3.3 or higher.
In WeWatch, there are three activities that use different XML layouts which can take advantage of data binding:
-
MainActivity: Consists of activity_main.xml, which contains a
RecyclerViewthat displays a list of your favorite movies retrieved from the Room database. -
AddMovieActivity: Consists of activity_add.xml, which contains a
ConstraintLayoutthat communicates withAddViewModel. -
SearchMovieActivity: Similar to
MainActivity, it contains aRecyclerViewthat displays a list of movies retrieved from the TMDB API.
In this chapter, you’ll implement both one-way data binding and two-way data binding. You’ll start with MainActivity, which is the perfect place to learn about one-way data binding. You’ll then move on to two-way data binding in AddMovieActivity. Once you’re done with that, you’ll add data binding to SearchMovieActivity during the challenge as it’s quite similar to MainActivity.
Adding data binding to MainActivity
There are four steps to implement data binding in your Views:
- Convert your regular layouts into data binding layouts.
- Add a
datatag with variables bound to your data source. - Use binding expressions to handle events emitted by your Views.
- Bind your data source to your XML layouts.
Because MainActivity contains a RecyclerView, you need to work directly with the layout that your RecyclerView is using to display a movie item; in this case, item_movie_main.xml.
Open item_movie_main.xml and select the root CardView. Click Alt-Enter and select Convert to data binding layout:
After selecting the option, your layout should look as follows:
Effectively, Android Studio wrapped the root CarView element in a new layout element and added a data element (which you’ll learn more about in the next section). This completes Step 1.
Data binding layouts start with a root tag of layout, followed by a data element. For the data element, you need to specify your own data source, which is usually a ViewModel, but can sometimes be a Model. The data source for this layout is the movie object returned by MovieListAdapter.
To add it to your layout, add the following variable tag inside the data element:
<data>
<variable
name="movie"
type="com.raywenderlich.wewatch.data.model.Movie"/>
</data>
The name property of the variable element is how you’ll call the data source inside the layout. The type property indicates the class that will be used; in this case, Movie. You can now use the expression language to display the data using the object inside the layout using the @{} syntax.
Scroll down to the TextView with an ID of movieTitleTextView. Replace the text assignment with the following:
android:text="@{movie.title}"
This displays the title of the movie object that was passed in as a parameter.
Scroll down to movieReleaseDateTextView and add the following text assignment:
android:text="@{movie.releaseDate}"
This displays the releaseDate of the movie object that was passed as a parameter.
The first three steps are complete, so it’s on to the fourth: binding the data source to this layout.
You typically want to bind your data source inside the Activity or Fragment that controls the layout, which in this case is MainActivity. However, since you’re working with a Recyclerview, the actual class that controls the list of movie items shown is MovieListAdapter.
Inside view/adapter, open MovieListAdapter.kt. Remove the body inside the MovieHolder inner class. and modify the constructor parameters to match this:
inner class MovieHolder(val binding: ItemMovieMainBinding) : RecyclerView.ViewHolder(binding.root)
MovieHolder is now expecting an ItemMovieMainBinding as a constructor parameter.
Wait a minute, you didn’t create an ItemMovieMainBinding class!
That’s what’s great about the Data Binding library — it automatically generates the classes required to bind your layouts with your data objects. Here’s how it works:
- A single class is generated for each layout file.
- Using the name of the layout file, the library names this new class using Pascal Case and the “Binding” suffix.
For example, if your layout’s name is item_movie_main.xml, the corresponding class it generates is ItemMovieMainBinding.
You’re almost ready to use the generated class to manage the information that’s displayed in your RecyclerView. But first, you need to bind it to the View.
Modify onCreateViewHolder() to match this:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ItemMovieMainBinding>(layoutInflater, R.layout.item_movie_main, parent, false)
return MovieHolder(binding)
}
Here, instead of calling the traditional LayoutInflater, you’re using DatabindingUtil, which allows you to inflate your View and bind ItemMovieObject to it by calling inflate().
Note: You should create your binding objects as soon as your layouts are inflated. With an Activity, this means adding some code to
onCreate(); With an Adapter, you need to add code toonCreateViewHolder().
Now that ItemMovieMainBinding is bound to the layout, you only need to tell the object which movie to display using onBindViewHolder().
Modify onBindViewHolder() method to match this:
//1
override fun onBindViewHolder(holder: MovieHolder, position: Int) {
//2
val movie = movies[position]
//3
holder.binding.movie = movie
//4
holder.binding.checkbox.setOnCheckedChangeListener{ checkbox, isChecked ->
if (!selectedMovies.contains(movie) && isChecked) {
selectedMovies.add(movies[position])
}else{
selectedMovies.remove(movies[position])
}
}
//5
holder.binding.checkbox.isChecked = selectedMovies.contains(movie)
}
Here’s what’s happening:
- Similar to any regular
onBindHolder(), this method takesholderand the currentpositionin the list. - You get a movie object from the list of movies that should be displayed.
- Here’s where the magic happens. Remember the
movievariable that you created inside item_movie_main.xml? This line tells the layout which movie object is going to be bound to your layout and therefore in all your assignment expressions, such as:android:text="@{movie.title}". - This method sets the listener needed to add the watched movies to your list in case they need to be deleted.
- Finally, this line assigns the correct state to your checkbox to either true or false.
The only step left is to display the appropriate thumbnail in the list using BindingAdapter.
Inside the root package, open Extensions.kt and add the following methods:
@BindingAdapter("imageUrl")
fun ImageView.setImageUrl(url: String?) {
Picasso.get().load(url).into(this)
}
@BindingAdapter("imageUrl")
fun ImageView.setImageUrl(int: Int) {
this.setImageDrawable(resources.getDrawable(int,null))
}
BindingAdapter objects are useful for changing the way the traditional bindings behave because they override the traditional adapters provided by the Android Framework. In this case, you’re using the power of Kotlin’s extension functions to call setImageUrl() on any ImageView to load a specific image using Picasso. The first method loads an image from a URL and the second from a drawable resource.
Now, in MovieListAdapter.kt, you need to call setImageUrl(), which you’ve just extended inside onBindViewHolder():
if (movie.posterPath != null) {
holder.binding.movieImageView.setImageUrl(
RetrofitClient.TMDB_IMAGEURL + movie.posterPath)
} else {
holder.binding.movieImageView.setImageUrl(
R.drawable.ic_local_movies_gray)
}
That’s it! MainActivity is ready for use.
Build and run the app to verify that MainActivity is still working properly.
Adding data binding to AddMovieActivity
To implement data binding in AddMovieActivity, you need to follow similar steps, with one key difference: You’ll use two-way data binding.
One-way data binding lets you set a value in one of your layout’s attributes, but you can also react to a change in that attribute by setting a listener. In the snippet below, you can see that onCheckedChanged attribute is set with a callback to watch for changes to the Checkbox:
<CheckBox
android:id="@+id/checkbox"
android:checked="@{viewmodel.watched}"
android:onCheckedChanged="@{viewmodel.watchedChanged}"
/>
Similarly, Two-way data binding, lets you consolidate setting those two attributes by allowing you to set values and react to changes at the same time:
<CheckBox
android:id="@+id/checkbox"
android:checked="@={viewmodel.watched}"
/>
Using @={} syntax, there’s no need to override onCheckedChanged() anymore. We’ll refer to this syntax as the Two-way data binding syntax. It’s worth noting the tiny little = that separates this syntax from the one way syntax.
You’ll use two-way data binding to get and set the movie title and movie release date of the AddViewModel using the values entered by the user into the text fields.
Open activity_add.xml and convert the layout into a data binding layout by selecting ConstraintLayout, and then clicking Alt-Enter. When prompted, select Convert to data binding layout.
Add the following variable inside the data tag:
<variable
name="viewModel"
type="com.raywenderlich.wewatch.viewmodel.AddViewModel"/>
In this case, you create a viewModel variable that’s bound to AddViewModel.
Next, you need to create the appropriate properties in AddViewModel that will get updated whatever the user types into the EditText fields. To create these properties, you have two options:
- Wrap
AddViewModelproperties inObservableField. - Make
AddViewModelextend fromBaseObservable, which can handle all of the properties at once.
Because AddViewModel already extends the architecture component ViewModel, you cannot also extend from BaseObservable. Instead, you’ll use an ObservableField on any properties that need updating using data binding.
Inside viewmodel, open AddViewModel.kt and add the following properties:
var title = ObservableField<String>("")
var releaseDate = ObservableField<String>("")
This creates two ObservableField properties, both of type String.
Finally, delete the old saveMovie() and replace it with the following code:
//1
private val saveLiveData = MutableLiveData<Boolean>()
//2
fun getSaveLiveData(): LiveData<Boolean> = saveLiveData
//3
fun saveMovie() {
if (canSaveMovie()) {
repository.saveMovie(Movie(title = title.get(), releaseDate = releaseDate.get()))
saveLiveData.postValue(true)
} else {
saveLiveData.postValue(false)
}
}
//4
fun canSaveMovie(): Boolean {
val title = this.title.get()
title?.let {
return title.isNotEmpty()
}
return false
}
Here’s what’s going on:
-
saveLiveDatais the property you’ll use to signal your Activity if a movie has been saved or not. -
getSaveLiveData()returns aLiveDataboolean that’s updated with the values insaveLiveData. -
saveMovie()usessaveMovie()of your repository to save a movie in the database using thetitleandreleaseDatethat the user inserts. If the movie successfully saves,saveLiveDataistrue, otherwise it’sfalse. - Since
titleandreleaseDateare automatically updated with whatever the user types into theEditTextfields, it’s now the responsibility ofviewModel(and not the Activity) to verify that the title is not empty.
With the properties ready, you can now use them inside the layout.
Open activity_add.xml, scroll down to the EditText element with an ID of titleEditText, and add the following property:
android:text="@={viewModel.title}"
Now, scroll down to the second EditText element, identified with yearEditText, and add the following property:
android:text="@={viewModel.releaseDate}"
It’s essential to use two-way binding syntax (@={}) whenever you need two-way data binding as it keeps your Observable fields up-to-date. If you use the one-way syntax (@{}), the title and releaseDate properties in AddViewModel won’t get updated.
Finally, scroll down to addMovieButton and replace the android:onClick attribute with the following:
android:onClick="@{()-> viewModel.saveMovie()}"
Not only is data binding used to display or update properties, you can also use it to call methods in your data source. In this case, when the user taps the button, you call saveMovie() of viewModel.
You’re nearly done; you just have to create a binding object once the layout is inflated. For that, you need to modify AddMovieActivity.
Open AddMovieActivity.kt and modify onCreate():
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//1
val binding = DataBindingUtil.setContentView<ActivityAddBinding>(this, R.layout.activity_add)
//2
viewModel = ViewModelProviders.of(this).get(AddViewModel::class.java)
//3
binding.viewModel = viewModel
}
Here’s what the code does:
- Similar to what you did for
MovieListAdapter, you useDatabindingUtilto inflate the layout and get a reference tobinding. - This creates a reference to
AddViewModel. - This assigns
viewModeltobinding.
Since you now call saveMovie() of viewModel directly from the layout (thanks to data binding!), you can now delete addMovieClicked() from the Activity.
There’s one last piece of plumbing left. You need to add an observer to the ViewModel when a movie gets saved. Add the following method:
private fun configureLiveDataObservers() {
viewModel.getSaveLiveData().observe(this, Observer { saved ->
saved?.let {
if (saved) {
finish()
} else {
showMessage(getString(R.string.enter_title))
}
}
})
}
This code sets up an observer on saveLiveData of viewModel to close the the activity. If the movie hasn’t been saved, it displays a message to the user indicating that the title cannot be empty.
Finally, call configureLiveDataObservers() as soon as your Activity is created by adding the following code to the end of onCreate():
configureLiveDataObservers()
That’s it!
Build and run your app, and try adding some movies to see it in action:
Challenge
You now know how to use data binding to improve your MVVM architecture. It’s time to put that knowledge into practice by refactoring SearchMovieActivity.
Your mission, should you choose to accept it: Change the item_movie_search.xml layout to use data binding and make SearchAdapter add a Movie as a data source for the layout as you did for item_movie_main.xml.
This challenge uses the steps as the ones you followed in the Adding data binding to MainActivity section of this chapter:
- Convert the regular layouts into data binding layouts.
- Add a data tag with variables bound to your data source.
- Use binding expressions to handle events emitted by your Views.
- Bind your data source to your XML layouts.
If you get stuck, review the challenge folder included with this chapter. But remember… practice makes perfect, so do your best to complete this challenge on your own.
Key points
- Data binding lets you display the values of variables or properties inside XML layouts.
- Data binding is not enabled by default; you need to activate it in the app-level build.gradle.
- Two-way data binding lets you set values and react to changes at the same time.
- The two-way binding syntax
@={}, lets you update the appropriate values in theObservableFields. - The one-way binding syntax
@{}, lets you display a certain property from theviewmodelin the assignment expression.
Where to go from here?
The Data Binding library works well with other Android Architecture Components such as the ViewModel. Also, data binding makes your code easy-to-read and maintain by providing a reliable way to bind your XML Layouts, thus reducing boilerplate.
In this chapter, you only learned the basics of data binding. If you want to learn more, check out these great resources:
-
The official Android documentation: https://developer.android.com/topic/libraries/data-binding/.
-
The expression language documentation: https://developer.android.com/topic/libraries/data-binding/expressions where you can find very interesting operators.
-
This MVVM video course: https://www.raywenderlich.com/8984-mvvm-on-android.