Recommended reading

DataBinding

DataBinding usage tutorial (3) : detailed explanation of each annotation

Android official architecture component Databinding-ex: Bidirectional binding

Have you ever used DataBinding? What are the advantages and disadvantages?

The DataBinding library (DataBinding) allows us to declaratively bind interface components in a layout to data sources in an application.

Introduction to use

Add the DataBinding element to the build.gradle file where the DataBinding module is needed:

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

Note: Even if application modules do not use data binding directly, you must configure data binding for application modules that depend on libraries that use data binding. If DataBinding is used in the module on which App_module depends, app_module also needs to set DataBinding = true. Otherwise, an error NoClassDefFoundError will be reported.

tip

Quickly generate a DataBinding layout

Put the cursor on the outermost layer of the layout and run Alt + Enter to select Covert to Data Binding Layout to generate the corresponding layout

Layout and binding expressions

Data binding

DataBinding generates a bound class for each layout file. By default, the class name is based on the name of the layout file, which is converted to Pascal case with a Binding suffix added to the end. Add the layout file named activity_main.xml, so the generated corresponding class is ActivityMainBinding.

Activity_main. XML:

<? The XML version = "1.0" encoding = "utf-8"? > <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.sample.jetpack.databinding.User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{@string/name_format(user.firstName, user.lastName)}" /> </LinearLayout> </layout>Copy the code

Bind data as follows:

Method 1: Use DataBindingUtil

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding = DataBindingUtil.setContentView<ActivityDataBindBinding>(this, R.layout.activity_data_bind)
    binding.user = User("Test", "User")
}
Copy the code

Method 2: Use bindings to generate classes

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    binding.user = User("Test", "User")
}
Copy the code

Null merge operator

Nulljudge syntax in DataBinding is simpler:

android:text="@{user.displayName ?? user.lastName}"
Copy the code

This is functionally equivalent to:

android:text="@{user.displayName ! = null ? user.displayName : user.lastName}"Copy the code

Avoid Null pointer exceptions

The generated data binding code automatically checks for null values and avoids null pointer exceptions. For example, in the expression @{user.name}, if user is Null, the default value Null is assigned to user.name. If you refer to user.age, where age is of type int, the data binding uses the default value 0.

That is, DataBinding sets default values for fields referenced in the layout.

View references

Expressions can refer to other views in the layout by ID using the following syntax:

<EditText
    android:id="@+id/example_text"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"/>
<TextView
    android:id="@+id/example_output"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{exampleText.text}"/>
Copy the code

Note: The binding class converts the ID to camel case.

A collection of

You can use the [] operator to access common collections, such as arrays, lists, sparse lists, and maps.

<data>
    <import type="java.util.List"/>
</data>

android:text="@{list[index]}"
Copy the code

String literals

You can enclose property values in single quotes so that double quotes can be used in expressions:

android:text='@{map["firstName"]}'
Copy the code

You can also enclose property values in double quotes. If you do, you should also enclose string literals in backsingle quotes:

android:text="@{map[`firstName`]}"
Copy the code

Resource, fill string

Referencing application resources in an expression is consistent with normal use:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
Copy the code

A string can be filled with arguments:

android:text="@{@string/nameFormat(firstName, lastName)}"
Copy the code

The event processing

The event handling here is basically specific to the onClick event; other event handling can be done by binding the adapter. DataBinding has two ways of handling events:

  • Method references
  • Listener binding

The main difference between method references and listener bindings is that the actual listener implementation is created when the data is bound, not when the event is fired. If you want to evaluate an expression when an event occurs, you should use a listener binding. Method references create event listeners when the data is bound, and listener bindings are created when the event occurs.

Method references

Method references are written as follows:

<? The XML version = "1.0" encoding = "utf-8"? > <layout> <data> <variable name="handle" type="***.MyHandle" /> </data> <LinearLayout> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="OnClick" android:onClick="@{handle::onHandleClick}"/> </LinearLayout> </layout> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding.handle = MyHandle() } class MyHandle { fun onHandleClick(view: View) { } }Copy the code

If this looks familiar, it is one of the three implementations of OnClickListener that specifies the onClick event in XML.

The requirements are the same, that is, the parameters of the Listener implementation method should be the same. For OnClickListener, the method parameter must have only one Viiew.

Listener binding

The listener binding is written as a lambda expression as follows:

<? The XML version = "1.0" encoding = "utf-8"? > <layout> <data> <variable name="handle" type="***.MyHandle" /> </data> <LinearLayout> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="OnClick" android:onClick="@{(view) -> handle.onHandleClick()}"/> </LinearLayout> </layout> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding.handle = MyHandle() } class MyHandle { fun onHandleClick() { } }Copy the code

The expression for the listener binding is written in much the same way as using Java + lambda

button.setOnClickListener { view -> onHandleClick() }

fun onHandleClick() {
}
Copy the code

The difference between the two methods is: Method references: parameters cannot be modified. Listener binding: You can add any parameter, but the return value must be the same as that of the listener implementation method.

Import, variable, include

Import:

import android.view.View
import java.util.List

<import type="android.view.View"/>
<import type="java.util.List"/>
Copy the code

Variable definition:

<variable name="image" type="Drawable"/>
Copy the code

The
tag is a variable declaration. The name represents the name of the variable and the type attribute represents the type of the variable.

Data binding can be passed using the bind: variable name:

<? The XML version = "1.0" encoding = "utf-8"? > <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> <include layout="@layout/name" Bind :user="@{user}" /> </LinearLayout> </layout> <? XML version="1.0" Encoding =" UTF-8 "?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.User" /> </data> <merge><! -- Doesn't work --> <include layout="@layout/name" bind:user="@{user}" /> </merge> </layout>Copy the code

Use observable data objects

slightly

The generated binding class

View with ID

The data binding library creates immutable fields in the binding class for each view with an ID in the layout. The field name is the same as the ID name. Farewell findViewById and better performance than ButterKnife.

Instant binding

When a mutable or observable changes, the binding is scheduled to change before the next frame. But sometimes the binding must be performed immediately. To enforce this, use the executePendingBindings() method.

Senior binding

Dynamic variables Sometimes the system does not know about a particular bound class. For example, a RecyclerView.Adapter that runs for arbitrary layouts does not know about specific binding classes. The binding value must still be specified when the onBindViewHolder() method is called.

Use DataBinding in RecyclerView as follows:

class DataBindingActivity : AppCompatActivity() {
    private val itemColorList = mutableListOf(
            Item("#CD950C"),Item("#8B658B"),Item("#FFC1C1"),
            Item("#EEB4B4"),Item("#CD9B9B"),Item("#8B6969"),
            Item("#FF6A6A"),Item("#EE6363"),Item("#CD5555"))

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = MainAdapter(this, itemColorList)
    }
}

class MainAdapter(private val context: Context, private val colorList: List<Item>) :
        RecyclerView.Adapter<MainViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
        return MainViewHolder(DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.list_item, parent, false))
    }

    override fun getItemCount(): Int {
        return colorList.size
    }

    override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
        holder.binding.setVariable(BR.item, colorList[position])
        holder.binding.executePendingBindings()
    }
}

class MainViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
Copy the code

The BR is automatically generated and contains the ID of the resource used for data binding. In the above code, the BR may not be imported automatically, so you need to add the apply plugin to build.gradle: ‘kotlin – kapt’, and then manually add the import androidx. Databinding. If baseAdapters. BR import.

Another way to bind a view in onBindViewHolder is as follows:

override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
    holder.binding.item = colorList[position]
    holder.binding.executePendingBindings()
}
Copy the code

The BR class is not required, and setVariable is also internally setItem.

Custom binding class name

The default binding class name generation rule is described above. You can customize the binding class name through the class attribute.

<data class="ContactItem">
</data>
Copy the code

Binding adapter

There are many annotations to use, so add them to build.gradle:

apply plugin: 'kotlin-kapt'
Copy the code

Automatic selection method

For properties that use DataBinding to bind data, DataBinding automatically tries to find setter methods for the property in the source code. For example, the setter for Android :text=”” is setText(). DataBinding does not consider the namespace of the attribute during the lookup, but only the attribute name and parameter type.

For attributes not defined in the Android: namespace, you can use the custom namespace app:, for example:

<android.support.v4.widget.DrawerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:scrimColor="@{@color/scrim}"
        app:drawerListener="@{fragment.drawerListener}">
Copy the code

Specify the custom method name

For properties that have non-conforming setter methods, such as Android :tint, which correspond to setImageTintList() instead of the expected setTint(), This requires manual binding using @BindingMethods, @BindingMethods annotations.

@BindingMethods(value = [
    BindingMethod(
        type = android.widget.ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList")])
Copy the code

Then, for tint properties that use DataBinding, DataBinding looks for the setImageTintList method and calls it.

@bindingMethods: Declare a set of @bindingMethods. @bindingMethod: Have 3 fields, all 3 fields are required, no less than one field:

  • type: Which View class is the property to operate on. The type is a class object, such as imageView.class
  • attribute: XML attribute, type String, for example: “Android :tint”
  • method: Specifies the set method of type String for the XML attribute, such as “setImageTintList”.

Note: We don’t need to use these annotations in most cases, Android already sets BindingMethod for most properties. Look at binding classes such as TextViewBindingAdapter.

Provides custom logic

Both methods are for properties that have setter methods associated with them. For non-setter properties that need to bind to a method, the @BindingAdapter annotation is required.

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}
Copy the code

After the @bindingAdapter annotation is added, DataBinding will call setPaddingLeft(view: View, padding: Int).

Note how to use @bindingAdapter annotations:

  • The annotation method must bePublic static methods;
  • The first parameter of the method must be of typeView.

There are two ways to use the @BindingAdapter annotation method in Kotlin:

First, mark a static method with the @jVMStatic annotation:

companion object {
    @BindingAdapter("android:paddingLeft")
    @JvmStatic
    fun setPaddingLeft(view: View, padding: Int) {
        view.setPadding(padding,
            view.getPaddingTop(),
            view.getPaddingRight(),
            view.getPaddingBottom())
    }
}
Copy the code

Second, the simplest is to declare it directly at the top level:

package com.xxx.xxx

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}
Copy the code

DataBinding’s event handling typically can only handle an interface or abstract class that has only one abstract method, such as OnClickListener, or an @BindingAdapter that has multiple abstract methods, such as TextWatcher.

Android has provided us TextWatcher event processing, can view androidx. Databinding. Adapters. TextViewBindingAdapter in source view.

Object conversion

Automatic conversion object

When the binding expression returns Object, DataBinding selects the method by which the user sets the property value. Object is converted to the parameter type of the selected method.

Custom conversion

When the return value type of a binding expression is inconsistent with the method parameter that sets the property, you can customize the conversion using the @bindingConversion annotation. For example, android:background property:

<View
    android:background="@{isError ? @color/red : @color/white}"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
Copy the code

Int should be converted to ColorDrawable whenever a Drawable is required and an integer is returned.

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}
Copy the code

However, the value types provided in the binding expression must be consistent.

<View
    android:background="@{isError ? @drawable/error : @color/white}"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
Copy the code

Bind layout views to architectural components

Use LiveData to notify the interface of data changes

Note: Add the setLifecycleOwner setting to declare the cycle owner when using a LiveData object as a data binding source. Otherwise, when the LiveData data value changes, the UI will not change.

class ViewModelActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)

        binding.setLifecycleOwner(this)
    }
}
Copy the code

Use ViewModel to manage interface data

slightly

Use the Observable ViewModel for better control of the binding adapter

Most of us prefer the ViewModel + LiveData approach to managing page data because it’s easier. There is no more fine-grained control over the data source, such as customizing data assignment operations or controlling when notifications of data changes are sent.

Class UserObservable: BaseViewModelObservable() {// The Bindable annotation generates an entry in the BR class file. String = "" set(value) { field = if (value.length > 1) value.substring(0, 1) + "-" + value else value saveData() notifyPropertyChanged(BR.userName) } fun updateUserNameValue() { userName = "${Random.nextInt(100)}" } fun saveData() { // do something } }Copy the code

Two-way data binding

Two-way data binding means that the source data and the UI interact. When one changes, the other changes.

To implement bidirectional data binding, the following conditions must be met:

  • Source data can be listened on
  • @ = {}notation

Custom feature bidirectional data binding

Android already implements bidirectional data binding for some common attributes. Such as TextViewBindingAdapter.

The @inversebindingAdapter and @InversebindingMethod annotations are required to add two-way data binding to custom attributes.

The BindingAdapter section introduces the @bindingadapter and @bindingmethod annotations, which correspond to each other’s innverse annotations.

The @inversebindingAdapter annotation contains two fields:

  • attributeAttribute name:
  • event: The event to be called because the value has been changed

Here is a custom view:

class CustomEditText(context: Context, attrs: AttributeSet?) : androidx.appcompat.widget.AppCompatEditText(context, attrs)
Copy the code
class CustomEditTextBindingAdapter { companion object { @BindingAdapter("app:customTextAttrChanged") @JvmStatic fun setChangeListener(view: CustomEditText, attrChange: InverseBindingListener?) { val newValue: TextWatcher = object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { attrChange? .onChange() } override fun afterTextChanged(s: Editable) { } } val oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher) if (oldValue ! = null) { view.removeTextChangedListener(oldValue) } if (newValue ! = null) { view.addTextChangedListener(newValue) } } @BindingAdapter("app:customText") @JvmStatic fun setCustomText(view:  CustomEditText, text: String) { val oldText: CharSequence? = view.text if (text == oldText) { return } view.setText(text) } @InverseBindingAdapter(attribute = "app:customText", event = "app:customTextAttrChanged") @JvmStatic fun getCustomText(view: CustomEditText): String { return view.text.toString() } } }Copy the code

Note: Each bidirectional binding generates a composite event property. This feature has the same name as the base feature, but contains the suffix AttrChanged. The synthesized event feature allows libraries to create methods that use the @BindingAdapter annotation to associate event listeners with the corresponding View instance.

To implement bidirectional data binding for custom attributes, note the following:

  • Must implement add@BindingAdapterannotationsAttrChangedComposite events.
  • Must be executed within the event methodonChange()Method callback.

Looking at the source code, the AttrChanged event is mandatory, and the UI changes the data source to fall on the binding class. InverseBindingListener. OnChange () method, that is to say, only the onChange () method is called, the data source to be updated. So the onChange() in the TextViewBindingAdapter is in the onTextChanged() callback to ensure that the data is updated in both directions in real time.

The bidirectional binding section of the document is particularly light on AttrChanged events, which can be misleading, and the difficulty of locating DataBinding problems is currently a headache.

An infinite loop of two-way data binding

The problem of infinite loops is easy to understand, so you must compare old and new data before assigning new values.

converter

If you want to transform bidirectionally bound data before displaying it on the UI, you need to add conversion methods and inverse conversion methods.

object Converter {
    @InverseMethod("stringToInt")
    @JvmStatic
    fun intToString(newValue: Int): String {
        return newValue.toString()
    }

    @JvmStatic
    fun stringToInt(newValue: String): Int {
        return newValue.toInt()
    }
}
Copy the code

Annotate the transformation method with @inversemethod. The annotation has only one field that corresponds to the method name of the reverse conversion method.

Note: The dateToString conversion method is incorrectly written in the Android documentation for testing. You can see the rules for conversion methods and inverse conversion methods in the @inversemethod annotation. The number of arguments in the two methods should be the same, and all but the last argument should be the return value of each other’s oppositions. The parameter can be more than one, but bidirectional binding cannot be performed even after the parameter is more than one in the actual test. So let’s just add a parameter.

Finally, the bidirectional binding part of the document is written so casually -_-! .

The problem record

java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/databinding/DataBinderMapperImpl;

Estimates that meet this kind of problem with the modular, wrote in the component dataBinding {enabled = true} but didn’t write main components, and then run the main component of time error, only need to in the main components are added dataBinding {enabled = true}.

thinking

DataBinding took nearly two weekends to sort through the theory, which makes you wonder what the meaning of DataBinding is. DataBinding has a mixed reaction. Since it has not been used in actual development, so it is a set of praise and criticism.

The advantages are mainly concentrated in:

  • Don’t have to writefindViewByIdTo improve performance.
  • The view layer is separated from the data layer.

Disadvantages mainly focus on:

  • It is difficult to locatebug. When something goes wrong, you basically see itDataBindingClass could not be generated, for specific reasons there is no need to check the code yourself.
  • ASDataBindingThe support is not friendly and the code completion is a bit poor.
  • Learning costs are high.

Learning about Jetpack makes it clear that Google’s intent is to provide a common development specification and architecture for Android developers. DataBinding is an important part of the MVVM architecture, separating the view layer from the data layer, and while it still has its problems, it is still a good library to use.