DataBinding data binding layout and binding expression in Android Jetpack

Posted by rationalrabbit on Fri, 25 Feb 2022 12:32:24 +0100

Layout and binding expressions

With the help of the expression language, you can write expressions to handle the events assigned by the view. The data binding library automatically generates the classes needed to bind the views in the layout to your data objects.

The data binding layout file is slightly different, starting with the root tag layout, followed by the data element and the view root element. This view element is the root in the unbound layout file. The following code shows a sample layout file:

<?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>
    

The user variable in data describes the properties that can be used in this layout.

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

Expressions in the layout are written to attribute properties using the "@ {}" syntax. Here, the TextView text is set to the firstName property of the user variable:

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

Note: layout expressions should be kept concise because they cannot be unit tested and have limited IDE support. To simplify layout expressions, you can use custom binding adapters.

data object

Now let's assume that you have a plain old object to describe the User entity:

Kotlin

    data class User(val firstName: String, val lastName: String)

Java

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

This type of object has data that never changes. It is common for applications to contain data that will not change after reading once. You can also use objects that follow a set of conventions, such as the use of accessor methods in Java, as shown in the following example:

Kotlin

    // Not applicable in Kotlin.
    data class User(val firstName: String, val lastName: String)

Java

    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() {
          return this.lastName;
      }
    }

From a data binding perspective, these two classes are equivalent. The expression @ {user.firstName} for the android:text attribute accesses the firstName field in the previous class and the getFirstName() method in the latter class. Or, if the method exists, it will also resolve to firstName().

Binding data

The system generates a Binding class for each layout file. By default, the class name is based on the name of the layout file. It is converted to Pascal case and the Binding suffix is added at the end. The above layout file name is activity_main.xml, so the generated corresponding class is ActivityMainBinding. This class contains all bindings from layout properties (for example, user variables) to layout views and knows how to specify values for Binding expressions. The recommended Binding creation method is to create when expanding the layout, as shown in the following example:

Kotlin

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

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(
                this, R.layout.activity_main)

        binding.user = User("Test", "User")
    }

Java

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

When running, the application will display the Test user in the interface. Alternatively, you can use layoutinflator to get the view, as shown in the following example:

Kotlin

    val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())

Java

    ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());

If you want to use data binding items in the Fragment, ListView, or RecyclerView adapter, you may prefer to use the inflate() method of the binding class or DataBindingUtil class, as shown in the following code example:

Kotlin

    val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

Java

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

Expression language

Common functions
The expression language is very similar to expressions in managed code. You can use the following operators and keywords in the expression language:

  • Arithmetic operators + - / *%
  • String concatenation operator+
  • Logical operator & &||
  • Binary operator & |^
  • Unary operator + -~
  • Shift operator > > > > ><<
  • Comparison operator = = > < > = < = (please note that < < needs to be escaped as <)
  • instanceof
  • Grouping operator ()
  • Literal operator - character, string, number, null
  • Type conversion
  • Method call
  • Field access
  • Array access []
  • Ternary operator?:
    Example:
android:text="@{String.valueOf(index + 1)}"
    android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
    android:transitionName='@{"image_" + id}'

Missing operation

The following operations are missing from the expression syntax that you can use in managed code:

  • this
  • super
  • new
  • Explicit generic call

Null merge operator

If the left operand is not Null, the Null merge operator (?) Select the left operand. If the left operand is Null, select the right operand.

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

This is functionally equivalent to:

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

Attribute reference

Expressions can reference attributes in classes using the following format, which is the same for fields, getter s, and ObservableField objects:

android:text="@{user.lastName}" 

Avoid Null pointer exceptions

The generated data binding code will automatically check for null values and avoid null pointer exceptions. For example, in the expression @ {user.name}, if user is null, it is user The default value assigned by name is null. If you reference user Age, where the type of age is int, the data binding uses the default value of 0.

View reference

Expressions can reference other views in a layout by ID using the following syntax:

android:text="@{exampleText.text}"

Note: the binding class converts the ID to hump case.

In the following example, the TextView view references the EditText view in the same layout:

<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}"/>

aggregate

For convenience, you can use the [] operator to access common collections, such as arrays, lists, sparse lists, and mappings.

<data>
        <import type="android.util.SparseArray"/>
        <import type="java.util.Map"/>
        <import type="java.util.List"/>
        <variable name="list" type="List&lt;String>"/>
        <variable name="sparse" type="SparseArray&lt;String>"/>
        <variable name="map" type="Map&lt;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]}" 

Note: to make XML free of syntax errors, you must escape the < character. For example: do not write in the form of List, but must write in the form of List < string >.

You can also use object The key notation refers to a value in the mapping. For example, @ {map[key]} in the above example can be replaced by @ {map.key}.

string literal

You can enclose the attribute value in single quotation marks, so you can use double quotation marks in the expression, as shown in the following example:

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

You can also use double quotation marks to enclose property values. If you do so, you should also use the inverse single quotation mark ` to enclose the literal of the string:

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

resources

Expressions can reference application resources using the following syntax:

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

You can evaluate format strings and plurals by providing parameters:

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

You can pass attribute references and view references as resource parameters:

android:text="@{@string/example_resource(user.lastName, exampleText.text)}" 

When a complex number has multiple parameters, you must pass all parameters:

      Have an orange
      Have %d oranges

    android:text="@{@plurals/orange(orangeCount, orangeCount)}" 

Some resources require explicit type evaluation, as shown in the following table:
|Type | general 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

event processing

With data binding, you can write expressions that are dispatched from the view to handle events (for example, the onClick() method). The event attribute name is determined by the name of the listener method, with some exceptions. For example, view Onclicklistener has an onClick() method, so the feature of this event is android:onClick.

There are some event processing scripts specifically for click events. These processing scripts need to use features other than android:onClick to avoid conflicts. You can use the following properties to avoid these types of conflicts:

classListener setterattribute
SearchViewsetOnSearchClickListener(View.OnClickListener)android:onSearchClick
ZoomControlssetOnZoomInClickListener(View.OnClickListener)android:onZoomIn
ZoomControlssetOnZoomOutClickListener(View.OnClickListener)android:onZoomOut

You can use the following mechanisms to handle events:

  • Method reference: in an expression, you can refer to a method that conforms to the listener's method signature. When the expression evaluation result is a method reference, the data binding encapsulates the method reference and owner object into the listener and sets the listener on the target view. If the expression evaluates to null, the data binding does not create a listener, but sets a null listener.
  • Listener binding: These are lambda expressions that are evaluated when an event occurs. Data binding always creates a listener to set up on the view. After the event is dispatched, the listener evaluates the lambda expression.

Method reference

Events can be directly bound to script processing methods, similar to specifying android:onClick for methods in an Activity. A major advantage over the View onClick feature is that the expression is processed at compile time, so if the method does not exist or its signature is incorrect, you will receive a compile time error.

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

To assign an event to its processing script, use a regular binding expression with the name of the method to call as the value. For example, consider the following example of layout data objects:

Kotlin

   class MyHandlers {
       fun onClickFriend(view: View) { ... }
   }  

Java

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

The binding expression can assign the click listener of the view to the onClickFriend() method, as follows:

<?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> 

Note: the method signature in the expression must be exactly the same as the method signature in the listener object.
Listener binding
Listener bindings are binding expressions that run when an event occurs. They are similar to method references, but allow you to run arbitrary data binding expressions. This feature is applicable to Android Gradle plug-ins of gradle version 2.0 and later.

In a method reference, the parameters of the method must match the parameters of the event listener. In the listener binding, only your return value must match the expected return value of the listener (except that the expected return value is invalid). For example, refer to the following presenter class with onsavelick() method:

Kotlin

    class Presenter {
        fun onSaveClick(task: Task){}
    } 

Java

    public class Presenter {
        public void onSaveClick(Task task){}
    } 

You can then bind the click event to the onsavelick () method, as follows:

<?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> 

When a callback is used in an expression, the data binding automatically creates and registers the necessary listeners for the event. When a view triggers an event, the data binding evaluates the given expression. As with regular binding expressions, when evaluating these listener expressions, you still get the Null value and thread safety of the data binding.

In the above example, we have not defined the view parameter passed to onClick(View). The listener binding provides two listener parameter options: you can ignore all parameters of the method or name all parameters. If you want to name parameters, you can use them in expressions. For example, the above expression can be written as follows:

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

Or, if you want to use parameters in an expression, use the following form:

Kotlin

    class Presenter {
        fun onSaveClick(view: View, task: Task){}
    } 

Java

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

You can use multiple parameters in a lambda expression:

Kotlin

    class Presenter {
        fun onCompletedChanged(task: Task, completed: Boolean){}
    }

    

Java

    public class Presenter {
        public void onCompletedChanged(Task task, boolean completed){}
    } 

Xml usage

<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
          android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
    

If the return type of the event you listen to is not a value of void, your expression must also return a value of the same type. For example, if you want to listen for a long press event, the expression should return a Boolean value.

Kotlin

    class Presenter {
        fun onLongClick(view: View, task: Task): Boolean { }
    } 

Java

    public class Presenter {
        public boolean onLongClick(View view, Task task) { }
    } 

Xml usage

android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}" 

If the expression cannot be evaluated due to a null object, the data binding returns the default value of that type. For example, the reference type returns null, int returns 0, boolean returns false, and so on.

If you need to use expressions with predicates (for example, ternary operators), you can use void as a symbol.

xml

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

Avoid using 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 interface to the callback method. You should implement any business logic in the callback method called from the listener expression.

Import, variables, and include

The data binding library provides functions such as import, variable, and include. With the import function, you can easily reference classes in layout files. The variable function allows you to describe the properties that can be used in binding expressions. By including functionality, you can reuse complex layouts throughout your application.

Import

With the import function, you can easily reference classes in layout files, just as in managed code. You can use multiple import elements in the data element or not. The following code example imports the View class into the layout file:

Xml

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

Importing the View class allows you to reference it through a binding expression. The following example shows how to reference the VISIBLE and GONE constants of the View class:

Xml

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

Type alias

When there is a conflict between class names, one of the classes can be renamed with an alias. The following example will be com example. real. Rename the View class in the estate package to Vista:

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

You can use Vista to reference com. In the layout file example. real. estate. View, use view to reference Android view. View.

Import other classes

Imported types can be used as type references in variables and expressions. The following example shows User and List as variable types:

<data>
        <import type="com.example.User"/>
        <import type="java.util.List"/>
        <variable name="user" type="User"/>
        <variable name="userList" type="List&lt;User>"/>
    </data> 

Note: Android Studio does not handle import yet, so the automatic filling function of import variables may not be available in your IDE. Your application can still be compiled, and you can solve this IDE problem by using fully qualified names in variable definitions.

You can also use the imported type to type convert part of the expression. The following example casts the connection property to the type User:

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

Imported types can also be used when static fields and methods are referenced in expressions. The following code will import the MyStringUtils class and reference its capitalization method:

<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"/> 

Just like in managed code, the system will automatically import Java lang.*.

variable

You can use multiple variable elements in a data element. Each variable element describes an attribute that can be set on the layout and will be used in the binding expression in the layout file. The following example declares the user, image, and note variables:

<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> 

The variable type is checked at compile time, so if the variable implements Observable or Observable set, it should be reflected in the type. If the variable is a base class or interface that does not implement the Observable interface, the variable is "unobservable".

If different configurations (such as landscape or portrait) have different layout files, the variables are merged together. There must be no conflicting variable definitions between these layout files.

In the generated binding class, each described variable has a corresponding setter and getter. Before calling setter, these variables always adopt the default managed code value, such as null for reference type, 0 for int, false for boolean, and so on.

The system will generate a special variable named context as needed to bind expressions. The value of context is the context object in the getContext() method of the root view. The context variable is replaced by an explicit variable declaration with that name.

contain

By using the variable names in the application namespace and attributes, variables can be passed from the included layout to the binding of the included layout. The following example shows an example from name XML and contact The user variable contained in the XML layout file:

<?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> 

Data binding does not support include as a direct child of a 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><!-- Doesn't work -->
           <include layout="@layout/name"
               bind:user="@{user}"/>
           <include layout="@layout/contact"
               bind:user="@{user}"/>
       </merge>
    </layout> 

Topics: Java kotlin