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:

  1. LifecycleOwner needs to be set up to use the LiveData object as the data binding source.
  2. The variable ViewModel is defined in XML and used.
  3. 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:

  1. use@BindingAdapterComment 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
  1. use@InverseBindingAdapterComment 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?