Android databinding Series

We have been around so much of architectural changes in android. First we had MVC which says that put all controller logics in activity file itself. Then we come up with beautiful idea of having MVP by dividing all logic into separate file as per SOLID principle and so we put business logic in presenter and passed data to ui using interfaces. For some reason we needed reaction to happen automatically so we don’t need to notify screen every time and we come-up with MVVM and databinding. There are many more architectures like Uncle BOB pattern and viper etc. But let’s concentrate on databinding for now.
First we are going to start with basics and we go with how it works and then how we can leverage to develop app faster and better by letting annotation processors to generate code.
Keep a keen look on the bold items as we will use it as reference.
- Setup
apply plugin: 'kotlin-kapt' //If you have kotlin configured
android {
dataBinding {
enabled = true
}
}2. Let’s create a model
data class User(
var age: String = "0",
var property: String = "property"
)So here we have a User class which is having age and property variables. and we are going to use the variables inside xml
3. Changes to layout file
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.parthdave93.databindingdemo.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
.. name space and all...>
<TextView
android:id="@+id/tvAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.age}'
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvProperty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.property}'
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>How annotation processor works is it checks for <layout tag and if any file contains layout with <data tag it means that this is databinding layout and it needs to generate the files which are important to set data on the screen.
Here we have used two textviews to show age and property from the user model that we have created earlier.
<data tag contains all the variables and imports that you want in-order to set the data on the screen.
@{} used for one way databinding means that the view/layout it self will not change the data but the code. We use @={} to let views/layout change data of the object like edittext if user types inside edit text we don’t need to fetch details from edit text and set it manually this can be done internally by the generated code.
4. Changes in the activity class
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val user = User() // Initialise Data class object as needed
binding.user = user
}
}So DataBindingUtil will set content of this activity and we will get binding object to which we are going to set user variable declared in the layout file. And screen will get updated.
Let’s deeply understand this how it works behind the scene…
When we build project the annotation processor of databinding will run and generate ActivityMainBindingImpl implementation of ActivityMainBinding class. (How annotation processor works is different topic but for reference you can check my old repo on link)
Anatomy of the class file is reverse the layout with Binding as postfix. we had activity_main so ActivityMainBinding. You can find that class in android studio via fast search double shift(or other keymap shortcut).
So there are two classes one ActivityMainBinding (class ActivityMainBindingImpl extends ActivityMainBinding) and ActivityMainBindingImpl (abstract class ActivityMainBinding extends ViewDataBinding).
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)When we write above line the Databinding util’s setContentView method will get called, which will eventually call bind method.
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}Take a look at sMapper.getDataBinder sMapper is basically a mapper which maps the layout id to the implementation generated by code.
Remember when we used setContentView method in DataBindingUtil so how it come to know using layout id (as R.layout.activity_main is an Integer) that which binding object it needs to give back to us? This sMapper takes care of it.
private static DataBinderMapper sMapper = new DataBinderMapperImpl();addMapper(new com.parthdave93.databindingdemo.DataBinderMapperImpl());
that DataBinderMapperImpl class is having all the logic of mapping layout id to binding implementation.
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
...
switch(localizedLayoutId) {
case LAYOUT_ACTIVITYMAIN: {
if ("layout/activity_main_0".equals(tag)) {
return new ActivityMainBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
}
}
}
return null;
}and so now we have ActivityMainBindingImpl which will be having variables declared in layout file like User variable.
binding.user = user//internal method of ActivityMainBindingImpl
public void setUser(@Nullable com.parthdave93.databindingdemo.User User) {
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
and once we use it, class will update the dirtyFlags and request for rebind which will eventually call execute pending bindings and get views updated on the screen.
Uptil now we set user variable in layout and bindings will update textview when we set the user in async manner. Let’s suppose after setting up the user we are changing the user object variables like property the view will not get updated why?
It’s important to note that it’s not reactive it’s async or should i say it will wait and when setUser method called it will update those views there’s no logic we have set to update UI when we change property of an object.
To do this let’s modify the model little bit:
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
//BaseObservable classclass User: BaseObservable() {
@get:Bindable
var age: String = "0"
@get:Bindable
var property: String = "property"
set(value) {
field = value
notifyPropertyChanged(BR.property)
}
}
Here we extended the class with base observable and when we update property we notify base observable with the integer of that property which will only be generated when we annotate property with Bindable(@get:Bindable).
and so let’s take a look at how setUser method changed in activityMainBindingImpl by this changes.
public void setUser(@Nullable com.parthdave93.databindingdemo.User User) {
updateRegistration(0, User);
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}The update registration method is having weakObservables to listen user class changes when we call notifyPropertyChanged method and so the updates will get pushed to the screen automatically.
Let’s take case 2, Our user class is already been extended by Person class and we need databinding class BaseObservable to extend what should we do in that case?
In that moment we need to write a little more code by implementing Observable interface to do the same stuff.
import androidx.databinding.Bindable
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
//Observable interface
class User: Person(), Observable {
private val registry = PropertyChangeRegistry()
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
registry.remove(callback)
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
registry.add(callback)
}
@get:Bindable
var age: String = "0"
@get:Bindable
var property: String = "property"
set(value) {
field = value
registry.notifyChange(this, BR.property)
}
}PropertyChangeRegistry is a class used for PropertyChangeCallbacks to notify changes. That’s it
TL’DR
Databinding helps us to leverage the annotation processor capabilities and build UI faster than ever. We have looked at the classes that generated behind the scene and how it works under the hood. There are bunch of other stuffs that helps us to go beyond the regular Adapter and class file generation which will be covered in next article.
Thanks for reading and hope you liked it. If you have any suggestion or I miss wrote something will be happy to change it, please do comment.
