The DataBinding DataBinding library, part of Android Jetpack, allows you to bind interface components in your layout to data sources in your application using declarative personalities rather than programmatically. In my opinion, when using DataBinding, don’t write complex logic in XML layout files, just bind the data. It is only responsible for the final data and UI directly bound, just a terminal value assignment, does not involve complex UI logic, and avoids the null processing of a large number of redundant codes in the code, at the same time avoids those common setVisible and other template method call, simplifies the development process, unified UI data source.
The basic use
Introduction to Simple Use
The XML layout is as follows:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.jackie.jetpackdemo.data.TestInfo"/>
<variable
name="userInfo"
type="TestInfo" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btnGetUserInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Get user information"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txtUserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{userInfo.age}"
app:layout_constraintTop_toBottomOf="@+id/btnGetUserInfo"
android:layout_marginTop="30dp"
android:textSize="30dp"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="text"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@+id/txtUserName"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Copy the code
The calling code for the Activity is as follows:
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
val activityBinding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)
activityBinding.lifecycleOwner = this
activityBinding.userInfo = TestInfo("lsm"."lsj")}Copy the code
TestInfo is defined as follows:
public class TestInfo extends BaseObservable { / / BaseObservable inheritance
private String age;
private String name;
public TestInfo(String age,String name){
this.name = name;
this.age = age;
}
public void setAge(String age) {
this.age = age;
notifyPropertyChanged(BR.age); // Add notifyPropertyChanged to the set method of the variable to be changed
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name); // Add notifyPropertyChanged to the set method of the variable to be changed
}
@Bindable // Variables that need to be changed are annotated with @bindable
public String getAge(a) {
return age;
}
@Bindable // Variables that need to be changed are annotated with @bindable
public String getName(a) {
returnname; }}Copy the code
TestInfo inherits the BaseObservable annotation with the @bindable annotation for the variable to listen for changes and the notifyPropertyChanged set method. Br.xxx is the annotation generation.
Two-way binding of data
With one-way data binding, you can set values for a property and set listeners that respond to changes to that property:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>
Copy the code
Two-way data binding provides a shortcut to this process:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>
Copy the code
The @={} notation, which importantly contains the “=” symbol, receives data changes for attributes and listens for user updates at the same time. The other Settings are consistent with the previous one-way data binding.
Used in conjunction with LiveData
TestInfo also inherits BaseObserble from DataBinding, using annotations and notifyPropertyChanged(), which can be complicated and intrusive to use. The steps to use LiveData with DataBinding are as follows:
- LifecycleOwner needs to be set up to use the LiveData object as the data binding source.
- The variable ViewModel is defined in XML and used.
- Binding Sets the ViewModel variable.
// Combine the ViewModel used by DataBinding
//1. To use the LiveData object as the data binding source, set the LifecycleOwner
binding.setLifecycleOwner(this);
ViewModelProvider viewModelProvider = new ViewModelProvider(this);
mUserViewModel = viewModelProvider.get(UserViewModel.class);
//3. Set the variable ViewModel
binding.setVm(mUserViewModel);
Copy the code
The XML file is defined as follows:
<! -- 2. Define ViewModel and bind -->
<variable
name="vm"
type="com.hfy.demo01.module.jetpack.databinding.UserViewModel" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.userLiveData.name}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.userLiveData.level}"/>
Copy the code
This is ok. You can see that we don’t need to get LivaData to observe(owner, Observer) in the Activity. DataBinding automatically generates code to do this for us, so we need to set the LifecycleOwner.
Bidirectional data binding using custom features
For example, if you want to enable bidirectional data binding for the “time” feature in a custom view named MyView, complete the following steps:
- use
@BindingAdapter
Comment on the method used to set the initial value and update it when it changes:
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
// Important to break potential infinite loops.
if(view.time ! = newValue) { view.time = newValue } }Copy the code
- use
@InverseBindingAdapter
Comment on the method for reading values from a view:
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
return view.getTime()
}
Copy the code
See here for more information.
Source code analysis
Our path in app/build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_main – layout. XML view the file
<Layout directory="layout" filePath="app/src/main/res/layout/activity_main.xml"
isBindingData="true" isMerge="false" layout="activity_main"
modulePackage="com.jackie.jetpackdemo" rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
<Variables name="userInfo" declared="true" type="TestInfo">
<location endLine="9" endOffset="29" startLine="Seven" startOffset="8" />
</Variables>
<Imports name="TestInfo" type="com.jackie.jetpackdemo.data.TestInfo">
<location endLine="6" endOffset="60" startLine="6" startOffset="8" />
</Imports>
<Targets>
<Target tag="layout/activity_main_0"
view="androidx.constraintlayout.widget.ConstraintLayout">
<Expressions />
<location endLine="47" endOffset="55" startLine="12" startOffset="4" />
</Target>
<Target id="@+id/txtUserName" tag="binding_1" view="TextView">
<Expressions>
<Expression attribute="android:text" text="userInfo.age">
<Location endLine="32" endOffset="41" startLine="32" startOffset="12" />
<TwoWay>false</TwoWay>
<ValueLocation endLine="32" endOffset="39" startLine="32" startOffset="28" />
</Expression>
</Expressions>
<location endLine="37" endOffset="13" startLine="27" startOffset="8" />
</Target>
<Target id="@+id/btnGetUserInfo" view="Button">
<Expressions />
<location endLine="25" endOffset="55" startLine="17" startOffset="8" />
</Target>
</Targets>
</Layout>
Copy the code
You can see that the
tag is our layout. The
tag corresponds to ConstraintLayout, the TextView, and activitY_main_0 corresponds to ConstraintLayout. Another path app/build/intermediates/incremental/mergeDebugResources/stripped dir/layout/activity_main XML
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" android:tag="layout/activity_main_0" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools">
<Button
android:id="@+id/btnGetUserInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Get user information"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txtUserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:tag="binding_1"
app:layout_constraintTop_toBottomOf="@+id/btnGetUserInfo"
android:layout_marginTop="30dp"
android:textSize="30dp"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="text"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@+id/txtUserName"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code
Expression attribute=” Android :text” text=” userinfo. age”
The specific attribute corresponds to the specific value.
Initialize the
Come from val activityBinding: ActivityMainBinding = DataBindingUtil. The setContentView (this, R.l ayout. Activity_main) to analyze the source code,
//DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId, @Nullable DataBindingComponent bindingComponent) {
activity.setContentView(layoutId); // We still need setContentView
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
Copy the code
The activity_XXx. XML we set is actually under Android.r.D.C. Tent, so look at the bindToAddedViews method
//DataBindingUtil.java
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); / / call the bind
} 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); / / call the bind}}}Copy the code
You’ll eventually call the bind method
//DataBindingUtil.java
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
Copy the code
SMapper DataBinderMapper, The real implementation class is through APT Generate DataBinderMapperImpl (app/build/generated/ap_generated_sources/debug/out/com/Jackie/jetpackdemo/DataBinderMapperImpl. J Ava)
public class DataBinderMapperImpl extends DataBinderMapper {...@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if(localizedLayoutId > 0) {
final Object tag = view.getTag();
if(tag == null) {
throw new RuntimeException("view must have a tag");
}
switch(localizedLayoutId) {
case LAYOUT_ACTIVITYMAIN: {
if ("layout/activity_main_0".equals(tag)) {
return new ActivityMainBindingImpl(component, view); // Key code, new ActivityMainBindingImpl}...Copy the code
Next we analyze ActivityMainBindingImpl (app/build/generated/ap_generated_sources/debug/out/com/Jackie/jetpackdemo/databinding/Ac TivityMainBindingImpl. Java) this class, it is APT to generate,
//ActivityMainBindingImpl.java
public ActivityMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
this(bindingComponent, root, mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));
}
private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 1
, (android.widget.Button) bindings[2]
, (android.widget.TextView) bindings[1]);this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
this.mboundView0.setTag(null);
this.txtUserName.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
Copy the code
We call the first method, and the 3 in it means that we have three nodes in our layout file (ConstraintLayout, Button, and TextView), but we have a TextView in our layout. Why not? Because we didn’t set the Id of our TextView, so we didn’t generate it, and if we set it and rebuild it, 3 will become 4.
Moving on to the mapBindings method:
protected static Object[] mapBindings(DataBindingComponent bindingComponent, View root,
int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) {
Object[] bindings = new Object[numBindings];
mapBindings(bindingComponent, root, bindings, includes, viewsWithIds, true);
return bindings;
}
Copy the code
It starts by creating an array of objects of size 3, and then parses the three labels and puts them into that array. The ActivityMainBindingImpl public constructor above calls the private constructor, but back to that
val activityBinding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)
Copy the code
After executing this code, you already have these three objects in activityBinding, so you can make this call
activityBinding.txtUserName
activityBinding.btnGetUserInfo
Copy the code
So far, initialization is complete.
Calling process
Let’s start with this call
activityBinding.userInfo = TestInfo("lsm"."lsj")
Copy the code
This activityBinding.userInfo calls the setUserInfo method in ActivityMainBinding
//ActivityMainBinding.java
public void setUserInfo(@Nullable com.jackie.jetpackdemo.data.TestInfo UserInfo) {
updateRegistration(0, UserInfo);
this.mUserInfo = UserInfo;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.userInfo);
super.requestRebind();
}
Copy the code
Here’s the updateRegistration method
//localFieldId is the Id in the BR file. Observable is the observer
protected boolean updateRegistration(int localFieldId, Observable observable) {
return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
}
/** * Method object extracted out to attach a listener to a bound Observable object. */
private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() {
@Override
public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
return newWeakPropertyListener(viewDataBinding, localFieldId).getListener(); }};Copy the code
The CREATE_PROPERTY_LISTENER name is also straightforward, indicating that a property listener is created, meaning the WeakPropertyListener listener is called back when the property changes.
LocalFieldId is the Id in the BR file, what is the BR file?
public class BR {
public static final int _all = 0;
public static final int age = 1;
public static final int name = 2;
public static final int userInfo = 3;
}
Copy the code
Because we imported TestInfo(userInfo) into our XML file, we also annotated @bindable on the age and name attributes to generate the above BR file. Because we’re calling the setUserInfo method up here, we’re passing in 0.
Setting name this way also works
activityBinding.setVariable(BR.name,"Jackie")
Copy the code
The Observable in updateRegistration is the UserInfo we passed in, so let’s look at updateRegistration
//ViewDataBinding
private boolean updateRegistration(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return unregisterFrom(localFieldId);
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
registerTo(localFieldId, observable, listenerCreator);
return true;
}
if (listener.getTarget() == observable) {
return false;//nothing to do, same object
}
unregisterFrom(localFieldId);
registerTo(localFieldId, observable, listenerCreator);
return true;
}
Copy the code
The mLocalFieldObservers array binds listeners for each property, such as the four values in our BR above.
If the listener is empty, registerTo is called to create the listener and register it
protected void registerTo(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return;
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
listener = listenerCreator.create(this, localFieldId);
mLocalFieldObservers[localFieldId] = listener;
if(mLifecycleOwner ! =null) {
listener.setLifecycleOwner(mLifecycleOwner);
}
}
listener.setTarget(observable);
}
Copy the code
A setTarget adds listeners to an observer
public void setTarget(T object) {
unregister();
mTarget = object;
if(mTarget ! =null) { mObservable.addListener(mTarget); }}Copy the code
The implementation class for mObservable here is WeakPropertyListener, which is called back when each property changes
@Override
public void addListener(Observable target) {
target.addOnPropertyChangedCallback(this);
}
Copy the code
Target’s implementation class is BaseObservable, which is why TestInfo inherits BaseObservable.
public class BaseObservable implements Observable {
private transient PropertyChangeRegistry mCallbacks;
public BaseObservable(a) {}@Override
public void addOnPropertyChangedCallback(@NonNull OnPropertyChangedCallback callback) {
synchronized (this) {
if (mCallbacks == null) {
mCallbacks = new PropertyChangeRegistry();
}
}
mCallbacks.add(callback);
}
Copy the code
The overall diagram is as follows:
Add (ViewDataBinding) in PropertryChangeRegistry binds the observer to the observed, WeakListener in ViewDataBinding [] mLocalFieldObservers each variable has a WeakListener, In BaseObservable addOnPropertyChangedCallback (WeakPropertyListener) is to add attributes change callback.
MainActivity calls the setUserInfo flowchart
If the diagram above is not clear, I will also draw the process, you can have a look
The ActivityMainBindingImpl inherits from the ActivityMainBinding and the ActivityMainBinding inherits from the ViewDataBinding
The final implementation is setText in TextViewBindingAdapter.
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.}}else if(! haveContentsChanged(text, oldText)) {return; // No content changes, so don't set anything.
}
view.setText(text);
}
Copy the code
How is the data binding done before setUser is called?
We talked about initialization earlier, because the ActivityMainBindingImpl inherits from the ActivityMainBinding, and the ActivityMainBinding inherits from the ViewDataBinding, The statically initialized block in ViewDataBinding is as follows
static {
if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
ROOT_REATTACHED_LISTENER = null;
} else {
ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
@TargetApi(VERSION_CODES.KITKAT)
@Override
public void onViewAttachedToWindow(View v) {
// execute the pending bindings.
final ViewDataBinding binding = getBinding(v);
binding.mRebindRunnable.run();
v.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View v) {}}; }}Copy the code
The run method of mRebindRunnable is executed
private final Runnable mRebindRunnable = new Runnable() {
@Override
public void run(a) {
synchronized (this) {
mPendingRebind = false;
}
processReferenceQueue();
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
// Nested so that we don't get a lint warning in IntelliJ
if(! mRoot.isAttachedToWindow()) {// Don't execute the pending bindings until the View
// is attached again.
mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
return; } } executePendingBindings(); }};Copy the code
The executePendingBindings method is then executed, and the process is the same as the one we analyzed earlier. And eventually it will be
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.}}else if(! haveContentsChanged(text, oldText)) {return; // No content changes, so don't set anything.
}
view.setText(text);
}
Copy the code
We started with text empty, so we just returned it.
In the past, we used to have separate classes for the observer and the observed. Now, because there may be multiple viewModels (TestInfo, xxxInfo, etc.), there may also be multiple activities (bound separately), using the original method is very expensive. The BR file generates multiple fields, which are handled by a single mLocalFieldObservers, each with its own specific listener, and each xxxInfo with its own specific listener. The design is quite reasonable.
conclusion
This article starts with simple use of DataBinding, one-way/bidirectional binding, and use with LiveData, as well as some custom features, and concludes with source code analysis. Finally, it is important not to make complex logical judgments in XML, but to think of it as a support library that facilitates end-user UI presentation, avoids a lot of null-processing, and consolidates the source of the data.
My other articles
Android Jetpack ViewModel from beginner to Master
【Android Jetpack】LiveData from the beginning to master
【Android Jetpack】Lifecycle goes from beginning to mastery
Android Bitmaps load efficiently, those little things you need to know
Android screen adaptation, those little things you need to know
The past and present of the Android lightweight storage solution
Performance optimization: Why use SparseArray and ArrayMap instead of HashMap?
Have you mastered the basic, intermediate, and advanced methods of Activity?