Data Binding class library

This document will teach you how to use the Data Binding library to write declarative layouts and minimize Binding of glue code to your application logic and layout. Data Binding is a flexible and widely compatible library. It is a support library, so you can use it on any Android 2.1(API Level 7+) or above. In order to use Data Binding, Android Gradle plugin version must be 1.5.0-alpha1 or above. See how to upgrade your Gradle plugin.

Build environment

To get a Data Binding, go to the Android SDK Manager and download its support library. Add dataBinding to build.gradle in your application Module to make your application support Data Binding. Configure a Data Binding with the following code snippet:

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

If you have an application module that uses a library that relies on Data Binding, configure Data Binding for that module as well. Also, if you want to use Data Binding, your Android Studio version must be equal to or greater than 1.3.


Data Binding layout file

Write your first Data Binding expression

The Data Binding layout file is a bit different. It has a Layout tag as the root tag and a Data element and a view element as child tags. The view element is the layout file you would have if you hadn’t used a Data Binding. Here’s an example:

<? xml version="1.0" encoding="utf-8"? > <layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>
Copy the code

The variable under the data tag is the object you might use in the data Binding layout file.

<variable name="user" type="com.example.User"/>
Copy the code

The layout uses the @{} syntax to wrap object properties in variable. In the following example, the value of the text property of the TextView is replaced by the firstName property of the user.

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>
Copy the code

The data object

Now let’s assume you have a plain Java object (POJO) User:

public class User { public final String firstName; public final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; }}Copy the code

This object property (final modification) is immutable, which is common if your data object is only read-only and cannot be modified later. We can also represent it as a JavaBeans object:

public class User {
   private final String firstName;
   private final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
   public String getFirstName() {
       return this.firstName;
   }
   public String getLastName() {
       returnthis.lastName; }}Copy the code

From a data binding perspective, these two classes are equivalent. The android:text property of TextView gets the value of the fistName field from the first class and the value returned by the getFirstName() method from the second class using the @{user.firstName} expression. In addition, the firstName() method can be retrieved if it exists.

Data binding

By default, a Binding class is generated based on the name of the layout file, converted to Pascal format with Binding as its suffix. The layout file above is named main_activity.xml, so the generated binding class is MainActivityBinding. This class binds layout properties (such as user variables) to the layout’s view and knows how to assign values via expressions. The easiest way to create a binding class is with a view inflate:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test"."User");
   binding.setUser(user);
}
Copy the code

Done! Run the application and you will see the User for the test in the interface. Alternatively, you can get the binding class in several ways:

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
Copy the code

If you use data binding phones in ListView or RecyclerView, you can get binding classes in several ways:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
Copy the code

The event processing

Data binding allows you to write expressions to handle events (such as onClick) dispatched from a view. With few exceptions, the event attribute name is determined by the method name in the listener. For example, view.onLongClickListener has an onLongClick() method, so the property of this event is Android: onLongClick. There are two ways to handle an event.

  • Method references: In an expression, you can refer to methods that match the listener method signature. When the expression is evaluated as a method reference, the data binding wraps the method reference and owner object in a listener that is set up on the target view. If the expression is evaluated as null, the data binding does not create a listener, but sets an empty listener.
  • Listener binding: Lambda expressions are evaluated when an event occurs. Data binding always creates a listener on the view. When the event is sent, the listener evaluates the lambda expression.

Method references

Events can be tied directly to the processing method, just as android:onClick can be used as a method of an Activity. A major advantage over the View# onClick property is that the expression is processed at compile time, so you will receive a compile-time error if the method does not exist or if it is incorrectly signed.

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.

To assign an event to its handler, use a regular binding expression whose value is the name of the method to invoke. For example, if your data object has two methods:

public class MyHandlers { public void onClickFriend(View view) { ... }}Copy the code

Binding expressions can assign a click listener to a View:

<? xml version="1.0" encoding="utf-8"? > <layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>
Copy the code

Note that the signature of the method in the expression must exactly match the signature of the method in the listener object.

Listener binding

A listener binding is a binding expression that runs when an event occurs. Similar to method references, but allows you to run arbitrary data binding expressions. This feature works with Gradle 2.0 and later Android Gradle plug-ins.

In a method reference, the method parameters must match the parameters of the event listener. In a listener binding, only your return value must match the listener’s expected return value (unless it returns void). For example, you could have a Presenter class with the following methods:

public class Presenter {
    public void onSaveClick(Task task){}
}
Copy the code

You can then bind your click events to your class, for example:

<? xml version="1.0" encoding="utf-8"? > <layout xmlns:android="http://schemas.android.com/apk/res/android">
      <data>
          <variable name="task" type="com.android.example.Task" />
          <variable name="presenter" type="com.android.example.Presenter" />
      </data>
      <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
          <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
          android:onClick="@{() -> presenter.onSaveClick(task)}" />
      </LinearLayout>
  </layout>
Copy the code

Listeners can only allow lambda expressions as root elements. When there are callbacks in the expression, the data binding automatically creates the necessary listeners and registries for the event. When the view fires the event, the data binding evaluates the given expression. Just like in regular binding expressions, you can still get null data binding values and be thread-safe when these listener expressions are evaluated.

Note that in the example above, we did not define the view parameter passed in onClick(Android.view.view). The listener binding provides two options for listener parameters: you can ignore all of the method’s parameters or name them all. If you want to name parameters, you can use them in expressions. For example, the above expression could be written as:

android:onClick="@{(view) -> presenter.onSaveClick(task)}"
Copy the code

Or if you want to use arguments in an expression, you can do something like this:

public class Presenter {
    public void onSaveClick(View view, Task task){}
}
Copy the code
 android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
Copy the code

You can use multiple arguments in lambda expressions:

public class Presenter {
    public void onCompletedChanged(Task task, boolean completed){}
}
Copy the code
<CheckBox 
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
Copy the code

If the event you are listening for returns a value other than void, the expression must return a value of the same type. For example, if you want to listen for long-press events, your expression should return a Boolean value.

public class Presenter {
    public boolean onLongClick(View view, Task task){}
}
Copy the code
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
Copy the code

If the expression cannot be evaluated because of an empty object, a Data Binding returns the default Java value for that type. For example, reference type null, int 0, Boolean false, and so on.

If you need to use expressions with predicates (such as ternary), you can use void as the symbol.

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
Copy the code

Avoid complex listeners

Listener expressions are very powerful and can make your code very easy to read. On the other hand, listeners that contain complex expressions can make your layout difficult to read and maintain. These expressions should be as simple as passing available data from the UI to the callback method. You should implement the business logic from within the callback method invoked by the listener expression. There are specialized click event handlers that require properties other than Android :onClick to avoid collisions. The following attributes have been created to avoid this conflict:

Class Listener Setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

Layout file details

Imports

Zero or more import elements can be used within a data element. These are just as easy to reference into your layout files as they are in Java.

<data>
    <import type="android.view.View"/>
</data>
Copy the code

The View class can now be used in your binding expressions.

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
Copy the code

If class names conflict, one of the classes needs to be aliased.

<import type="android.view.View"/>
<import type="com.example.real.estate.View" alias="Vista"/>
Copy the code

Now, in the layout file, Vista as com. Example.. Real estate introduced the View, the View is treated as android. View. The View. Imported types can be used as type references in variables and expressions:

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List< User>"/>
</data>
Copy the code

Note: Android Studio does not yet handle imports, so automatic importing of variables may not be possible in your IDE. Your application will still compile properly, and you can solve this IDE problem by using fully qualified names in the variable definitions.

<TextView
   android:text="@{((User)(user.connection)).lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
Copy the code

When referring to static fields and methods in expressions, you can also use imported types:

<data>
    <import type="com.example.MyStringUtils"/>
    <variable name="user" type="com.example.User"/ > < / data >... <TextView android:text="@{MyStringUtils.capitalize(user.lastName)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
Copy the code

Just as in a Java file, java.lang.* is automatically imported.

Variables

Any variable can be used inside the data element. Each variable represents a property that can be set in the layout to be used for binding expressions in the layout file.

<data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user"  type="com.example.User"/>
    <variable name="image" type="Drawable"/>
    <variable name="note"  type="String"/>
</data>
Copy the code

Variable types are checked at compile time, so if a variable implements an Observable or an Observable collection, it should be reflected in the type. If the variable is a base class or interface that has no Observable interface, it will not be observed!

Variables are merged when different configurations (such as landscape or landscape) have different layout files. There should be no conflicting variable definitions between these layout files.

The generated binding class sets a setter and getter method for each described variable. The variable will take the default Java value until the setter is called. Null for reference types, 0 for int, false for Boolean, and so on.

Name of the custom binding class

By default, a Binding class is generated based on the name of the layout file, starting with a capital letter, removing the underscore (_) and capitalizing the first letter of the following word, and then adding the suffix Binding. This class will be placed in the data binding package under the module package. For example, the layout file contact_item.xml will generate a ContactItemBinding. . If the package is com module. Example. My app, it will be placed in a com. Example.. My app. Databinding.

Binding classes can be renamed or placed in different packages by adjusting the class attribute of the data element. Such as:

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

This generates the binding class ContactItem in the data binding package in the module package. You can use “if the class should be generated in another package in the module package. As a prefix:

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

In this case, ContactItem is generated directly in the module package. If the full package is provided, any package can be used:

<data class="com.example.ContactItem">... </data>Copy the code

Includes

Variables can be passed from the contained layout to the contained layout’s binding by using application namespaces and variable names in attributes:

<? 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:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </LinearLayout>
</layout>
Copy the code

In this case, both name.xml and contact.xml layout files must have a user variable.

Data binding does not support include as a direct child of the Merge element. For example, the following layouts are not supported:

<? 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>
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>
Copy the code

Expression language

A common feature

The expression language looks a lot like Java expressions. These are the same:

  • Mathematical + - / * %
  • String concatenation +
  • Logical && ||
  • Binary & | ^
  • Unary + -! ~
  • Shift >> >>> <<
  • Comparison == > < >= <=
  • instanceof
  • Grouping (a)
  • Literals – character, String, numeric, null
  • Cast
  • Method calls
  • Field access
  • Array access []
  • Ternary operator ? :Such as:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
Copy the code

Missing operations

Some of the expression syntax you use in Java does not support binding operations.

  • this
  • super
  • new
  • Explicit generic calls

An empty merge operator

Null merge operator?? The result on the left (if it is not NULL) or the result on the right (if it is null) is selected.

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

Attribute references

When an expression refers to an attribute of a class, it uses the same format for fields, setters, and ObservableFields.

android:text="@{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, user.name will be assigned its default value (NULL). If you refer to user.age, where age is an int, it defaults to 0.

A collection of

Generic collections: arrays, lists, SparseArray, maps, can be easily accessed using the [] operator.

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List< String>"/>
    <variable name="sparse" type="SparseArray< String>"/>
    <variable name="map" type="Map< String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/ > < / data >... android:text="@{list[index]}"... android:text="@{sparse[index]}"... android:text="@{map[key]}"
Copy the code

String text

When single quotes are used around attribute values, double quotes are used in expressions:

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

You can also enclose attribute values with double quotes. To do so, string literals should use either single ‘or backquotes (‘).

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

resources

With normal syntax, resources can be accessed as part of an expression:

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

Format strings and complex numbers can be evaluated by providing arguments:

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

When a complex number has more than one argument, all arguments should be passed:

 Have an orange
 Have %d oranges
 android:text="@{@plurals/orange(orangeCount, orangeCount)}"
Copy the code

Some resources require explicit type evaluations:

Type Normal Reference Expression Reference
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

Data Objects

Any plain old Java object (POJO) can be used for data binding, but modifying the POJO will not result in a UI update. The real power of data binding is by giving your data objects notification when data changes. There are three different data change notification mechanisms, Observable Objects, Observable Fields, and Observable Collections.

When these observable data objects are bound to the UI, and the properties of the data object change, the UI is automatically updated.

Observable Objects

Classes that implement the Observable interface will allow a single binding listener to be attached to a binding object to listen for changes to all properties on that object.

The Observable interface adds and removes listeners, but notification is up to the developer. To simplify development, the base class BaseObservable was created to implement the listener registration mechanism. The data class implementer is still responsible for notifying properties of changes. This is done by assigning a Bindable annotation to the getter and notifying the setter.

private static class User extends BaseObservable {
   private String firstName;
   private String lastName;
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) { this.lastName = lastName; notifyPropertyChanged(BR.lastName); }}Copy the code

The Bindable annotation generates an entry in the BR class at compile time. The BR class files will be generated in the module package. If the base class of the data class does not change, the Observable interface can be implemented using convenient PropertyChangeRegistry to efficiently store and notify listeners.

ObservableFields

ObservableField and its siblings ObservableBoolean, ObservableByte, ObservableChar, ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDoubl and ObservableParcelable. ObservableFields are independent observables with a single field. The original version avoids boxing and unboxing during access operations. To use, create a public final field in your data class:

private static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}
Copy the code

That’s it! To access this value, use the set and get methods to access:

user.firstName.set("Google");
int age = user.age.get();
Copy the code

Observable Collections

Some applications use more dynamic structures to hold data, and watch collections allow key-value access to these data objects. ObservableArrayMap is useful when the key is of a reference type, such as String.

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName"."Google");
user.put("lastName"."Inc.");
user.put("age"17);Copy the code

In the layout file, map is accessed by string keys:

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap< String, Object>"/ > < / data >... <TextView android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
Copy the code

ObservableArrayList is used when the key is integer:

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
Copy the code

In the layout, the list can be accessed by index:

<data>
    <import type="android.databinding.ObservableList"/>
    <import type="com.example.my.app.Fields"/>
    <variable name="user" type="ObservableList< Object>"/ > < / data >... <TextView android:text='@{user[Fields.LAST_NAME]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
Copy the code

Generate bound classes

The generated binding class links the layout variables to the views in the layout. As mentioned earlier, the name and package of the binding may be custom. The generated binding classes all extend ViewDataBinding.

create

Bindings should be created immediately after the inflate to ensure that the View hierarchy is undisturbed. There are several ways to bind to a layout. The most common is to use static methods in bound classes. The inflate method synchronizes the View hierarchy in one step. There is a simpler version that requires only a LayoutInflater and a ViewGroup:

MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
Copy the code

If the layout uses a different mechanism inflate, it may be bound separately:

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
Copy the code

Sometimes bindings cannot be known in advance. In this case, you can create the binding using the DataBindingUtil class:

ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
Copy the code

Views With IDs

A public final field is generated for each view in the layout. This binding performs a single pass on the view hierarchy, extracting the view with its ID. This mechanism can be faster than calling multiple views with findViewById. Such as:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
   android:id="@+id/firstName"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
  android:id="@+id/lastName"/>
   </LinearLayout>
</layout>
Copy the code

A bound class is generated with the following fields:

public final TextView firstName;
public final TextView lastName;
Copy the code

IDs is not as necessary as without data binding, but there are still cases where code needs to access views.

variable

Each variable will be assigned an accessor method.

<data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user"  type="com.example.User"/>
    <variable name="image" type="Drawable"/>
    <variable name="note"  type="String"/>
</data>
Copy the code

Setters and getters are generated in the binding class:

public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);
Copy the code

ViewStubs

The ViewStub is a little different from a normal view. They start out invisible, and when they either become visible or are explicitly told that they are inflate, they replace the layout with another layout.

Since the ViewStub essentially disappears from the view hierarchy, the view in the binding object must also disappear to allow collection. Because the view is final, the ViewStubProxy object replaces the ViewStub, which the developer can access while it exists, and which view is synchronized if it is synchronized.

When you inflate another layout, you must create a binding for the new layout. Therefore, ViewStubProxy must listen ViewStub ViewStub. OnInflateListener and establish the binding at this time. Since only one can exist, ViewStubProxy allows the developer to set an OnInflateListener object after the binding is created.

Senior binding

Dynamic variables

Sometimes, the specific binding class will not be known. For example, a RecyclerView.Adapter for arbitrary layouts will not know the specific binding classes. It must still assign the binding value during onBindViewHolder(VH,int).

In this example, all the layouts of the RecyclerView binding have an Item variable. BindingHolder has a getBinding method that returns the ViewDataBinding base class.

public void onBindViewHolder(BindingHolder holder, int position) {
   final T item = mItems.get(position);
   holder.getBinding().setVariable(BR.item, item);
   holder.getBinding().executePendingBindings();
}
Copy the code

Immediately binding

When a variable or Observable changes, the binding is scheduled to change before the next frame. Sometimes, however, the binding must be performed immediately. To enforce this, use the executePendingBindings() method.

A background thread

As long as it is not a collection, the data model can be changed in background threads. Data binding will localize each variable/field at evaluation time to avoid any concurrency issues.


Attribute set

Every time the binding value changes, the generated binding class must use the binding expression to call setter methods on the view. The data binding framework can customize which method to call to set the value.

Automatic setters

For an attribute, the data binding will try to find a way to set the attribute. The namespace of an attribute is not important, only the attribute name itself is. For example, the expression associated with the TextView property Android: text looks for setText(String). If the expression returns int, the data binding searches for a setText(int) method. Make sure the expression returns the correct type, and convert if necessary. Data binding works even if no attributes exist for a given name. You can then easily create properties for any setter using data binding. For example, DrawerLayout in the Support library has no properties, but many setters. You can use one of these using the autosetter.

<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

Renames the setter

Some attribute setters do not match the name. For these methods, a property may be associated with the setter via the BindingMethods annotation. This must be associated with a class that each renamed method contains a BindingMethod annotation. For example, the Android: tint property does associate with setImageTintList(ColorStateList), not setTint.

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"})),Copy the code

Developers are unlikely to need to rename the setter; the Android framework already implements these properties.

Custom setters

Some properties require custom binding logic. For example, the Android :paddingLeft property has no associated setter. Instead, setPadding(EFt, top, right, bottom) exists. Statically bound adapter methods using BindingAdapter annotations allow developers to customize how the property’s setters are invoked.

Android properties have created BindingAdapters. For example, here is paddingLeft:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
   view.setPadding(padding,
                   view.getPaddingTop(),
                   view.getPaddingRight(),
                   view.getPaddingBottom());
}
Copy the code

Binding adapters are useful for other types of customization. For example, a custom loader can be called to an offline thread to load an image. When a conflict occurs, the developer creates a binding adapter that overrides the data binding default adapter. You can also have the adapter accept more than one parameter.

@BindingAdapter({"bind:imageUrl"."bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
   Picasso.with(view.getContext()).load(url).error(error).into(view);
}
Copy the code
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>
Copy the code

This adapter is called if both imageUrl and Error are used for ImageView and imageUrl is a string and error is a drawable. Custom namespaces are ignored during matching. You can also write adapters for the Android namespace. The binding adapter method can choose to use the old value in its handler. To take the old and new values approach, all the old values of the attribute should be first, followed by the new values:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
   if (oldPadding != newPadding) {
       view.setPadding(newPadding,
                       view.getPaddingTop(),
                       view.getPaddingRight(),
                       view.getPaddingBottom());
   }
}
Copy the code

Event handlers can only be used for interfaces or abstract classes that have only one abstract method. Such as:

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if(oldValue ! = null) { view.removeOnLayoutChangeListener(oldValue); }if(newValue ! = null) { view.addOnLayoutChangeListener(newValue); }}}Copy the code

When a listener has more than one method, it must be split into multiple listeners. For example, a View. There are two methods: OnAttachStateChangeListener onViewAttachedToWindow () and onViewDetachedFromWindow (). Then we must create two interfaces to distinguish between their properties and handlers.

@TargetApi(VERSION_CODES.HONEYCOMB_MR1) public interface OnViewDetachedFromWindow { void onViewDetachedFromWindow(View v); } @TargetApi(VERSION_CODES.HONEYCOMB_MR1) public interface OnViewAttachedToWindow { void onViewAttachedToWindow(View v);  }Copy the code

Because changing one listener also affects the other, we must have three different binding adapters, one for each property and one for both, which should be set.

@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
    setListener(view, null, attached);
}

@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
    setListener(view, detached, null);
}

@BindingAdapter({"android:onViewDetachedFromWindow"."android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
        final OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        final OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if(attach ! = null) { attach.onViewAttachedToWindow(v); } } @Override public void onViewDetachedFromWindow(View v) {if(detach ! = null) { detach.onViewDetachedFromWindow(v); }}}; } final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener);if(oldListener ! = null) { view.removeOnAttachStateChangeListener(oldListener); }if(newListener ! = null) { view.addOnAttachStateChangeListener(newListener); }}}Copy the code

The above example a little more complicated than normal, because the View of the listener to add and delete, rather than on the OnAttachStateChangeListener using the set method. Android. Databinding. Adapters. ListenerUtil class helps to follow-up the previous listener, so that they can be removed in the binding adapter. By using the @targetAPI (version_codes.honeycomb_MR1) annotation interface OnViewDetachedFromWindow and OnViewAttachedToWindow, Data binding code generator know should only 12 or more devices in the API calls addOnAttachStateChangeListener (View. OnAttachStateChangeListener) to run run to the listener.


converter

Object conversion

When an object is returned from a binding expression, one of the automatic, renaming, and custom setters is selected. This object is converted to the parameter type of the selected setter. This is handy for developers who use ObservableMaps to store data. Such as:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
Copy the code

UserMap returns an object that is automatically converted to the parameter type found in setText(CharSequence). When parameter types can be confused, the developer needs to enter them in the expression.

Custom conversion

Sometimes conversions should occur automatically between specific types. For example, when setting background:

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

In this case, the background requires a Drawable, but the color is an integer. Whenever a Drawable is judged to return an integer, the integer should be converted to a ColorDrawable. This conversion is done using a static method annotated with BindingConversion:

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

Note that conversions only occur at the setter level, so mixing types is not allowed, as shown below:

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

Android Studio supports data binding

Android Studio supports many code editing features for data binding code. For example, it supports the following capabilities of data-binding expressions:

  • Syntax highlighting
  • Expression syntax error warning
  • XML code completion
  • Includes references for navigation (such as navigating to declarations) and quick documentation

Note: Arrays and general types (such as Observables) may display errors if there are no errors.

The preview pane shows the default values for data binding expressions, if provided. As the following example extracts elements from the layout XML file, the preview pane will display PLACEHOLDER default text values in the TextView.

<TextView android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{user.firstName, default=PLACEHOLDER}"/>
Copy the code

If you need to display default values at project Design Time, you can also use tool properties instead of default expression values, as described in Design Time Layout Attributes.


Address: the original developer. The android. Google. Cn/topic/libra…