Google I/O 2015 has come and gone now, only leaving in its tracks one developer tool that really gets me excited.
We saw an array of nice incremental improvements announced. Like Android M and its various user-centric features, NDK (C/C++) support in Android Studio (if you’re into that kinda thing), image generation from vector files, heap analysis, improved theme and layout editors, Gradle performance improvements, etc. I am pleased we finally have a Design Support Library so we can implement the Material Design UI patterns we’ve been guided toward for about a year now. But most of these things were already being done in one form or another by leveraging community tools and libraries.
One thing however that the community’s been craving, but hasn’t come to a good set of patterns or tools on, is how to improve the code that coordinates between the model and the views inside our projects. Until now, Activities and Fragments have typically contained a ton of fragile, untestable and uninteresting code to work with views. But that all changes with the Data Binding Library.
Goals and Benefits
We should all be interested in this library because it will allow us to be more declarative in the way we work with our views. Going declarative should help remove a lot of the code that’s not very fun to write, and along with it, a lot of pesky UI orchestration bugs that result. Less code means less bugs, right? Right.
Another big goal of mine and something the community needs, is lower friction unit testing for our view and application logic. It’s always been possible to have tests here, but it’s been so hard and required so much additional work that a lot of us (not me of course) just skip right over them. This is our opportunity to do better.
MVVM
In the official docs for this library, they give you an example of directly binding a domain entity properties from User to attributes in the layout. And they give you the idea that you can do fancy things like this:
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
But let’s be really clear here: I don’t recommend binding directly to domain entities or putting logic into those bindings in the layout file. If you do either of those things, it will make it harder to test your view logic and harder to debug. What we’re after is the opposite: easier to test and debug.
That’s where the MVVM pattern comes into the picture. To over-simplify things, this will decouple our Views from the Model by introducing a ViewModel layer in between that binds to the View and reacts to events. This ViewModel will be a POJO and contain all the logic for our view, making it easy to test and debug. With this pattern, the binding will only be a one-to-one mapping from the result of a ViewModel method into the setter of that View property. Again, this makes testing and debugging our view logic easy and possible inside of a JUnit test.
Project Setup
Let’s get to it then. NOTE: I’ll probably skip over some useful information here in the interest of brevity, so I recommend referencing the official docs.
Start off by adding these dependencies to your project’s build.gradle:
classpath 'com.android.databinding:dataBinder:1.0-rc4'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
These dependencies must be added to project level build.gradle complete file content:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0-beta2'
classpath 'com.android.databinding:dataBinder:1.0-rc4'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
mavenCentral()
}
}
And then add this to your app module’s build.gradle:
apply plugin: 'com.android.application'
apply plugin: 'com.android.databinding'
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
apt 'com.android.databinding:compiler:1.0-rc4'
}
NOTE: if you have any provided dependencies like dagger-compiler, you will now need to change the provided keyword to apt to prevent them from being added to your classpath and if you’r using kotlin like me you need to change apt to kapt as follow:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar','*.so'])
compile 'com.android.support:design:23.1.0'
compile 'com.android.support:appcompat-v7:23.1.0'
compile 'com.android.support:cardview-v7:23.1.0'
compile 'com.android.support:recyclerview-v7:23.1.0'
compile 'com.android.support:support-v4:23.1.0'
compile 'com.google.code.findbugs:jsr305:1.3.9'
compile 'eu.chainfire:libsuperuser:1.0.0.+'
compile 'io.reactivex:rxandroid:1.0.1'
compile 'io.reactivex:rxjava:1.0.16'
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile 'com.android.support:support-annotations:23.1.0'
// Dagger 2
compile 'com.google.dagger:dagger:2.0.2'
compile 'javax.inject:javax.inject:1'
compile 'javax.annotation:javax.annotation-api:1.2'
apt 'com.google.dagger:dagger-compiler:2.0.2'
provided "com.google.dagger:dagger-compiler:2.0.2"
provided 'javax.annotation:jsr250-api:1.0'
provided 'org.glassfish:javax.annotation:10.0-b28'
// Data Binding
kapt 'com.android.databinding:compiler:1.0-rc4'
}
kapt {
generateStubs = true
}
The official docs don’t mention android-apt anywhere, but you will want it. The android-apt plugin will make Android Studio aware of classes generated during the build process. This becomes crucial when trying to debug issues and learn more about how the binding mechanism works.
Binding Setup
The mechanism by which your View will receive it’s initial values and updates, and by which your ViewModel will handle events from the View, is through bindings. The Data Binding Library will automatically generate a binding class that will do all most of the hard work for you. Let’s look at the pieces required for this binding to occur.
Variable Declarations
In your layout files, you will need to add a new top level layout wrapper element around your existing layout structure. The first element inside of this will be a data element which will contain the types you will be working with in your layout bindings.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data class="FragmentCustomerBinding">
<variable name="viewModel" type="com.example.viewmodels.CustomerViewModel" />
</data>
...
<!-- the rest of your original layout here -->
</layout>
Here, we declared a viewModel variable that we will later set to a specific instance inside our Fragment.
Binding Declarations
We can now use this viewModel variable to do lots of interesting things by binding its properties to our layout widget attributes.
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/customer_name"
android:inputType="textCapWords"
android:text="@{viewModel.customerName}"
app:enabled="@{viewModel.primaryInfoEnabled}"
app:error="@{viewModel.nameError}"
app:addTextChangedListener="@{viewModel.nameWatcher}"
app:onFocusChangeListener="@{viewModel.nameFocusListener}" />
Here, we’re binding the text value, enabled state, error message, a text changed listener and a focus change listener.
NOTE: the android namespace can be used for any standard xml attribute on a view, but the app namespace must be used to map to setters that do not have a corresponding xml attribute. Also, using the app namespace instead of android for standard attributes seems to remove error highlighting in the IDE.
WARNING: due to the order in which the binding code is generated, you will want to use the android namespace for the text attribute to prevent ordering issues inside the generated binding code. Otherwise, the setText() will happen after the setError() and clear the error.
ViewModel Implementation
Now, let’s look at the corresponding methods on the ViewModel that will be bound to the view properties. The ViewModel extends BaseObservable (it doesn’t have to, but it saves you a lot of work), exposes public methods whose name matches the name in the layout binding and the return type matches the type expected by the view setter method being bound to.
public class CustomerViewModel extends BaseObservable {
public String getCustomerName() {
return customer.getName();
}
public boolean isPrimaryInfoEnabled() {
return editMode && !customer.isVerified();
}
@Bindable
public String getNameError() {
if (customer.getName().isEmpty()) {
return "Must enter a customer name";
}
return null;
}
public TextWatcher getNameWatcher() {
return new SimpleTextWatcher() {
@Override
public void onTextChanged(String text) {
customer.setName(text);
}
};
}
public EditText.OnFocusChangeListener getNameFocusListener() {
return (v, hasFocus) -> {
if (!hasFocus) notifyPropertyChanged(BR.nameError);
};
}
}
The first method is just doing a simple delegation to a domain entity to get the return value. The second and third are performing some logic to determine the return value. The rest are returning watchers or listeners to react to changes in the view. The great thing here is that this EditText will automatically get populated with the value from the ViewModel, show an error if it doesn’t pass validation rules and send updates back to the ViewModel as things change.
Notifications
Notice in the validate() method above, the listener calls notifyPropertyChanged(…). This will trigger the view to rebind the property and potentially show an error if one is then returned. The BR class is generated for you, much like the R file, to allow you to reference bindable properties in code. This granular notification isn’t possible unless you annotate the property with @Bindable. Since we only specified the viewModel variable in the layout, it’s the only “bindable” value it creates by default.
You can also trigger the view to rebind all of its properties by using the more generic notifyChange() method.
Be careful here. You can get into situations where you have a TextWatcher that calls notifyChange() which causes the text to be rebound, which triggers the TextWatcher, which causes a notifyChange() , which… you see where this is going?
It seems like best practices here will be one of the following:
- Short circuit the notification cycle by checking to see if the value actually changed before notifying.
- Avoid notifying the views that changed inside their own change listeners. If other views need to be notified in this situation, you will need to bind and notify at a more granular level.
Bringing it all together
So far we’ve set up the declarative pieces that will all react to each other and do the right thing. The only thing left is to bootstrap the bind mechanism. This will happen inside your Activity or Fragment. Since I use Fragments for all my views, I’ll show what that looks like.
FragmentCustomerBinding binding = FragmentCustomerBinding.bind(view);
CustomerViewModel viewModel = new CustomerViewModel();
binding.setViewModel(viewModel);
Taking it further
We looked at the basic building blocks of creating a UI that reacts to changes in the ViewModel as they are changing. Since you aren’t on the hook for writing the code that updates the UI, you can spend your time creating:
- Buttons that enable/disable based on the validity of the ViewModel
- Loading indicators that show/hide based on work being done in the ViewModel
- Unit tests that exercise every aspect of your view’s logic
Limitations
There are still some things that don’t seem to be handled well in this new binding world. For example, you can’t easily bind an ActionBar to a ViewModel. (Maybe forgoing the old ActionBar interface and just using a Toolbar directly could help?)
You will also need to delegate back to the Activity for framework-specific things that require the Activity Context. (Which is a lot!) You could inject interface implementations into your ViewModels or set the Activity/Fragment as a listener on your ViewModel, or just use the ViewModel inside the fragment and call methods on it. Either way, you can still use a ViewModel to house all your view logic and delegate out as needed.
Just think of the Fragment now as the place where you have to write your manual binding code – which is what it always was before, except now with all the time you save not writing most of that code, you can spend on the writing automated tests for your ViewModel!
What’s Missing
This library works very well but is still in beta, and you can tell when you use it. I look forward to seeing it mature and provide a better developer experience. Some of the things I look forward to seeing:
- CTRL+B navigation from method in Layout to the method in the ViewModel
- Clearer error messages when something goes wrong
- Auto complete and type checking inside the layout file
- Reduced boilerplate by combining standard two-way binding functionality
- Binding support for going from collections to AdapterViews