Data binding and Model-View-ViewModel in Android
In my posts Writing scalable Android applications part 1, part 2, part 3 and part 4 I talked about implementing a model-view-presenter pattern to separate the Android boilerplate from the presentation and application logic. With the addition of a view model I was able to further separate the management of UI state from presentation logic. Although the approach with the MVP pattern and the later addition of the view model concept are interesting approaches at dealing with mobile software architecture, they still involve a lot of boiler plate code. It seems Google took notice and introduced a data binding framework at the Google I/O 2015. I could use this post to explain all the various features of the data binding framework. However you can just head over to Google’s data binding guide and find a great resource for that. I’m more interested in exploring the benefits this framework can bring when addressing architectural issues. Specifically I am interested in learning how the data binding framework can help us implement Data binding and Model-View-ViewModel in Android.
What is MVVM
Mode-View-ViewModel is an architectural pattern on the presentation layer. The idea behind it is that a view’s state is modelled by a corresponding view model. On Android, the MVVM pattern can look something like this:
Each Activity or Fragment has a view model. The view model maintains the state of the view and talks to the business layer. Through a binding mechanism, the changes in the view model’s state are automatically reflected in the view. On Android this binding mechanism is provided by the Android Data Binding library.
Sample Application
I extended the Songster sample application developed in the Writing scalable Android applications posts. For the implementation of the new features, I used the Model-View-ViewModel pattern. I apologize in advance for the length of this post but I want to explore three different aspects when implementing MVVM using the data binding library. The first thing I will discuss is the setup of the MVVM pattern in Android and how view models can be used to model the state of views. The second part will demonstrate the use of the data binding framework and MVVM when working with RecyclerViews. The third and final part will address the interaction of multiple views, each with their own view models, to create a more complex business case. There is going to be a lot of jumping back and forth between view models, xml views, fragments and so on. Therefore I recommend that you download the sample and open it up in Android Studio. The code is available here. So let’s get started!
Settings
The first feature I added to our Songster app was a relatively simple registration workflow. This feature enables the user to create a user account. The account is currently not stored anywhere nor does the app perform any network requests at the time of this post. The whole workflow is mocked and just serves to show the application of the data binding framework and the MVVM pattern.
In the presentation layer we have an Activity called SettingsActivity, a UserModel and a SettingsViewModel. In the data layer we have a corresponding repository. Let’s take a look at the SettingsActivity and how the SettingsViewModel is used to model the state of the activity’s view. The first thing we notice when we open the SettingsViewModel is a list of properties.
@Bindable private boolean mUserRegistered; @Bindable private boolean loading; @Bindable private boolean editing; @Bindable private UserModel user; @Bindable private boolean settingsUpdated; @Bindable private boolean settingsUpdateSuccessful; @Bindable private String settingsUpdateMessage; @Bindable private String mUsernameError; @Bindable private String mPasswordError;
A lot of the properties are boolean properties. A view can always be thought of as traversing a state machine. At any given point in time, the view is in a certain state. The booleans at the top of the SettingsViewModel class model these various states. When we scroll down to the onResume() method we see that the initial state of the view model is that its loading boolean is set to true:
public void onResume(){ setLoading(true); mRepository.start(); mRepository.loadUserSettings(new LoadedUserSettingsHandler()); }
Let’s jump over to the corresponding view XML file, the content_settings.xml file. Notice the ProgressBar at the very bottom of the xml file:
<ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}" android:layout_centerInParent="true"/>
The value of the visibility property contains the first data binding expression. It states that the progress bar should be visible if the viewModel’s loading property is true. In this data binding expression, the viewModel is a variable declared at the top of the xml file:
<data> <variable name="viewModel" type="com.samples.songster.presentation.settings.SettingsViewModel" /> <import type="android.view.View" /> </data>
Notice that the class of the view model that we bind to here is the SettingsViewModel. Each view only has one corresponding view model type. In the Songster App we follow the convention that the name of the view model is always ViewNameViewModel. So for the SettingsActivity we have a view model named SettingsViewModel. This view model is thus referenced in the SettingsActivity’s corresponding layout xml file.
In addition to declaring the view model variable, we also import Android’s View class. This allows us to call properties on the View class from within our data binding expressions.
If we go back to the view model’s onResume method, we see that after the view model’s loading property is set to true, the user’s settings are loaded from the repository:
mRepository.loadUserSettings(new LoadedUserSettingsHandler());
The corresponding listener looks like this:
private class LoadedUserSettingsHandler extends SettingsRepository.SettingsRepositoryListenerAdapter { @Override public void onLoadedUserSettings(UserModel userModel) { setUser(userModel); setLoading(false); if(userModel.getUsername() != null){ setUserRegistered(true); } else { setUserRegistered(false); } } }
After we set the model’s user to the user loaded from the repository, we set the loading property to false. That is all that is necessary to make the progress bar in the view disappear. There is no need to keep an instance of the ProgressBar in the fragment and to tell the fragment to hide the progress bar. In fact, we don’t have to interact with the fragment at all. We simply change the state of our view model and the data binding framework does the rest! Well almost. We do need to do a few small things. Let’s take a look at the setLoading() method of our SettingsViewModel:
public void setLoading(boolean loading) { this.loading = loading; notifyPropertyChanged(BR.loading); }
The call to notifyPropertyChanged() causes the ProgressBar to be updated to reflect the value of the data bound property. This method is a member of the BaseObservable class so we have to extend this class in our view model:
public class SettingsViewModel extends BaseObservable {
Finally we need to annotate our loading property with the @Bindable annotation:
@Bindable private boolean loading;
Now the ProgressBar has the ability to observe our view model’s loading property and can react to changes in the property’s value. There are multiple ways to have a view observe a view model’s property. See Google’s Databinding Guide for more information on this. This is just the way I chose to implement it.
The settings view, though simple, already reflects the power of data binding. It allows us to create a view model which truly models the view’s state without having to write cumbersome boilerplate code to wire up our model with the view. Feel free to inspect the rest of the SettingsActivity and its associated view model and view xml to learn more about the application of the data binding framework and MVVM pattern. Keep in mind that Android’s data binding framework should not be abused to move complex logic into the view’s xml file. The xml of the SettingsActivity file might already contain too much logic but the sample also serves to demonstrate the features of the data binding library.
My List
Our Songster app allows users to bookmark songs they have searched for. However, up until this post it did not allow the user to actually view those bookmarks. I went ahead and added this feature with the help of the data binding library and the MVVM pattern. In the scope of this feature we will see another great benefit of Android’s data binding framework: the vast reduction in boilerplate when dealing with RecyclerViews. Please also note that a lot of the code for this feature has been mocked. Songs bookmarked in the search screen are not actually stored and displayed in the user’s list of bookmarks. Instead, mock data is provided by the various repositories. Feel free to checkout the code and modify the sample!
Let’s start by looking at our view model, the MyListViewModel. The view model is always a good starting point when dealing with a new feature since it models the state and behaviour of the view. When we look at the onResume() method, we notice that some sort of info message is displayed if we don’t have any bookmarked items:
public void onResume(){ //Code omitted for brevity //... if(mMyItems == null || mMyItems.size() == 0){ setLoading(false); setDisplayInfoMessage(true); } }
Let’s open the corresponding view xml file, fragment_mylist.xml. Right below the declaration of the RecyclerView we see the following LinearLayout:
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" android:orientation="vertical" android:gravity="center_horizontal" android:visibility="@{viewModel.displayInfoMessage ? View.VISIBLE : View.GONE}"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal" android:text="@string/label.mylist.info"/> <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_refresh_black_24dp" android:background="@null" android:padding="@dimen/space.small" android:onClick="@{viewModel.onClickRefresh}"/> </LinearLayout>
This LinearLayout is visible if the viewModel’s displayInfoMessage property is true. The LinearLayout also contains an ImageButton that displays a refresh button. Its onClick attribute is bound to the viewModel’s onClickRefresh method. Android’s data binding framework not only allows us to propagate state changes from the model to the view but it also allows us to notify the model when the corresponding view is interacted with. In our view model we have our onClickRefresh() method:
public void onClickRefresh(View view){ setLoading(true); setDisplayInfoMessage(false); mRepository.loadMyList(new LoadedMyListHandler()); }
In order for the onClickRefresh() method to be called, it has to have the signature above. Within the method we change the view model’s loading property to true, its displayInfoMessage property to false and load the user’s bookmarked songs from the repository. As soon as the repository has loaded our bookmarks, it gets interesting:
public class LoadedMyListHandler implements MyListRepository.MyListRepositoryListener, MyListItemModel.MyListItemModelListener { @Override public void onLoadedMyList(List<SongDto> songs) { setLoading(false); //Build item models using the retrieved songs //Add a header item MyListItemModel headerItem = new MyListItemModel(this); headerItem.setItemType(MyListItemModel.ItemType.HEADER); headerItem.setTitle("My Songs"); mMyItems.add(headerItem); //Add items for each song for(SongDto song : songs){ MyListItemModel songItem = new MyListItemModel(this); songItem.setSong(song); songItem.setItemType(MyListItemModel.ItemType.RESULT); mMyItems.add(songItem); } mView.updateRecyclerView(); } //Rest omitted for brevity… }
As soon as our bookmarks have been loaded, we set the view model’s loading property to false. Afterwards we display the loaded bookmarks in our RecyclerView. We do this by creating a list of MyListItemModels. Each one of the items in that list represents an entry in the RecyclerView. The first item is a header which displays a title and the remaining items are the rows used to display the songs. This list is maintained directly by our view model’s mMyItems property. At the end we tell our view to update the RecyclerView. At this point you might stop and wonder: ‘Why do we need to tell the view to update the UI? Isn’t that what data binding is supposed to do for us?’ That is a legitimate question but in order to answer it, we need to look at how data binding works with RecyclerViews and their corresponding adapters. Let’s go on over to our Fragment, the MyListFragment. Take a look at the configureRecyclerView() method which is called after the Fragment’s Activity has been created:
private void configureRecyclerView() { mRecyclerView.setHasFixedSize(true); mLayoutManager = new LinearLayoutManager(getActivity()); mRecyclerView.setLayoutManager(mLayoutManager); mMyListAdapter = new MyListAdapter(mViewModel.getMyItems()); mRecyclerView.setAdapter(mMyListAdapter); }
The things that take place in this method are pretty straight forward, standard RecyclerView boilerplate. Interesting is the line
mMyListAdapter = new MyListAdapter(mViewModel.getMyItems());
Here we instantiate our RecyclerView’s adapter and pass it our view model’s list of MyListItemModels. Let’s look at our adapter, the MyListAdapter. The first major method that is called by Android when dealing with our adapter is the onCreateViewHolder() method:
@Override public BindingViewHolder<ViewDataBinding> onCreateViewHolder(ViewGroup parent, int viewType) { ViewDataBinding binding; MyListItemModel.ItemType itemType = MyListItemModel. ItemType.values()[viewType]; switch (itemType) { case HEADER: binding = DataBindingUtil.inflate( LayoutInflater.from(parent.getContext()), R.layout.item_mylist_header, parent, false); break; default: binding = DataBindingUtil.inflate( LayoutInflater.from(parent.getContext()), R.layout.item_mylist_result, parent, false); break; } return new BindingViewHolder<>(binding); }
This method is called for every item in our list. Remember that our list of MyListItemModels contains two types of items: a single header item and a slew of song items. We have a separate layout xml file for each of these two types of items. For our header item we use our item_mylist_header.xml and for our song items we use the item_mylist_result.xml. We now need to create data bindings that are compatible with those views. We do this by calling the DataBindingUtil class’ inflate() method. This will return a ViewDataBinding object which we can pass to the ViewHolder that we are creating.
Once our view holders have been created, Android calls our adapter’s onBindViewHolder() method. This method is also called for each item in our list of MyListItemModels. The onBindViewHolder() method is defined in our adapter’s superclass, the BindingAdapter class:
@Override public void onBindViewHolder(BindingViewHolder<T> holder, int position) { T binding = holder.getBinding(); updateBinding(binding, position); }
In onBindViewHolder() we retrieve the holder’s data binding and update it:
@Override protected void updateBinding(ViewDataBinding binding, int position) { MyListItemModel item = mItems.get(position); switch (item.getItemType()) { case HEADER: ((ItemMylistHeaderBinding) binding).setItem(mItems.get(position)); break; default: ((ItemMylistResultBinding) binding).setItem(mItems.get(position)); break; } }
That’s it! That’s all that is needed to update our RecyclerView. We don’t need to bind to the Views of our layout files and update them. We simply pass our model object to the corresponding binding and the data binding framework does the rest! Let’s take a look at one of the layout files, the item_mylist_header.xml file. This file contains one TextView that is bound to the MyListItemModel’s title property:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{item.title}" tools:text="Item Text" style="@style/Widget.TextView.Bold.Medium.Dark"/>
The declaration of our data model is at the top of the file and looks as follows:
<data> <import type="com.samples.songster.presentation.mylist.MyListItemModel" /> <variable name="item" type="MyListItemModel" /> </data>
We declare our item as being of type MyListItemModel. The item we pass into our binding with the setItem() in our updateBinding() method is also of type MyListItemModel:
((ItemMylistHeaderBinding) binding).setItem(mItems.get(position));
This is all that’s needed and the data binding framework wires up the rest. The data binding framework greatly reduces the boilerplate in our RecyclerView’s adapter. But back to our question: Why do we need to tell the view to update the RecyclerView? The reason is because our RecyclerView is not directly bound to our data model. The RecyclerView’s adapter is bound to the data model. To my knowledge there is no way to associate the adapter with the RecyclerView in the view xml. Furthermore, simply updating the list of MyListItemModels doesn’t suffice when adding items to the RecyclerView’s contents, even with the data binding framework. We still have to call notifyDataSetChanged() or one of its variants. Nevertheless, the data binding framework takes care of a lot of the boilerplate when dealing with RecyclerViews.
Purchasing a song
From our list of bookmarked songs we also have the option to buy individual songs. We do this by clicking on the Buy Button next to one of the songs. We use the item_mylist_result.xml file to define the layout for each song item in our RecyclerView. In the xml file we have the following declaration for our Buy Button:
<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/label.buy" android:visibility="@{item.beingPurchased || item.purchased ? View.GONE : View.VISIBLE}" android:onClick="@{item.onClickBuy}"/>
There are two data binding expressions within this declaration. The first toggles the visibility of the Button based on whether a song is currently in the process of being purchased and if it has already been purchased. The second expression attaches an onClick handler. The handler is attached to the item model’s onClickBuy method:
public void onClickBuy(View view){ mListener.onBuyItem(this); }
Each item in the list of songs is of type MyListItemModel so this method is defined within that class. Within the onClickBuy() method we call our listener’s onBuyItem() method and pass the item to it. The listener of the MyListItemModel is the class that creates the MyListItemModels. In this case it’s the handler that gets called when the list of bookmarked songs has been loaded:
public class LoadedMyListHandler implements MyListRepository.MyListRepositoryListener, MyListItemModel.MyListItemModelListener { //Omitted for brevity… @Override public void onBuyItem(MyListItemModel item) { if(mItemBeingPurchased == null) { mItemBeingPurchased = item; if (item.getItemType() == MyListItemModel.ItemType.RESULT) { item.setBeingPurchased(true); mUseCase.purchaseSong(item.getSong()); } } } }
The LoadedMyListHandler is defined within our MyListViewModel. Our purchase workflow is fairly complex so we want to make sure that the user can only initiate a purchase workflow for one song at a time. We then check whether the item the user is trying to buy is actually a song. Then we let the song know that it’s currently being purchased. Finally we start our purchase workflow. For some of the songs it is required that we are logged in with our Songster account. If that’s the case and we are not logged in yet, the following method will be called in our MyListViewModel:
@Override public void showLoginView(SongDto songDto) { mView.showLoginView(); }
This causes a DialogFragment to be shown with a login mask called LoginFragment. Like our other Fragments, the LoginFragment is also backed by a view model. The view model manages the state and data of the view. Once the user has successfully logged in, the LoginViewModel notifies its view, the LoginFragment. The LoginFragment then notifies its listener, the MyListFragment:
@Override public void onLoggedIn(UserDto user) { LoginFragment loginFragment = (LoginFragment) getFragmentManager(). findFragmentByTag(TAG_LOGIN_FRAGMENT); if(loginFragment != null){ loginFragment.dismiss(); } mViewModel.onLoggedIn(user); Toast.makeText(getActivity(), "Logged in as " + user.getUsername(), Toast.LENGTH_LONG).show(); }
The MyListFragment dismisses the LoginFragment and notifies the MyListViewModel that the user has logged in successfully. The purchase workflow then continues until the song has finally been purchased. Once the song has been purchased, the MyListViewModel is updated accordingly:
@Override public void showPurchaseSuccsessMessage(SongDto song) { mItemBeingPurchased.setPurchased(true); mItemBeingPurchased.setBeingPurchased(false); mItemBeingPurchased = null; }
Once purchased, we mark the song that the user bought as having been purchased.
Conclusion
Android’s data binding framework is a great asset when tackling software architecture on Android. It allows us to use patterns such as the Model-View-ViewModel pattern and frees us from a lot of Android boilerplate code. The version of the library along with the version of Android Studio that I used to develop my sample have worked fairly well. However, there are still a few issues with the tools. A lot of times I found that the IDE would complain about binding classes that have not been generated yet. Some data binding expressions were also marked red in the xml files. These issues never actually prevented me from building, running or debugging the app though so they were not show stoppers. It is important to note that data binding is a very complex topic. As with all convenient and powerful frameworks, there is a tradeoff between control and comfort. You give up some of the control of how the data is bound to the views in exchange for not having to reference the views in the java code and having to update the data manually. In my opinion, the benefits already far outweigh the costs. Especially considering that you can apply the data binding framework on a per-view basis. I hope the toolset continues to improve and that Google continues to provide additional powerful tools that enable us to create more powerful and scalable architecture on Android.
References
http://developer.android.com/tools/data-binding/guide.html
https://realm.io/news/data-binding-android-boyar-mount/