Recent Posts

Those who are free of resentful thoughts surely find peace. - Buddha

MVVM - Part1

Posted on 27th Nov 2017


<-Back to Blogs

Data Binding

What is data binding?

Data binding means that data from data sources is bound to data consumers. For us this usually means that data from local storage or network is bound to layouts. Also, an important feature of data binding is that data changes are automatically synchronized between sources and consumers.

data-binding

What are the benefits of the data binding library?

TextView textView = (TextView) findViewById(R.id.label);
EditText editText = (EditText) findViewById(R.id.userinput);
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress);

editText.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override public void afterTextChanged(Editable s) { }
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
model.setText(s.toString());
}
});

textView.setText(model.getLabel());
progressBar.setVisibility(View.GONE);

We’ve all been writing this kind of code. Lots of findViewById() calls and later many calls to setters and listeners and so on. Even with libraries like ButterKnife it doesn’t really get better. With the data binding library, this is a thing of the past.

A binding class is created on compile time for you, which provides all views with an ID as fields. This means no more findViewById(). Actually it’s faster than manually callingfindViewById() multiple times, because the data binding library creates code that traverses the view hierarchy only once.

The binding class also implements the binding logic from the layout files, so all those setters are actually called in the binding class. You don’t have to care about it anymore. In short, this means less code in your activities, fragments and view holders.

How to setup data binding?

android {
   compileSdkVersion 25
   buildToolsVersion "25.0.2"
   ...
   dataBinding {
       enabled = true
   }
   ...
}
 
 
First thing to do is to add dataBinding { enabled = true } to your app’s build.gradle. This tells the build system to enable additional processing for data binding, like creating the binding classes from your layout files.
 
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <variable name="vm" type="com.softhinkers.ui.main.MainViewModel" />
    <import type="android.view.View" />
  </data>
  ...
</layout>
 
 
Next, wrap your layout’s top element in <layout> tags, so that a binding class is created for this layout. The binding class has the name of your layout xml file with Binding added at the end, e.g. ActivityMainBinding for activity_main.xml. As you can see above, namespace declarations also move to the layout tag. Then – inside the layout tag – declare the data you are binding as variables, giving them a name and type. In our case the only variable will be the view model, but more to this later. Optionally you can import classes so that you can use constants like View.VISIBLE or static methods.
 

How to bind data?

<TextView
    android:id="@+id/my_layout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{vm.visible ? View.VISIBLE : View.GONE}">
    android:padding="@{vm.bigPadding ? @dimen/paddingBig : @dimen/paddingNormal}"
    android:text='@{vm.text ?? @string/defaultText + "Additional text."}' />
 
 
Data binding instructions on view attributes start with an @ and are enclosed by brackets. You can use any variables and imports you declared in the data section. The expressions support nearly everything you can do in code, for example arithmetic operators or string concatenation.
 
As you can see on the visibility attribute, the ternary if-then-else operator is also supported. A null coalescing operator ?? is provided, which returns the right operand if the left one was null. You can see this above with the text attribute. You can access resources as you would in normal layouts, therefore you can for example choose different dimension resources based on a boolean property of one of your variables, as you can see with the padding attribute.
 
Properties of your declared variables can be accessed via field access syntax, even if your code uses getters and setters. You can see this again in the text attribute on the slides. vm.text calls the getText() method of the view model. Finally, some small restrictions apply, e.g. no new objects can be created. However, the data binding library is still very powerful.
 

Which attributes can be bound?

android:text="@{vm.text}"
android:visibility="@{vm.visibility}"
android:paddingLeft="@{vm.padding}"
android:layout_marginBottom="@{vm.margin}"
app:adapter="@{vm.adapter}"
 
Actually, most properties of the standard views are already supported by the data binding library. Internally, the library looks for setters on the view type for the attribute names where you use data binding. For example, when you bind data to the text attribute, the library looks for a setText() method in your view’s class with the right parameter type (in this case String).
 
This also means that you can use setters, which normally do not have a corresponding layout attribute, by using data binding. For example you could use the app:adapter attribute on a recycler view in the xml layout to set the adapter via data binding.
 
With standard attributes, not every one of those have a corresponding setter method on the View, for example paddingLeft. In this case, the data binding library ships a custom setter, so that binding to padding attributes works out of the box. Now, what to do when no custom setters are provided by the library, for example for layout_marginBottom?
 

Custom setters

@BindingAdapter("android:layout_marginBottom")
public static void setLayoutMarginBottom(View v, int bottomMargin) {
   ViewGroup.MarginLayoutParams layoutParams =
           (ViewGroup.MarginLayoutParams) v.getLayoutParams();
  
   if (layoutParams != null) {
       layoutParams.bottomMargin = bottomMargin;
   }
}
For these cases, custom setters can be written. Setters are annotated with the @BindingAdapter annotation, which takes the layout attribute name as argument, for which the binding adapter should be called. Above you see a binding adapter for layout_marginBottom.
 
The method must be public static void and must accept as parameters first the view type for which the binding adapter should be called and then the data to be bound with your desired type. In this example, we define a binding adapter for type View (and subtypes) with a bound type of int. Finally, implement the binding adapter. For layout_marginBottom, we need to get the layout parameters and set the bottom margin. Easy.
 
@BindingAdapter({"imageUrl", "placeholder"})
public static void setImageFromUrl(ImageView v, String url, int drawableId) {
   Picasso.with(v.getContext().getApplicationContext())
           .load(url)
           .placeholder(drawableId)
           .into(v);
}
 
It’s also possible to require multiple attributes to be set for a binding adapter to be called. To achieve this, provide your list of required attribute names to the @BindingAdapter annotation. Also, each of these attributes now have a typed parameter in the method. These BindingAdapters are only called, when all declared attributes are set.
 

Applying the binding in code

MyBinding binding;
 
// For Activity
binding = DataBindingUtil.setContentView(this, R.layout.layout);
// For Fragment
binding = DataBindingUtil.inflate(inflater, R.layout.layout, container, false);
// For ViewHolder
binding = DataBindingUtil.bind(view);
 
// Access the View with ID text_view
binding.textView.setText(R.string.sometext);
 
// Setting declared variables
binding.set<VariableName>(variable);
 
Now that we defined our bindings in the xml file and have written custom setters, how do we apply the binding in code? The data binding library does all the hard work for us, by generating a binding class. For getting an instance of the corresponding binding class for your layout, use the helper methods provided by the library. For activities use DataBindingUtil.setContentView(), for fragments use inflate() and for view holders use bind(). As already mentioned, the binding class provides all views which have an ID defined as final fields. Also, you set the variables you declared in the layout files on the binding object.
 

Auto-updating the layout

One of the benefits of data binding is that the layout can be updated automatically by the library when data changes. However, the library still needs to be notified of data changes. This is accomplished by having the variables you set on the binding implement the Observable interface (don’t confuse this with the RxJava Observable).

For simple data types like int or boolean, the library already provides appropriate types which implement Observable, for example ObservableBoolean. Also, there is an ObservableField type for use with other objects, like strings.

public class MyViewModel extends BaseObservable {
   private Model model = new Model();
 
   public void setModel(Model model) {
       this.model = model;
       notifyChange();
   }
  
   public void setAmount(int amount) {
       model.setAmount(amount);
       notifyPropertyChanged(BR.amount);
   }
 
   @Bindable public String getText() { return model.getText(); }
   @Bindable public String getAmount() { return Integer.toString(model.getAmount()); }
}
 
In more complex situations, like with view models, a BaseObservable class exists, which provides easy methods for notifying the layout of changes. As you can see above in the setModel() method, we can then update the whole layout at once when the model changes by calling notifyChange().
 
When you look at setAmount(), you see that only one property of our model is changed. In this case, we do not want the whole layout to be updated, but just those parts that use this exact property. To achieve this, the corresponding getters of the property can be annotated with @Bindable. Then, a field in the BR class is generated, which can be passed into the notifyPropertyChanged() method. With this, the data binding library only updates those parts of your layout that actually depend on the changed property.
 

Summary

  1. Declare variables in the layout files and use them to bind attributes of your views.
  2. Create the binding in code and set the variables.
  3. Make sure that your variable types implement Observable – for example by extending BaseObservable – so that data changes are automatically reflected by the layouts.


<-Back to Blogs

Categories

Good, better, best. Never let it rest. Untill your good is better and your better is best. - St. Jerome

© SOFTHINKERS 2013-18 All Rights Reserved. Privacy policy