Understanding Data-Binding''s generated code and How does Android Data-Binding compiler work
This post isn't for who want to learn how to use Android data-binding or getting to know the basis concept. I suggest you go directly to Google Documentation as a great starting point, which helps you integrate data-binding easily, a number of example code as well as other cool stuffs you can ...
This post isn't for who want to learn how to use Android data-binding or getting to know the basis concept. I suggest you go directly to Google Documentation as a great starting point, which helps you integrate data-binding easily, a number of example code as well as other cool stuffs you can achieve when decided to apply it for your project. After having some experiences with data-binding, you guys may curious about what happen under the hood, how Google make tradditional XML files are able to combine with data-binding, how can xml interact with java code, and how its compiler give us the magic. This topic intent to expose a big picture about data-binding mechanism, and a deeply look into what happen underlying. So, developers will have a deeper understand which help them use data-binding in the right way, take advantage of data-binding power to build the great app.
Recently, data-binding is a hottest android trend, which make developer life easier. Plenty of developers move to data-binding because of its beauty. But honestly, data-binding brings us a lot of troubles. Although, data-binding concept is pretty simple: "Yay! define a ViewModel class and try to match it to layout.xml, and we don't care about UI anymore, our data go straight into UI in a magic way." Really fast, really simple, but nothing is a bullet-silver. When something goes wrong, when your data suddenly cannot go to UI, when compiler raise a plenty of error messages and you really don't know what it means. What can we do!
I have to face with these data-binding issue a lot and use a variety of tricks to get over. And I think, there is no way but take a look at data-binding code, find out how it works and I wouldn't have to deal with these issues anymore. Let's clone this Repository from Google and read code together Data-Binding Repository
Part 1: Data-binding flow, Obsevable pattern mechanism and what data-binding's generated-code means
To remember the code on higher layer, which we use in our application to play with data-binding, I created a simplest set of component as an example.
- Create a simple R.layout.activity_main
<?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> <variable name="viewModel" type="com.example.main.MainViewModel" /> </data> <TextView android:layout_awidth="match_parent" android:layout_height="match_parent" android:text="@{ viewModel.text }" /> </layout>
- Create a simple MainActivity
public class MainActivity extends AppCompatActivity { @Inject MainViewModel mViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding bind = DataBindingUtil.setContentView(this, R.layout.activity_main); bind.setViewModel(mViewModel); } }
- Create MainViewModel
public class MainViewModel extends BaseObservable { public final ObservableField<String> text = new ObservableField<>(); public MainViewModel() { } }
After compile the project, here are the new files we got from data-binding:
- activity_main-layout.xml (in data-binding-info folder)
<?xml version="1.0" encoding="utf-8" standalone="yes"?> <Layout layout="activity_main" absoluteFilePath="/home/framgia/Projects/harpa-crista/harpacrista/android/app/src/main/res/layout/activity_main.xml" directory="layout" isMerge="false" modulePackage="com.harpacrista"> <Variables declared="true" name="viewModel" type="com.example.main.MainViewModel"> <location endLine="8" endOffset="51" startLine="6" startOffset="8" /> </Variables> <Imports name="View" type="android.view.View"> <location endLine="10" endOffset="42" startLine="10" startOffset="8" /> </Imports> <Targets> <Target tag="layout/activity_main_0" view="TextView"> <Expressions> <Expression attribute="android:text" text=" viewModel.text "> <Location endLine="16" endOffset="41" startLine="16" startOffset="8" /> <TwoWay>false</TwoWay> <ValueLocation endLine="16" endOffset="39" startLine="16" startOffset="24" /> </Expression> </Expressions> <location endLine="16" endOffset="44" startLine="14" startOffset="4" /> </Target> </Targets> </Layout>
- An shorter activity_main.xml version (in data-binding-layout-out folder)
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_awidth="match_parent" android:layout_height="match_parent" android:tag="layout/activity_main_0" />
- And ActivityMainBinding.java, this is our main actor, where most of the magic happens.
public class ActivityMainBinding extends android.databinding.ViewDataBinding { private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes; private static final android.util.SparseIntArray sViewsWithIds; static { sIncludes = null; sViewsWithIds = null; } // views private final android.widget.TextView mboundView0; // variables private com.example.main.MainViewModel mViewModel; // values // listeners // Inverse Binding Event Handlers public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) { super(bindingComponent, root, 2); final Object[] bindings = mapBindings(bindingComponent, root, 1, sIncludes, sViewsWithIds); this.mboundView0 = (android.widget.TextView) bindings[0]; this.mboundView0.setTag(null); setRootTag(root); // listeners invalidateAll(); } @Override public void invalidateAll() { synchronized(this) { mDirtyFlags = 0x4L; } requestRebind(); } @Override public boolean hasPendingBindings() { synchronized(this) { if (mDirtyFlags != 0) { return true; } } return false; } public boolean setVariable(int variableId, Object variable) { switch(variableId) { case BR.viewModel : setViewModel((com.example.main.MainViewModel) variable); return true; } return false; } public void setViewModel(com.example.main.MainViewModel viewModel) { updateRegistration(0, viewModel); this.mViewModel = viewModel; synchronized(this) { mDirtyFlags |= 0x1L; } notifyPropertyChanged(BR.viewModel); super.requestRebind(); } public com.example.main.MainViewModel getViewModel() { return mViewModel; } @Override protected boolean onFieldChange(int localFieldId, Object object, int fieldId) { switch (localFieldId) { case 0 : return onChangeViewModel((com.example.main.MainViewModel) object, fieldId); case 1 : return onChangeTextViewMode((android.databinding.ObservableField<java.lang.String>) object, fieldId); } return false; } private boolean onChangeViewModel(com.example.main.MainViewModel viewModel, int fieldId) { switch (fieldId) { case BR._all: { synchronized(this) { mDirtyFlags |= 0x1L; } return true; } } return false; } private boolean onChangeTextViewMode(android.databinding.ObservableField<java.lang.String> textViewModel, int fieldId) { switch (fieldId) { case BR._all: { synchronized(this) { mDirtyFlags |= 0x2L; } return true; } } return false; } @Override protected void executeBindings() { long dirtyFlags = 0; synchronized(this) { dirtyFlags = mDirtyFlags; mDirtyFlags = 0; } com.example.main.MainViewModel viewModel = mViewModel; java.lang.String textViewModel = null; android.databinding.ObservableField<java.lang.String> textViewModel1 = null; if ((dirtyFlags & 0x7L) != 0) { if (viewModel != null) { // read viewModel.text textViewModel1 = viewModel.text; } updateRegistration(1, textViewModel1); if (textViewModel1 != null) { // read viewModel.text.get() textViewModel = textViewModel1.get(); } } // batch finished if ((dirtyFlags & 0x7L) != 0) { // api target 1 android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView0, textViewModel); } } // Listener Stub Implementations // callback impls // dirty flag private long mDirtyFlags = 0xffffffffffffffffL; public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot) { return inflate(inflater, root, attachToRoot, android.databinding.DataBindingUtil.getDefaultComponent()); } public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot, android.databinding.DataBindingComponent bindingComponent) { return android.databinding.DataBindingUtil.<ActivityMainBinding>inflate(inflater, com.harpacrista.R.layout.activity_main, root, attachToRoot, bindingComponent); } public static ActivityMainBinding inflate(android.view.LayoutInflater inflater) { return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent()); } public static ActivityMainBinding inflate(android.view.LayoutInflater inflater, android.databinding.DataBindingComponent bindingComponent) { return bind(inflater.inflate(com.harpacrista.R.layout.activity_main, null, false), bindingComponent); } public static ActivityMainBinding bind(android.view.View view) { return bind(view, android.databinding.DataBindingUtil.getDefaultComponent()); } public static ActivityMainBinding bind(android.view.View view, android.databinding.DataBindingComponent bindingComponent) { if (!"layout/activity_main_0".equals(view.getTag())) { throw new RuntimeException("view tag isn't correct on view:" + view.getTag()); } return new ActivityMainBinding(bindingComponent, view); } /* flag mapping flag 0 (0x1L): viewModel flag 1 (0x2L): viewModel.text flag 2 (0x3L): null flag mapping end*/ //end }
As we can see above, from our activity_main.xml, data-binding generate for us 3 extra files: an activity_main-layout.xml, an shorter version activity_main.xml and an ActivityMainBinding.java. From there, we can have a first look at generated code from xml layout. Basically, an xml layout with outter tag<Layout></Layout> is different from the normal one. If the normal layout xml was used directly by android application, and sit in res/layout folder inside apk package, then xml layout with outter tag <Layout></Layout> was used indirectly. The compiler search through app layout folder to compile any file with outter tag <Layout></Layout> into the shorter version of activity_main.xml. And this version xml look exactly like the normal layout xml, which we gonna write if we don't go on data-binding way.
So, bassically, xml layout with data-binding is just a special version of the normal one, data-binding give them some lexical and grammar and force them rewrite normal layout in this way. Xml file cannot be understood by Android Framework, It's only understandable by data-binding compiler, and help compiler know where and how the data from ViewModel can map to View. Finally, compiler translate xml file into normal version, which is put inside apk beside activity_main-layout.xml and ActivityMainBinding.java
We can imagine that the original xml file contains 2 part: the normal part and the binding-part. Normal part is exactly the code we gonna write without binding. The binding-part is the code that help compiler able to generate java code, which is a nicely bridge between UI and Data.
activity_main-layout.xml (in data-binding-info folder) contains the binding-part. Just like its name, this file is the information about binding ability of the layout. Look at this file again. Outer tag is still <layout> tag, but It's not like before.
<Layout layout="activity_main" absoluteFilePath="/home/framgia/android/app/src/main/res/layout/activity_main.xml" directory="layout" isMerge="false" modulePackage="com.harpacrista"> ... </Layout>
layout="activity_main" attribute indicate this file is the binding-part of acitivity_main.xml, which currently located at "/home/framgia/android/app/src/main/res/layout/activity_main.xml". Inside <Layout> tag, no doubt, there are <Variables> tag and <Import> tag. Because we define/improve this before to setViewModel, and reference to some class in layout.
<Variables declared="true" name="viewModel" type="com.example.main.MainViewModel"> <location endLine="8" endOffset="51" startLine="6" startOffset="8" /> </Variables> <Imports name="View" type="android.view.View"> <location endLine="10" endOffset="42" startLine="10" startOffset="8" /> </Imports>
They also contains location information. I don't know why compiler need to save this location of Variable/Import, maybe its used for trace back or something like that. And this is most important info in this file, <Target> tag tell compiler where the ViewModel should map to View, binding type is one-way or two-way, location of View, location of value in View,
<Targets> <Target tag="layout/activity_main_0" view="TextView"> <Expressions> <Expression attribute="android:text" text=" viewModel.text "> <Location endLine="16" endOffset="41" startLine="16" startOffset="8" /> <TwoWay>false</TwoWay> <ValueLocation endLine="16" endOffset="39" startLine="16" startOffset="24" /> </Expression> </Expressions> <location endLine="16" endOffset="44" startLine="14" startOffset="4" /> </Target> </Targets>
We have a list of <Target> tags here. Remember we can have many View/ViewGroup in layout, but only View/ViewGroup contains data-binding expression are appeared here. And <Expression> tag inside <Target> tag is represent for data-binding expression in View. For example:
-
Data-binding expression android:visibility="@{ viewModel.isVisible ? View.VISIBLE : View.