preface

Android’s application layer architecture has been slowly improving, but Android developers have made little progress. I, for example, didn’t start using dataBinding until 2021.

I initially in RecyclerView item trial, the feeling is quite good.

Environment to prepare

AGP(Android Gradle Plugin)

The official said 1.5.0 or above is good, certainly there is no problem.

Speaking of which, what has AGP done for us? There are a lot of things about the Android build process, but my current understanding of it is limited.

gradle

android {
        ...
        dataBinding {
            enabled = true}}Copy the code

With the above configuration, dataBinding can be turned on, or buildFeatures can be used for AGP 4.0.

With AGP support, the compilation process does a lot for us.

General usage

Data binding is implemented by binding expressions in a layout file.

Anyone who writes the back end or front end should be familiar with this all too well.

An example layout file

<! --notification_no_picture.xml-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">

  <data>
    <variable
      name="notification"
      type="com.ptrain.note.model.Notification" />
  </data>

  <TextView
    android:id="@+id/description"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:text="@{notification.description}"
    android:textColor="@color/teal_700"
    android:textSize="16sp" />
  
</layout>
Copy the code

Pass in an instance of Notification and use @{notification. XXX} for the value. You can also use @={notification. XXX} for bidirectional binding.

So how do you bind instances?

XXXBinding binding = databindingutil.inflate (context, layoutId, parent, attachToParent);// In the example above
/ / NotificationOnePictureBinding binding = DataBindingUtil. Inflate (context, layoutId, parent, attachToParent);
binding.setXXXX(model);
binding.executePendingBindings();
// In the example above
// binding.setNotification(notification);
Copy the code

Data classes must either use observable objects or directly use observable fields

Several important classes

Why did a few lines of code help us with data binding (one-way data binding in the example above)?

Mainly during compilation, AGP helped generate some code, in the example NotificationOnePictureBinding. Java and its setNotification () method, which are generated during compilation.

  1. XXXBinding. Java extends XXXBinding. This class extends XXXBinding. Such as notification_no_picture. XML will generate the corresponding NotificationNoPictureBinding. Java. Class holds a View object with an ID and a bound data class.

  2. Java extends XXXBinding. The naming logic is the same as xxxbinding. Java. Most of the data binding callback logic is in this Java class.

  3. The viewDatabing.java class is not compiled, but is a class in the Databing-Runtime library. ViewDataBing extends BaseObservable, XXXBingding extends ViewDataBinding. This class is important.

  4. The BaseObservable. Java class is not compiled, as is ViewDataBinding. As the name suggests, it is used to bind a listener to trigger a data refresh notifyXXX().

  5. Br.java is generated at compile time. I’m not sure why it’s called that, maybe it’s an abbreviation? This class is a list of keys containing the IDS of the resources used for data binding.

Use observables and fields

When using databinding, there are special requirements for data classes.

You use observable objects, collections, and fields, all of which are inherited from BaseObservables, and how they interact will be explained later.

What are observable data types?

Google docs https://developer.android.com/topic/libraries/data-binding/observability

Of course, you can also use LiveData directly to save more trouble.

Subsequent articles are based on using Java and not using LiveData.

DataBinding source code analysis

Wrong Model class

Suppose the data class looks like this, inheriting from BaseObservable.

public class Notification extends BaseObservable {
  public String description;
  public String title;
}
Copy the code

The layout file is as follows:


      
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
  <data>
    <variable
      name="notification"
      type="com.ptrain.note.model.Notification" />
  </data>

  <androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="8dp"
    android:paddingRight="8dp">

    <TextView
      android:id="@+id/title"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@{notification.title}"
      android:textColor="@color/black"
      android:textSize="24sp"
      app:layout_constraintBottom_toTopOf="@+id/description"
      app:layout_constraintTop_toTopOf="parent" />

    <TextView
      android:id="@+id/description"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@{notification.description}"
      android:textColor="@color/teal_700"
      android:textSize="16sp"
      android:layout_marginTop="8dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/title" />
  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Copy the code

The code to modify the data class and bind is as follows:

// Bind the associated code in onCreateViewHolder and onBindViewHolder
@Override
public NotificationOnePictureViewHolder onCreateViewHolder(ViewGroup parent) {
    NotificationOnePictureBinding binding = DataBindingUtil
    .inflate(LayoutInflater.from(parent.getContext()), NotificationType.ONE_PICTURE, parent,
        false);
    return new NotificationOnePictureViewHolder(binding);
}

// onBindViewHolder the Adapter I used has been reconstructed
@Override
protected void onBind(Notification data) {
    // Pass the data class to binding
    binding.setNotification(data);
    binding.executePendingBindings();
}

// ----------------------------------------------------------
// Data initialization and modification related code
// Omit initializing notificationList data
List<Notification> notificationList = new ArrayList<>();
// Pass to adapter
adapter.setData(notificationList);
// Feel free to simulate the source data for 5 seconds and modify it, regardless of the Handler
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
    @Override
    public void run(a) {
    notificationList.get(0).setTitle("i have changed"); }},5000);
Copy the code

Does this allow for data binding? no

The problem is that the data class inherits from a BaseObservable, but does not call notify().

public class Notification extends BaseObservable {
  public String description;
  public String title;

  public String getTitle(a) {
    return title;
  }

  public String getDescription(a) {
    return description;
  }

  public void updateTitle(String title) {
    this.title = title;
    // Change here
    notifyPropertyChanged(BR.notification);
  }

  public void updateDescription(String description) {
    this.description = description;
    // Change herenotifyPropertyChanged(BR.notification); }}Copy the code

Does this allow for data binding? no

Two questions how do br.java keys be generated? What does notifyPropertyChanged() do

How is the key generated in Br.java?

In the above code, br.java has only two properties, notification and _all.

All, as the name implies, means all

Notification is defined in the layout file

So the conclusion is that BR generates the key from the name of the Layout file data node, right? Obviously not

The @bindable annotation is mentioned in the Google documentation, but we don’t use it here. Once we annotate the getter method with the @bindable annotation, we see that the key in the BR increases accordingly.

So the conclusion is that the key in Br.java is determined by the @bindable and layout files

Strike while the iron is hot add @bindable annotation

The code is modified as follows:

public class Notification extends BaseObservable {
  public String description;
  public String title;
  
  // Change here
  @Bindable
  public String getTitle(a) {
    return title;
  }
  // Change here
  @Bindable
  public String getDescription(a) {
    return description;
  }

  public void updateTitle(String title) {
    this.title = title;
    notifyPropertyChanged(BR.notification);
  }

  public void updateDescription(String description) {
    this.description = description; notifyPropertyChanged(BR.notification); }}Copy the code

Does this allow for data binding? no

You have to look at the notifyPropertyChanged method

NotifyPropertyChanged () elicits the entire link

Since Notification extends BaseObservable, the notifyPropertyChanged code in BaseObservable is as follows

public void notifyPropertyChanged(int fieldId) {
    synchronized (this) {
        if (mCallbacks == null) {
            return;
        }
    }
    mCallbacks.notifyCallbacks(this, fieldId, null);
}
Copy the code

So it’s an mCallbacks callback, so when does the mCallbacks come in?

The call stack is as follows:

addOnPropertyChangedCallback:33, BaseObservable (androidx.databinding)
addListener:1446, ViewDataBinding$WeakPropertyListener (androidx.databinding)
addListener:1431, ViewDataBinding$WeakPropertyListener (androidx.databinding)
setTarget:1404, ViewDataBinding$WeakListener (androidx.databinding)
registerTo:688, ViewDataBinding (androidx.databinding)
updateRegistration:612, ViewDataBinding (androidx.databinding)
updateRegistration:627, ViewDataBinding (androidx.databinding)
setNotification:74, NotificationOnePictureBindingImpl (com.ptrain.note.databinding)
onBind:42, NotificationOnePictureBinder$NotificationOnePictureViewHolder (com.ptrain.note.viewholder)
Copy the code

Remember that onBindViewHolder passes model instances via bind.setNotification? At this point, the mCallbacks are registered with a series of calls to setNotification

The updateRegistration() method corresponds to the observable data type mentioned earlier

protected boolean updateRegistration(int localFieldId, Observable observable) {
    return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
}

protected boolean updateRegistration(int localFieldId, ObservableList observable) {
    return updateRegistration(localFieldId, observable, CREATE_LIST_LISTENER);
}

protected boolean updateRegistration(int localFieldId, ObservableMap observable) {
    return updateRegistration(localFieldId, observable, CREATE_MAP_LISTENER);
}

protected boolean updateLiveDataRegistration(intlocalFieldId, LiveData<? > observable) {
    mInLiveDataRegisterObserver = true;
    try {
        return updateRegistration(localFieldId, observable, CREATE_LIVE_DATA_LISTENER);
    } finally {
        mInLiveDataRegisterObserver = false; }}Copy the code

Observable(BaseObservable implements Observable), so CREATE_PROPERTY_LISTENER.

@Override
public void addListener(Observable target) {
    target.addOnPropertyChangedCallback(this);
}

@Override
public void addOnPropertyChangedCallback(@NonNull OnPropertyChangedCallback callback) {
    synchronized (this) {
        if (mCallbacks == null) {
            // Create PropertyChangeRegistry here
            mCallbacks = new PropertyChangeRegistry();
        }
    }
    mCallbacks.add(callback);
}
Copy the code

WeakPropertyListener. AddListener himself back, Then addOnPropertyChangedCallback created PropertyChangeRegistry extends CallbackRegistry

So the mCallbacks element in this example is really a WeakPropertyListener(a Listener of the corresponding observable type if the data type is any other type)

MCallbacks. NotifyCallbacks will call to CallbackRegistry. NotifyCallbacks, this method does not look, but is a recursive, through its annotations know, for all the callback callbacks.

private void notifyCallbacks(T sender, int arg, A arg2, final int startIndex,
            final int endIndex, final long bits) {
    long bitMask = 1;
    for (int i = startIndex; i < endIndex; i++) {
        if ((bits & bitMask) == 0) {
            mNotifier.onNotifyCallback(mCallbacks.get(i), sender, arg, arg2);
        }
        bitMask <<= 1; }}Copy the code

In CallbackRegistry mNotifier by PropertyChangeRegistry mNotifier callback to WeakPropertyListener. OnPropertyChanged, Finally to XXXBindingImpl. OnChangeXXXXX processing properties change.

The call stack is as follows:

onChangeNotification:92, NotificationOnePictureBindingImpl (com.ptrain.note.databinding)
onFieldChange:87, NotificationOnePictureBindingImpl (com.ptrain.note.databinding)
handleFieldChange:549, ViewDataBinding (androidx.databinding)
access$800:65, ViewDataBinding (androidx.databinding)
onPropertyChanged:1468, ViewDataBinding$WeakPropertyListener (androidx.databinding)
onNotifyCallback:30, PropertyChangeRegistry$1 (androidx.databinding)
onNotifyCallback:26, PropertyChangeRegistry$1 (androidx.databinding)
notifyCallbacks:201, CallbackRegistry (androidx.databinding)
notifyFirst64:122, CallbackRegistry (androidx.databinding)
notifyRemainder:169, CallbackRegistry (androidx.databinding)
notifyRecurse:145, CallbackRegistry (androidx.databinding)
notifyCallbacks:91, CallbackRegistry (androidx.databinding)
notifyPropertyChanged:76, BaseObservable (androidx.databinding)
setTitle:31, Notification (com.ptrain.note.model)
Copy the code

So notifyPropertyChanged is very simple. It handles the callback after a property change. The code for the callback is actually in the generated BindingImpl, in this case:

// The onChangeYYY(YYY is the name of the binding) method in the generated xxxbindingimp.java
private boolean onChangeNotification(com.ptrain.note.model.Notification Notification, int fieldId) {
    if (fieldId == BR._all) {
        synchronized(this) {
                mDirtyFlags |= 0x1L;
        }
        return true;
    }
    else if (fieldId == BR.title) {
        synchronized(this) {
                mDirtyFlags |= 0x2L;
        }
        return true;
    }
    else if (fieldId == BR.description) {
        synchronized(this) {
                mDirtyFlags |= 0x4L;
        }
        return true;
    }
    return false;
}
Copy the code

The cause of the model class error

As you can see from the onChangeNotification code above, the update will only be triggered if fieldId is _all, title, or description. So notifyPropertyChanged in our setter method needs to be changed.

Why didn’t onChangeNotification handle the logic of br.notification?

I don’t know because this class is generated at compile time.

But it’s also logical. Because if you’re dealing with br. notification, that’s the concept of br._all.

So what did onChangeNotification do

The notifyPropertyChanged part traces to onChangeNotification, so what happens after that?

Call requestRebind() to update the UI for the next frame.

The code is as follows:

// ViewDataBinding.java
protected void requestRebind(a) {
    if(mContainingBinding ! =null) {
        mContainingBinding.requestRebind();
    } else {
        final LifecycleOwner owner = this.mLifecycleOwner;
        if(owner ! =null) {
            Lifecycle.State state = owner.getLifecycle().getCurrentState();
            if(! state.isAtLeast(Lifecycle.State.STARTED)) {return; // wait until lifecycle owner is started}}synchronized (this) {
            if (mPendingRebind) {
                return;
            }
            mPendingRebind = true;
        }
        if (USE_CHOREOGRAPHER) {
            mChoreographer.postFrameCallback(mFrameCallback);
        } else{ mUIThreadHandler.post(mRebindRunnable); }}}Copy the code

You can see that the lower version uses mUIThreadHandler, the higher version registers FrameCallback, and then eventually executes mRebindRunnable, and finally executes

ExecutePendingBindings () to executeBindings().

ExecuteBindings () updates the View data with dirtyFlags.

The call stack for higher Versions of Android is as follows:

executeBindings:115, NotificationOnePictureBindingImpl (com.ptrain.note.databinding)
executeBindingsInternal:473, ViewDataBinding (androidx.databinding)
executePendingBindings:445, ViewDataBinding (androidx.databinding)
run:197, ViewDataBinding$7 (androidx.databinding)
doFrame:291, ViewDataBinding$8 (androidx.databinding)
run:1310, Choreographer$CallbackRecord (android.view)
doCallbacks:1123, Choreographer (android.view)
doFrame:934, Choreographer (android.view)
run:1298, Choreographer$FrameDisplayEventReceiver (android.view)
Copy the code

So much for one-way binding

Two-way binding

Layout. XML using the @ = {}

Two-way binding is a way of generating a listener in the corresponding control, calling back setter methods for the data class, and compiling an error if you don’t write it.

The generated code is still in the corresponding bindingimp.java.

conclusion

A few important classes, if you have a question, go here to find the answer

process

Because the code is generated at compile time, depending on the data type or layout. XML file name, etc., use X instead.

It can be roughly divided into three steps

1. BindListener process

This step registers the listener for the property and buries the callback logic.

2. Notify process

This step, by triggering property updates, gets through the previously buried callback logic all the way to onFieldChange and onChangeNotification in xbindingImp.java generated by compilation.

3. Refresh process

Refresh process is simpler, the next frame calls when ViewDataBinding. MRebindRunnable, has been called to compile the generated XBindingImpl. In Java executeBindings, to update the view.

Other (but important)

DataBinding also has a custom Binding Adapter, which makes DataBinding more flexible, allowing you to manipulate the model data content to suit the view.

Google docs https://developer.android.com/topic/libraries/data-binding/binding-adapters