First of all, this is not an introduction to the use of DataBinding, nor is it a source code analysis that covers the low-level implementation logic. It is more about the problems and principles involved in the actual development process, through which we can quickly help to troubleshoot problems and improve the development efficiency.

Before we get started, let’s consider the following questions. If you think you know something but are specious, then this article will give you a comprehensive understanding of Databinding. If you think these questions are too easy, are you… Should also review (forced retention).

  1. android:text=@{user.name}Does this common binding cause null-pointer problems when the user object is null? Why is that?
  2. Do all attributes of the View support Binding? Such as padding? Margin? Why is that?
  3. How does bidirectional binding handle infinite loop calls?

configuration

Android Gradle plugin (version >= 3.0-alpha06) supports automatic generation of binding classes.

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

The official document is here. The sample is from the official sample. For more information, please refer to the official blog.

The basic use

Assigning a value to an attribute uses the expression @{}, which can use Java code internally, but not exclusively. Consider the following example:

<ImageView
    android:id="@+id/imageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:tint="@{user.likes > 9? @color/star : @android:color/black}"
    app:srcCompat="@{user.likes < 4? R.drawable.ic_person_black_96dp : R.drawable.ic_whatshot_black_96dp }" />
Copy the code

Where the user object type is the entity class TestProfile:

public class TestProfile { private String name; private String lastName; private int likes; public TestProfile(String name, String lastName, int likes) { this.name = name; this.lastName = lastName; this.likes = likes; } // omit getter/setter... }Copy the code

In the databinding expression, the three entries for user.likes are used, but the likes attribute in TestProfile is private. In fact, the Databinding compiler does a mapping for us by looking for the getXxx() and XXX () methods of the XXX property in the user object’s class, and reporting compilation problems if neither is found.

In addition, databinding expressions allow direct access to the specific values of Obserable objects such as ObservableInt that are used for unidirectional binding. For example, we changed TestProfile slightly.

public class TestProfile { private String name; private String lastName; ObservableInt Private ObservableInt likes; public TestProfile(String name, String lastName, ObservableInt likes) { this.name = name; this.lastName = lastName; this.likes = likes; } // omit getter/setter... }Copy the code

After this, the binding expression does not need to be modified, that is, the compiler does the conversion for us: user.likes == user.getlikes ().get().

Do you need an air test?

Android :text=@{user.name} does this binding cause null pointer problems when the user object is null? What if the user object passed in is assumed to be empty in our example?

In fact, the DataBinding compiler already has this problem in mind. If you don’t deal with the basic nullation problem, you can imagine an XML layout full of nullation statements, and you can accept nullation once. How would you handle nullation like a.b().c()? So the DataBinding framework nullates the value of variable before using it, and if there is a cascading call, nullates it before using it.

With the likes binding example above, let’s take a look at how the DataBinding compiler actually implements null validation. As a side note, the actual bindings between controls and data completion occur in the executeBindings method of the bindingImpl class (which is also automatically generated).

# ObservableFieldProfileBindingImpl
protected void executeBindings() {... / / TestProfile ObservableInt object androidx. Databinding. ObservableInt userLikes = null; / / mUser com for the actual binding. Example. Android. Databinding. Basicsample. Data. TestProfile user = mUser; //ObservableInt ObservableInt userLikesGet = 0;if((dirtyFlags & 0x7L) ! = 0) {// first step null user nullif(user ! = null) { //readuser.likes userLikes = user.getLikes(); } updateRegistration(0, userLikes); ObservableInt ObservableInt ObservableIntif(userLikes ! = null) { //readuser.likes.get() userLikesGet = userLikes.get(); }... }}Copy the code

How is bind done?

Before we answer the second question, let’s consider this: How do custom views support DataBinding for their custom properties?

Consider the following example:

class MyCustomView : View { var mDrawable: Drawable? = null constructor(context: Context? , attrs: AttributeSet?) : super(context, attrs) { val typedArray = context!! .theme.obtainStyledAttributes(attrs, R.styleable.MyCustomView, 0, 0) mDrawable = typedArray.getDrawable(R.styleable.MyCustomView_img) typedArray.recycle() } override fun onDraw(canvas: Canvas?) { mDrawable? .run {setBounds(0, 0, this.intrinsicWidth, this.intrinsicHeight) draw(canvas!!) }}}Copy the code

MyCustomView is a custom view that internally defines a custom attribute img of type Drawable to draw on the canvas.

The test_custom_view.xml layout used 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="myDrawable"
            type="android.graphics.drawable.Drawable" />
    </data>
    <com.example.android.databinding.basicsample.ui.MyCustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:img="@{myDrawable}" />
</layout>
Copy the code

The binding code in the activity:

val binding: TestCustomViewBinding = DataBindingUtil.setContentView(this, R.layout.test_custom_view)
binding.myDrawable = resources.getDrawable(R.mipmap.ic_launcher)
Copy the code

If run at this point, an error will be reported.

****/ data binding error ****msg:Cannot find the setter for attribute 'app:img' with parameter type android.graphics.drawable.Drawable on com.example.android.databinding.basicsample.ui.MyCustomView.
file:/android-databinding/BasicSample/app/src/main/res/layout/test_custom_view.xml loc:14:19 - 14:28 ****\ data binding error ****
Copy the code

We can already infer from the error message that the binding is done through the setter method of the app:img property. In fact, this setter method matches the above setter method of the binding entity. That is, the corresponding setter methods of img, including the img() and setImg() methods, An error is reported if this setter method is not found in the custom view. We solved the problem by adding setImg to MyCustomView.

class MyCustomView : View {
    ...
    fun setImg(d: Drawable) {
        mDrawable = d
    }
}
Copy the code

But often the actual situation is that the custom view is not written by ourselves, and there is no setter method corresponding to the control property in the source code, and we can not directly modify its source code, how to deal with this?

@ BindAdapter annotations

The @BindAdapter annotation helps us do this, and is located in the Databinding-common library, which defines all the annotations supported by databinding. Assuming that the img set method in MyCustomView is signed setMyImg, we simply add a static method in any class that declares this annotation.

# BindingAdapters.kt. @BindingAdapter("img")
@JvmStatic
fun setImg(view: MyCustomView, drawable: Drawable) {
    view.setMyImg(drawable)
}
Copy the code

This static method name is optional, the namespace of the IMG attribute in the annotation is optional, and any namespace declared in the XML is not wildcard. The important thing is the parameter signature. The first parameter must be the name of the bound control, and the second parameter must be the type of the img attribute to be bound. Both parameters are valid only if they match the binding declaration in XML. You can declare a function like this if you want to get the pre-binding value.

@BindingAdapter("img"Fun YYy (view: MyCustomView, old: Drawable? , drawable: Drawable) { view.setMyImg(drawable) }Copy the code

Multiple properties can be bound using one method at a time, written like this:

@BindingAdapter(value = ["property1"."property2"], requireAll = false)
Copy the code

RequireAll indicates whether the static method is executed only if all properties declared in the value array are bound. The default is true. This detail is not expanded here.

@ BindingMethod annotations

Note that our binding implementation in the above example simply calls the view.setmyimg (drawable) method. Such cases can be implemented in a more concise form, the @bindingMethod annotation, which essentially describes a mapping. In our case, the img attribute is mapped to the setMyImg method.

@BindingMethods(BindingMethod(type = MyCustomView::class, attribute = "img", method = "setMyImg"))
class MyCustomView : View {
    ...
}
Copy the code

If we cannot modify the MyCustomView source code, we can bind it by creating any class and declaring BindingMethods annotations for it.

With this we can answer the second question from the beginning: Do all attributes of a View support binding? Such as padding? Margin? Why is that?

With the above analysis, the answer to this question can be translated into the following three sub-questions:

  1. View source code whether there is a signature to meet the requirements of the setPadding, setMargin method.
  2. Is there a static method in the Databinding framework that declares @bindAdapter (” Android :padding”) annotations with the required signature?
  3. Is there an @bindingMethod annotation declared in the Databinding framework that is mapped in the View source code to implement setPadding/setMargin?

SetPadding (int left, int top, int right, int bottom) does not meet the method signature requirements, so 1 does not satisfy either method signature requirements.

In the Databinding – Adapters library, you can define extended binding relationships for common components, such as View corresponding to its ViewBindingAdapter and TextView corresponding to its TextViewBindingAdapter.

Where the ViewBindingAdapter completes the binding of the padding property.

@BindingMethods({
        @BindingMethod(type = View.class, attribute = "android:backgroundTint", method = "setBackgroundTintList"),
        @BindingMethod(type = View.class, attribute = "android:nextFocusLeft", method = "setNextFocusLeftId"),... @BindingMethod(type = View.class, attribute = "android:onLongClick", method = "setOnLongClickListener"),
        @BindingMethod(type = View.class, attribute = "android:onTouch", method = "setOnTouchListener"),
})
public class ViewBindingAdapter {
    @BindingAdapter({"android:padding"})
    public static void setPadding(View view, floatpaddingFloat) { final int padding = pixelsToDimensionPixelSize(paddingFloat); view.setPadding(padding, padding, padding, padding); }... }Copy the code

However, there is no margin attribute, and there is no mapping implementation in its @bindingMethod declaration. Therefore, it is concluded that android:padding attribute supports binding, while margin does not. Taking layout_marginBottom as an example, here is a simple implementation.

@BindingAdapter("android:layout_marginBottom")
public static void setBottomMargin(View view, float bottomMargin) {
    MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
    layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin,
        layoutParams.rightMargin, Math.round(bottomMargin));
    view.setLayoutParams(layoutParams);
}
Copy the code

One-way binding

The previous section explained how controls and data are bound, but in a real world scenario where data is constantly changing, how can changes be reflected in the UI in a timely manner? This is what one-way binding is about in this section.

One-way binding can be implemented in two ways

  • Use the ObservableXxx type instead of the existing type
  • Entities inherit from the BaseObservable class and declare property getters with the @bindable annotation.

ObservableXxx

Here XXX contains both basic and collection types, such as int for ObservableInt, Boolean for ObservableBoolean, ArrayList for ObservableArrayList, Reference types use an ObservableField uniformly, such as String for ObservableField. For convenience, ObservableXxx is represented by an ObservableField uniformly below.

For example, suppose our entity looks like this:

data class ObservableFieldProfile(
        var likes: Int
)
Copy the code

Observable_field_profile. XML layout

<layout

    <data>
        <variable
            name="user"
            type="com.example.android.databinding.basicsample.data.ObservableFieldProfile" />
    </data>

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content". android:src="@{user.likes < 4? R.drawable.ic1 : R.drawable.ic2 }"/>
</layout>
Copy the code

Binding code

val binding: ObservableFieldProfileBinding =
        DataBindingUtil.setContentView(this, R.layout.observable_field_profile)
val user = ObservableFieldProfile(0)
binding.user = user
Copy the code

When we change the properties of the user. Likes =10, we do not trigger a UI refresh. To achieve a synchronous refresh, we simply change the type of the likes from Int to ObservableInt.

data class ObservableFieldProfile(
        val likes: ObservableInt
)
Copy the code

Also use user.likes. Set (10) for data changes.

BaseObservable + @bindable annotation

One problem with this approach is that the type of the entity changes, which in a real project would require modification wherever this attribute is used. We could write it a different way.

class ObservableFieldProfile(var name: String, val lastName: String) : BaseObservable() {

    constructor(name: String, lastName: String, likes: Int) : this(name, lastName) {
        this.likes = likes
    }

    @get:Bindable
    var likes: Int = 0
        set(value) {
            field = value
            notifyPropertyChanged(BR.likes)
        }
}
Copy the code
  1. Make the entity class inherit from the BaseObservable class.
  2. Add a Bindable annotation to the property’s set method.
  3. The custom set method calls notifyPropertyChanged to trigger the refresh.

The notifyPropertyChanged method inherits from BaseObservable and takes br.likes. The BR class is automatically generated by the Databinding compiler and internally defines a unique identifier of type int for all mutable objects. These mutable objects include:

  • Variable declared in the XML layout
  • The property name of the method identified with the @bindable annotation (the property doesn’t have to exist, just have a getter).

That’s why it’s important to declare a Bindable annotation so that you can specify a particular attribute to refresh; And because you can write arbitrary code inside the getter, it’s much more maneuverable.

Compared to ObservableField, there is no need to change the type of the property, but the corresponding entity class faces more problems:

  1. Using inheritance instead of implementation is not feasible if the entity class itself has an inheritance relationship (if you must do this, you can only implement the Observable interface and copy the BaseObservable’s internal implementation into the existing entity).
  2. We need to rewrite the set method, but Kotlin is too unfriendly to use set for attributes inside a data type. Look what a simple data class looks like. .

The relationship between the two

ObservableField is implemented based on a BaseObservable.

With data-driven UI changes, how does the Databinding framework know which UI controls to update, in other words, how does it associate changeable data with UI controls?

The binding principle

The reason for asking this question is that you often encounter scenarios like this in development. For example, a TextView needs to be bound to an ObservableInt, but it needs to do a lot of judgment and conversion work before binding, and it would be very unreadable to write all this logic in a BIND expression, so the usual approach is to encapsulate the judgment and conversion into a method, Call the method directly in the BIND expression. What seems reasonable can be buggy in practice.

Look at the following example, the ViewModel looks like this:

class ProfileLiveDataViewModel : ViewModel() {
    val likes: MutableLiveData<Int> = MutableLiveData(0)
}
Copy the code

Here’s the layout file.

<layout
    xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewmodel"
            type="com.example.android.databinding.basicsample.data.ProfileLiveDataViewModel"/>
    </data>
    
    <TextView
        android:id="@+id/likes"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{Integer.toString(viewmodel.likes+1)}"
        ./>
Copy the code

We can see from the contents of the bind expression that we need to convert likes plus 1 to a String to the text property. Android :text=”@{viewModel.testConvert (viewmodel.likes)}”, where the testConvert method is added to the viewModel to encapsulate the conversion action.

# viewmodel
fun testConvert(likes: Int): String {
    return Integer.toString(likes + 1)
}
Copy the code

Android :text=”@{viewModel.testconvert2 ()}” android:text=”@{viewModel.testconvert2 ()}”

# viewmodel
fun testConvert2(): String {
    returnInteger.toString(likes.value? .plus(1) ? : 0)}Copy the code

This seemingly simple change has caused a bug———— that if we update the value of likes, it will not be synchronized to the view.

Why is that? For a brief explanation, this is related to parsing the bind expression, which looks for mutable parts of the expression. The mutable parts include variable, ObservableField, getter marked with the @bindable annotation, LiveData, and duplicate items are merged. The comments are eventually printed in the generated ViewBindingImpl class.

// dirty flag
private  long mDirtyFlags = 0xffffffffffffffffL;
/* flag mapping
    flag 0 (0x1L): viewmodel.likes
    flag 1 (0x2L): viewmodel.lastName
    flag 2 (0x3L): viewmodel.popularity
    flag 3 (0x4L): viewmodel.name
    flag 4 (0x5L): viewmodel
    flag 5 (0x6L): null
flag mapping end*/
//end
Copy the code

MDirtyFlags is a refresh flag, which is used to determine which view is refreshed when the data changes. Flag Mapping is the mapping flag for each refresh item. All the changeable parts are displayed here. In our example android:text=”@{viewModel.testconvert2 ()}” only maps to variable– viewModel, i.e. the view is refreshed only when the viewModel changes. The internal logic of the called method testConvert2 will be ignored, and Android :text=”@{viewmodel.testconvert (viewmodel.likes)}” will map to viewModel and viewmodel.likes, Any change in either of these will refresh the view.

Two-way binding

If a one-way binding is data-driven, then a two-way binding is data-driven + event-driven, identified by an @={} expression.

So to implement bidirectional binding, that is, to add event-driven logic on top of one-way binding. Event-driven is ultimately manifested in data changes, so you can summarize the three steps to achieve event-driven.

  1. Listen for event changes and throw them to the framework
  2. Gets the current property value of the control
  3. Update data with the current property value

Some properties of common controls have been officially bidirectional binding.

Because EditText inherits from TextView, we use the text property of EditText and TextViewBindingAdapter source code to illustrate the process of reverse binding.

# TextViewBindingAdapter1 //event can default @inversebindingAdapter (attribute ="android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
    returnview.getText().toString(); } ② @bindingAdapter (value = {... ."android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, ... , final InverseBindingListener textAttrChanged) { final TextWatcher newValue; . newValue = newTextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { ... } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { ... // Received an event notification framework changeif(textAttrChanged ! = null) { textAttrChanged.onChange(); } } @Override public void afterTextChanged(Editable s) { ... }}; .if (newValue != null) {
        view.addTextChangedListener(newValue);
    }
}
Copy the code

For EditText, the event that drives data changes is obviously the TextChangedListener, which calls the onTextChanged method whenever the input character changes, The dataBinding framework uses an InverseBindingListener interface for users to throw event notifications. The InverseBindingListener interface provides the framework with a binding attribute called textAttrChanged, which by default consists of the attribute name +AttrChanged (code ② above). The InverseBindingListener interface concrete implementation code is generated by the framework and basically gets the current value of a control property and updates the data with that value.

To get the current value, we need to tell the framework that the usual solution is:

  • Use @inversebindingAdapter to annotate a static method that returns the current value of a control property such as the code above.
  • Annotate a class with @inversebindingMethod and declare the inverse binding for the control, properties, inverse event name (default), and the current value of the property within the control (default). Such as:
@InverseBindingMethods({@InverseBindingMethod(
     type = android.widget.TextView.class,
     attribute = "android:text",
     event = "android:textAttrChanged",
     method = "getText")})
 public class MyTextViewBindingAdapters
Copy the code

Avoid infinite loops

As you can see from the legend, if the event-driven reverse binding is successful and the data changes, logical logic would continue to trigger one-way binding, which would lead to an infinite loop.

To break this loop, it is common practice to verify that the old and new data are the same before updating the UI, and not refresh if they are. TextView, for example, has a setText method that does not check for consistency between old and new data, so the Android: Text property is rebound to the TextViewBindingAdapter and the validation logic is added.

@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence Text) {// Verify final CharSequence oldText = view.gettext ();if (text == oldText || (text == null && oldText.length() == 0)) {
        return; }... view.setText(text); }Copy the code

The commonly used skill

  1. Default Value Specifies a default value when binding data is not already assignedandroid:text='@{user.firstName, default="Placeholder text"}'.
  2. The triadic operation can be replaced by “??” Operators such as:android:text="@{user.displayName ?? user.lastName}"Is equivalent toandroid:text="@{user.displayName ! = null ? user.displayName : user.lastName}".
  3. String placeholder you can use that,android:text="@{@string/nameFormat(firstName, lastName)}".
  4. The binding expression escapes characters
    • With “<” symbol&lt;Instead of
    • Use “&” symbol&amp;Instead of