This is the fourth part of a series of articles on Android Data Binding Library. Here are the others:
- Introduction to Data Binding
- Data Tags, Binding Expressions, Imports and Include
- RecyclerViews and Event Handling with Data Binding
- Binding Adapters and Observable Objects(You’re here)
- Two-Way Data Binding
And here is the github repository with several data binding samples(both in Kotlin and Java) as a companion to these articles.
In the previous article, we have seen how to handle events with data binding and how to implement data binding in RecyclerView item layouts. In this fourth article, we’ll see how to make custom binding adapters and how to use observable data with data binding.
Binding Adapters
In the previous articles, we have used expressions like this:
android:text="@{product.name}"
When we use such binding expressions with xml attributes, here is what happens under the hood: The library looks for a setter for text attribute, which would be named setText according to conventions and which accepts a String as an argument(since we are trying to pass a String to it).
Similarly, when we declare:
app:imageResource="@{product.imageResourceId}"
it will look for a setter named setImageResource, which accepts an int as argument(since imageResourceId field of the product is an integer).
And it will indeed successfully find them. This is thanks to binding adapters. A binding adapter is a kind of annotated method which associates an xml attribute(or attributes) with the appropriate method calls. Binding adapters provide us some xml attributes and make sure that those attributes call correct methods. There are many binding adapters provided by the framework. The examples above are some of them. However, we can also define our custom binding adapters according to our needs.
A very typical use case for defining a custom binding adapter is for loading images with Glide or Picasso. The binding adapter above, setImageResource will simply set the image resource to imageView. But we can define an attribute which takes the image url and loads the image by using Glide or Picasso:
Let’s look at the above method. The method loadImage() is annotated as a BindingAdapter with the attribute name “imageSrc”. This will create a custom attribute called imageSrc, that we can use in our xml. The first argument of the method is the ImageView that the image will be loaded. The second argument is the url of the image. The name of the method, loadImage, is arbitrary and is not important. The name of the attribute “imageSrc” is arbitrary as well, but you’d better chose something that is not already used by the framework.
Once you define it like this, you can use this attribute in your xml with app namespace:
app:imageSrc="@{article.imageUrl}"
You may see sometimes people write the attribute name with a namespace in the binding adapter, like “app:imageUrl” or “android:imageUrl”. This won’t create an error, but it is not the correct way of doing it, because the library doesn’t take that namespace into account. The ideal way is not to add a namespace in the binding adapter definition, and adding “app” namespace in your xml while you use that attribute.
Your binding adapter will associate your attribute with the method you have provided and call that method whenever needed. You might recognize that we are passing only one argument, image url, to the binding adapter, although the method has two parameters. Remember that the first parameter is the imageView. Since we will be using this attribute within the tags of an imageview, the library will use that imageView as the first parameter.
In Kotlin, you can write your binding adapters as extension functions as well. In that case, you won’t need to add a parameter for the view:
You can see further examples of binding adapters from our samples: Kotlin, Java
Observable Fields and LiveData
By default, if the value of a variable declared in your layout changes, the changes won’t be reflected in UI. You will need to set it again and/or call invalidateAll() on your binding instance. However, if you use observable fields or LiveData, the changes will be reflected automatically on UI.
Before proceeding, let me note that, Data Binding Library had come out before LiveData and it had come out with these observable fields and objects that we could use. And once Google Android team brought out LiveData, they also provided LiveData support in Data Binding Library. So, we have now these both options. But according to this recent article, Google team now advises to use LiveData instead of ObservableFields.
That said, for the sake of completeness, I’ll talk about and demonstrate both options. But you’re free to skip ObservableFields part if you wish.
First, let me demonstrate use of LiveData in data binding. (I assume you are familiar with LiveData.)
In our NewsApi samples (Kotlin, Java), for dealing with the visibility states of the progress bar, recyclerview and error image, we created an enum with three states:
Then in the viewmodel, we declared a MutableLiveData of type UIState and initialized it with the default value of UIState.Loading
//Java
private final MutableLiveData<UIState> uiState = new MutableLiveData<>();
uiState.setValue(UIState.LOADING);//Kotlin
private val _uiState : MutableLiveData<UIState> = MutableLiveData()
_uiState.value = UIState.LOADING
This value changes on the fly, according to the current state of the application. If network check fail, uiState is set to UIState.NETWORK_ERROR. If data is successfully retrieved, uiState is set to UIState.SUCCESS.
As usual, as a good practice, we expose this MutableLiveData as LiveData to UI. And in our ArticleListFragment we pass it to binding implementation.
//Kotlin
binding.included.uiState = mViewModel.uiState//Java
binding.included.setUiState(mViewModel.getUiState());
When integrating LiveData into data binding, we also need to assign a lifecycleowner to the binding implementation:
//Kotlin
binding.lifecycleOwner = this.viewLifecycleOwner//Java
binding.setLifecycleOwner(this.getViewLifecycleOwner());
And inside the xml layout, here is how we declared and used LiveData:
“<” is used to escape < character. And app:visible attribute here refers to a custom binding adapter, which accepts a boolean as parameter and shows or hides the views accordingly.
Note that we don’t need to call uiState.value when we use it in xml layouts. The library does it for us.
And that’s it. Now, whenever ui state changes, layout will be automatically updated. Check out the MainViewModel and ArticleListFragment of our samples to have a better grasp what I told above.
As for observable fields, although it is possible to implement Observable interface and create your custom observable objects, Data Binding Library already provides observable fields of various types. For primitive types, there are ObservableBoolean, ObservableInt, ObservableLong etc. (For full list, check out official documentation.)
You can declare these fields as follows in your Java/Kotlin code:
//Kotlin
val age = ObservableInt()
val isVisible = ObservableBoolean(true)//Java
public final ObservableInt age = new ObservableInt();
public final ObservableBoolean isVisible = new ObservableBoolean();
Note that you need to use public final modifiers in Java and read-only property val in Kotlin.
For Strings and other non-primitive objects, we use ObservableFields:
//Kotlin
val name = ObservableField<String>()//Java
public final ObservableField<String> name = new ObservableField<>();
You can set or get the value of these properties from Java/Kotlin code as follows:
//Java & Kotlin
age.set(11)
name.set(“John”)age.get()
name.get()
To demonstrate the use of observable fields, in our two-way data binding samples (Kotlin, Java), we used an observable field this time for controlling our ui state.
UiState is again an enum for facilitating the switch between different states, similar to the previous example. (Though states are a bit different since this is an inventory app). We keep this variable inside viewmodel again, with a default value of UIState.LOADING:
//Kotlin
val uiState = ObservableField<UIState>(UIState.LOADING)//Java
public final ObservableField<UIState> uiState = new ObservableField<>(UIState.LOADING);
Then inside the layout, we declare and use this variable as follows:
Again, we don’t need to call uiState.get() in xml layouts. Library does it for us. Different than LiveData, we don’t need to set a lifecycleowner to the binding instance.
Finally we set this uiState in ToyListFragment as follows:
//Java
binding.setUiState(mViewModel.uiState);//Kotlin
binding.uiState = mViewModel.uiState
And we update it as follows:
If there is no item in the database, we set it to EMPTY state which will show an image to inform the user, and if there is data, we set it to HAS_DATA state, which will show the list of items.
In most of the cases, LiveData(or observable fields) will suffice for your needs. However, sometimes, you might want to make your custom objects observable by implementing Observable class. The easiest way to achieve that is to extend your class from BaseObservable. You need to add @bindable annotation to the getters of the fields you want to observe; when rebuild, this will create fields for those variables in the generated BR class that we mentioned before. Then inside the setters of those fields, you call notifyPropertyChanged for that property. Here is an example:
This way, when you use this class with data binding, changes in a field will be automatically reflected in UI, without the need to wrap it in LiveData or ObservableField.
In the next article, we will talk about two way data binding.