Author: Mark Zhai
Original: http://blog.zhaiyifan.cn/2016/06/16/android-new-project-from-0-p7/
Introduction
Data Binding has been available since its release at last year’s Google I/O, and it has been nearly a year now. Over this year, from Beta to a more stable version, and from Android Studio 1.3 to the current 2.1.2, Data Binding has become a technology that is quite usable and can significantly improve productivity.
However, in reality, there are still relatively few companies and projects that truly use Data Binding. This may be due to stability concerns, a lack of familiarity with the Data Binding technology itself, or simply a lack of pursuit of new technologies.
Our company has comprehensively used Data Binding technology in new products. Whether it’s me or the new engineers who are directly facing Data Binding, we all find it indispensable; once you get used to it, it’s hard to stop.
I hope that after reading this article, more friends will take an interest in using Data Binding and participate in its discussion.
Demo Source Code Repository: DataBindingSample
What is Data Binding
Data Binding, as the name suggests, is a way to bind data to XML directly, implemented by Google for MVVM in Android, allowing for automatic updates. The latest version also supports two-way binding, although the usage scenarios are not that many.
Data Binding can improve development efficiency (saving a lot of Java code that used to be written by hand), has high performance (even surpassing handwritten code), and offers strong functionality (powerful expression support).
Uses
-
Eliminate most UI code within Activities & Fragments (setOnClickListener, setText, findViewById, etc.)
-
XML becomes the only real source of UI
-
Reduce the main purpose of defining view IDs (data binding occurs directly in XML)
Open Source Solutions
-
ButterKnife, a well-known library by Jake, can reduce a lot of findViewById, setOnClickListener, and instead use annotations to generate code.
-
Android Annotations, also through annotations, with a lot of annotations, which are quite intrusive and require following its specifications to write some code, such as operations on Views can only be done under the @AfterViews annotation.
-
RoboBinding, a solution very similar to Data Binding, also does many things in XML, using AspectJ for generation. Besides these well-known ones, there are many different solutions, but since the release of Data Binding, they have all lost their place, as Data Binding is better in terms of performance, functionality, and IDE support.
Advantages
-
UI code is placed in XML, tightly coupling layout and data
-
Performance exceeds handwritten code
-
Ensures execution on the main thread
Disadvantages
-
IDE support is still not perfect (hints, expressions)
-
Error messages are not very direct
-
Refactoring support is poor (refactoring in XML will not automatically modify Java code)
Usage
It is very simple to use; you just need to add a few lines of code in the app module’s build.gradle.
Gradle
android {
…
dataBinding {
enabled = true
}
}
Layout Tag
To turn a normal layout into a data binding layout, only a few modifications are needed:
<layout>
// Original layout
</layout>
Just wrap the layout tag around the outermost XML, and after modification, you can see that the corresponding *Binding class for that layout has been generated.
Binding Generation Rules
The default generation rule: XML is generated through the file name, using underscores to separate camel case.
For example, activity_demo.xml will generate ActivityDemoBinding, and item_search_hotel will generate ItemSearchHotelBinding.
The view generation rule is similar, but since it is a class variable, the first letter is not capitalized; for example, if a TextView has an ID of first_name, it will generate a TextView named firstName.
We can also customize the generated class name, just need to:
<data class="ContactItem">…</data>
Then the generated class will be named ContactItem.
Basic Usage
Generating Binding Instances
All Binding instances can be generated through DataBindingUtil
, with the method name consistent with the original inflate method of that view, such as activity still being setContentView, just adding a parameter to obtain the activity.
Removing findViewById
After using Data Binding, we no longer need findViewById because all views with IDs have already been initialized in the Binding class and can be accessed directly through the binding instance.
Variable Binding
Using the data tag, we can declare variables in XML and use the field of that variable, setting it through the binding instance. For example:
<data>
<variable
name="employee"
type="com.github.markzhai.databindingsample.Employee"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".DemoActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{employee.lastName}"
android:layout_marginLeft="5dp"/>
</LinearLayout>
Then we can use it in Java code:
binding.setEmployee(employee);
// Or directly through setVariable
binding.setVariable(BR.employee, employee);
Event Binding
Strictly speaking, event binding is also a type of variable binding. We can directly bind in XML:
-
android:onClick
-
android:onLongClick
-
android:onTextChanged
-
…
Method Reference
Usually, a class named Handler or Presenter is defined in Java code, and then set in, with the method signature needing to match the corresponding listener method.
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="employee"
type="com.github.markzhai.databindingsample.Employee"/>
<variable
name="presenter"
type="com.github.markzhai.databindingsample.DemoActivity.Presenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".DemoActivity">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter First Name"
android:onTextChanged="@{presenter::onTextChanged}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{presenter.onClick}"
android:text="@{employee.firstName}"/>
</LinearLayout>
</layout>
In Java code:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
binding.setPresenter(new Presenter());
...
}
public class Presenter {
public void onTextChanged(CharSequence s, int start, int before, int count) {
employee.setFirstName(s.toString());
employee.setFired(!employee.isFired.get());
}
public void onClick(View view) {
Toast.makeText(DemoActivity.this, "Clicked", Toast.LENGTH_SHORT).show();
}
}
Listener Binding (Lambda)
It is possible not to follow the default method signature:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:visibility="@{employee.isFired ? View.GONE : View.VISIBLE}"
android:onClick="@{() -> presenter.onClickListenerBinding(employee)}"/>
public class Presenter {
public void onClickListenerBinding(Employee employee) {
Toast.makeText(DemoActivity.this, employee.getLastName(), Toast.LENGTH_SHORT).show();
}
}
Data Binding Principles
Narrow Principle
Narrowly speaking, we can directly observe its working principle by calling interfaces and generated classes.
As an entry point, let’s take a look at the interface of DataBindingUtil:
public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId,
DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
ViewGroup parent, int startChildren, int layoutId) {
final int endChildren = parent.getChildCount();
final int childrenAdded = endChildren - startChildren;
if (childrenAdded == 1) {
final View childView = parent.getChildAt(endChildren - 1);
return bind(component, childView, layoutId);
} else {
final View[] children = new View[childrenAdded];
for (int i = 0; i < childrenAdded; i++) {
children[i] = parent.getChildAt(i + startChildren);
}
return bind(component, children, layoutId);
}
}
We can see that it will then run to the specific Binding class:
public ItemFeedRecommendUserBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
super(bindingComponent, root, 9);
final Object[] bindings = mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds);
this.mboundView0 = (android.widget.LinearLayout) bindings[0];
this.mboundView0.setTag(null);
this.recommendUserFirst = (com.amokie.stay.databinding.IncludeRecommendUserBinding) bindings[1];
this.recommendUserFourth = (com.amokie.stay.databinding.IncludeRecommendUserBinding) bindings[4];
this.recommendUserSecond = (com.amokie.stay.databinding.IncludeRecommendUserBinding) bindings[2];
this.recommendUserThird = (com.amokie.stay.databinding.IncludeRecommendUserBinding) bindings[3];
setRootTag(root); // listeners
invalidateAll();
}
We can see that all views are initialized all at once, which is evidently faster than doing findViewById one by one.
In addition to view initialization, in executeBindings
, it will determine whether each field needs to be updated through mDirtyFlags
, and its setting is done through each set function.
Process Principle
-
Process layout files -> Convert to layout files without data binding
-
Parse expressions -> Ensure expression syntax is correct
-
Parse dependencies -> user.isAdmin, isAdmin is a field or method…
-
Setter -> such as visibility
-
0 reflection
-
findViewById requires traversing the entire view group, while now it only needs to initialize all required views once
-
Uses bit flags to check for updates (dirtyFlags)
-
Data changes will trigger operations only on the next batch update
-
Expression caching, no repeated calculations during the same refresh
-
Arithmetic + – / * %
-
String concatenation +
-
Logic && ||
-
Bitwise & | ^
-
Unary + – ! ~
-
Shift >> >>> <<
-
Comparison == > < >= <=
-
Instanceof
-
Grouping ()
-
Literal – character, String, numeric, null
-
Cast
-
Method calls
-
Field access
-
Array access []
-
Ternary ?:
Performance
Advanced Usage
Expressions
It does not yet support this, super, new, and explicit generic calls. It is worth mentioning that there is also a null-coalescing operator, such as:
android:text="@{user.displayName ?? user.lastName}"
Which takes the first non-null value as the result.
Here is a common example where the margin of a certain view is the margin of its left ImageView plus the width of that ImageView. In the past, we may need to define a dimension to hold these two values, but now we just need:
android:marginLeft="@{@dimen/margin + @dimen/avatar_size}"
We can even directly concatenate strings, such as:
android:text="@{@string/nameFormat(firstName, lastName)}"
<string name="nameFormat">%s, %s</string>
Avoiding Null Pointers
Data binding will automatically help us avoid null pointers; for example, @{employee.firstName}, if employee is null, then employee.firstName will be assigned a default value (null). For ints, it will be 0.
It is important to note the array out of bounds, as this is XML and not Java, so there is no place to check the size. Include
<include layout="@layout/name" bind:user="@{user}"/>
For included layouts, the usage is similar, but it is necessary to bind twice inside: the outer layout that includes the layout uses bind:user
to set it in.
It should be noted that the top-level of the included layout must be a ViewGroup; currently, the implementation of Data Binding, if the top level is a View instead of a ViewGroup, the binding index will conflict (be overwritten), resulting in some unexpected results.
ViewStubs
ViewStub is special; it is not visible before being actually inflated, so a special scheme is used, with a final ViewStubProxy
to represent it and listen to ViewStub.OnInflateListener
:
private OnInflateListener mProxyListener = new OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
mRoot = inflated;
mViewDataBinding = DataBindingUtil.bind(mContainingBinding.mBindingComponent, inflated, stub.getLayoutResource());
mViewStub = null;
if (mOnInflateListener != null) {
mOnInflateListener.onInflate(stub, inflated);
mOnInflateListener = null;
}
mContainingBinding.invalidateAll();
mContainingBinding.forceExecuteBindings();
}
};
It will only be initialized during onInflate.
Observable
A pure Java ViewModel class will not update the UI when it is updated. With data binding, we certainly hope that when data changes, the UI will refresh immediately; Observable is the concept born for this purpose.
BaseObservable
The class inherits from BaseObservable:
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
BaseObservable provides a series of notify functions (actually notifyChange and notifyPropertyChanged); the former refreshes all values, while the latter only updates the corresponding BR flag, and the BR is generated through the @Bindable annotation. In the above example code, we can see that both get methods are annotated, so we can access them through BR and notify specific property changes.
Observable Fields
If every bindable property needs to create an Observable class, that would be too cumbersome. Therefore, Data Binding also provides a series of Observable types, including ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable. We can also declare other types through ObservableField generics, such as:
private static class User {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
In XML, the usage is the same as normal String and int, but it will refresh automatically; however, accessing it in Java can be relatively cumbersome:
user.firstName.set("Google");int age = user.age.get();
Relatively speaking, having to get/set each time is quite troublesome; I personally think it is better to inherit from BaseObservable
directly.
Observable Collections
Some applications use more dynamic structures to store data; in this case, we would like to use a Map to store the data structure. Observable provides ObservableArrayMap:
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
In XML, we can access them directly by index key:
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text='@{user["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user["age"])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
When we do not want to define a key, we can use ObservableArrayList
:
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
In the layout, we can directly access them by numeric index.
Dynamic Variables
Sometimes, we do not know what the generated binding class is. For example, in RecyclerView, there may be multiple ViewHolders, and the holder we get is just a base class (how this base class is written will be discussed in the next article), at this time, we can define the same variable name in these item layouts, such as item, and directly call setVariable
:
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
executePendingBindings will force an immediate refresh of the binding changes.
References
https://developer.android.com/topic/libraries/data-binding/index.html
About Java and Android Experts Channel
Java and Android Experts Channel is a public account with tens of thousands of followers discussing Java and Android development, sharing and creating the most valuable dry goods articles, helping you become an expert in this field!
We discuss the cutting-edge technologies of Android and Java development: Android performance optimization, plug-in development, cross-platform, dynamic development, strengthening and anti-cracking, etc., and also discuss design patterns/software architecture. It is composed of a team of engineers from BAT.
Follow us to receive red envelopes, reply: “Baidu”, “Ali”, “Tencent” for surprises!!! After following, you can join the WeChat group. The group consists of experts from Baidu, Ali, and Tencent.
Welcome to follow us, discuss technology together, scan and long press the QR code below to quickly follow us. or search for the public account: JANiubility.
Public Account:JANiubility