• 2.2.1.Architecture components_data binding


    参考

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

    https://blog.csdn.net/qiang_xi/article/details/74347880

    配置

    l 设备需要是Android4.0 api14及以上版本,

    l 项目根目录的build.gradle中的classpath 'com.android.tools.build:gradle:xxx' 版本需要1.5.0及以上。

    l 在module的build.gradle中

    android {
        dataBinding {
            enabled = true
    }
    }

    注意1:即使app module不直接使用data binding,如果app module依赖的库使用了 data binding,那么app module也必须配置。

    注意2:Arrays and a generic type, such as the Observable class, might incorrectly display errors.

    注意3:不用添加依赖,添加完上边的后就会直接引入几个库:

    wps64

    布局和绑定表达式

    你可以在布局文件中写入 布局 和 绑定表达式,如下所示:

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

    绑定布局和普通的布局有些许不同,

    不同之处在:绑定布局根元素是layout,在layout内部接着data元素和布局。

    • l data元素:就是用来声明一些绑定参数,例如要绑定的bean。
    • l 布局:就是普通布局中的布局,只不过在具体属性的设置用@{bean.xxx}来赋值
    <TextView android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="@{user.firstName}" />

    另外:

    • l data binding库会自动生成将布局中的视图与数据对象绑定所需的类。
    • l 布局表达式应保持小而简单,因为不能测试、ide对此的支持是有限的。

    数据类的定义规则

    public class User {
      public final String firstName;
      public final String lastName;
      public User(String firstName, String lastName) {
          this.firstName = firstName;
          this.lastName = lastName;
      }
    }
    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;
      }
    }

    这两种定义方式都是可以的,

    在布局中只有一种写法android:text="@{user.firstName}",但在查找对应的值时,

    • l 会去查找变量名对应的字段,比如firstName
    • l 也可以找此字段的get方法,比如getFirstName();
    • l 或者此变量名 有对应的方法也可以,比如firstName();
    • l 当然不一定要用final修饰,只不过User不是一个可观察的对象,所以user数据发生变化并会不通知相关ui进行更新。

    Binding data

    上边定义了一个布局和一个数据类,那么想要显示对应的ui肯定还是需要进行 数据对象的创建 及 和布局的绑定设置的。

    系统为每个绑定布局文件生成一个绑定类。 默认情况下,类的名称基于布局文件的名称,将其转换为Pascal大小写并向其添加Binding后缀。 上面的布局文件名是activity_main.xml,因此相应的生成类是ActivityMainBinding。 此类包含从布局属性(例如,user 变量)到布局视图的所有绑定,并且知道如何为绑定表达式分配值。

    创建绑定的推荐方法是在inflating 布局的同时进行绑定,例如 如以下示例所示:

    @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);
    }

    或者,您可以使用LayoutInflater获取绑定对象,如以下示例所示:

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

    如果你是在Fragment、ListViewAdapter、RecyclerViewAdapter等中使用data binding的话,可以使用下边方法:

    ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);

    // or

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

    表达式语法

    操作符

    The expression language looks a lot like expressions found in managed code.

    You can use the following operators and keywords in the expression language:

    • Mathematical + - / * %
    • String concatenation +
    • Logical && ||
    • Binary & | ^
    • Unary + - ! ~
    • Shift >> >>> <<
    • Comparison == > < >= <= (Note that < needs to be escaped as &lt;)
    • instanceof
    • Grouping ()
    • Literals - character, String, numeric, null
    • Cast
    • Method calls
    • Field access
    • Array access []
    • Ternary operator ?:

    Examples:

    android:text="@{String.valueOf(index + 1)}"
    android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
    android:transitionName='@{"image_" + id}'

    default

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{user.name,default=1}" />

    default只在layout编辑器中有效,效果和tools:text=”1”一样。

    不能使用的操作

    以下操作是不能使用的:

    • l this
    • l super
    • l new
    • l Explicit generic invocation

    空合并运算符??

    空合并运算符??

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

    如果不是null则取前边的,否则取后边的,等效于下边:

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

    属性引用

    表达式可以使用以下格式来引用类中的属性,以下格式对于字段,getter和ObservableField对象是相同的:

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

    自动避免空指针

    生成的databinding代码会自动的检查null值,来避免空指针。

    例如, @{user.name},如果user为null,那么最终就取name的默认值,即String的默认值,即null,

    如果@{user.age},则会取age的默认值,即int的默认值,为0;

    集合

    arrays, lists, sparse lists, and maps这些集合都可以通过[ ]来访问。

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

    注意:要使XML语法正确,必须转义 < 。例如:您必须编写List<;String>,而不是List<String>。

    你也可以使用@{map.key}等效于@{map[key]}

    引号和字符串常量

    你可以使用,单引号在外边,双引号在里边:

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

    你也可以使用双引号在外边,back quotes(键盘左上角的字符`)在里边:

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

    Resources

    You can access resources in an expression by using the following syntax:

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

    也可以使用格式化字符串:

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

    nameFormat为字串的id名后边括号里的是具体的实参。

    当然也可以引用strings.xml中的:

    android:text="@{@string/format_test(@string/activity_lifecycle)}"

    一些资源需要显式类型求值,如下表所示:

    Type

    Normal reference

    Expression reference

    String[]

    @array

    @stringArray[index]

    int[]

    @array

    @intArray[index]

    TypedArray

    @array

    @typedArray

    Animator

    @animator

    @animator

    StateListAnimator

    @animator

    @stateListAnimator

    color int

    @color

    @color

    ColorStateList

    @color

    @colorStateList

    事件处理

    data binding允许你写表达式来处理那些从view分发的事件,比如onClick事件,

    事件属性名称由侦听器方法的名称确定,但有几个例外。

    例如,View.OnClickListener有一个 onClick(View)方法,因此在布局中的属性就叫做 android:onClick。

    你可以使用两种方式处理事件:

    • l 方法引用
    • l 监听器绑定

    方法引用

    java8双引号::

    在表达式中,可以引用符合侦听器方法签名的方法。

    事件可以直接绑定到处理程序方法,类似于可以将android:onClick分配给Activity中的方法的方式。 但和View onClick属性相比,一个主要优点是表达式是在编译时处理的,因此,如果该方法不存在或其签名不正确时,则会收到编译时错误。

    方法引用和侦听器绑定之间的主要区别在于,方法引用的 实际的侦听器实现是在绑定数据时创建的,而不是在事件触发时创建的。 如果您希望在事件发生时执行表达式,则应使用侦听器绑定。

    在创建处理器方法时,方法的参数必须和要和要监听的事件一致,比如下边监听的是onClick,View.OnClickListener有一个 onClick(View)方法,那么在创建事件处理器时就需要创建的方法参数中也要带view:

    public class MyHandlers {
        public void onClickFriend(View 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>

    监听器绑定

    java8的lambda表达式

    侦听器绑定是事件发生时运行的绑定表达式。 它们类似于方法引用,但是它们使您可以运行任意数据绑定表达式。 适用于Gradle 2.0版及更高版本的Android Gradle插件提供了此功能。

    onClick

    • l 在上边方法引用中,方法的参数必须与事件侦听器的参数匹配。
    • l 而在侦听器绑定中,只有您的返回值必须与侦听器的期望返回值匹配(除非期望返回void)。 例如,假设以下具有onSaveClick()方法的presenter类:
    public class Presenter {
        public void onSaveClick(Task task){}
    }
    <?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>
    • l 在表达式中使用回调时,数据绑定会自动为事件创建并注册必要的监听器。当视图触发事件时,数据绑定会对给定表达式求值。
    • l 与常规绑定表达式一样,在对这些监听器表达式求值时,仍会获得数据绑定的 Null 值和线程安全。

    在上面的示例中,我们尚未定义传递给onClick(View)的view参数。 侦听器绑定为侦听器参数提供了两种选择:您可以忽略该方法的所有参数,也可以全部命名。 如果您想命名参数,则可以在表达式中使用它们。 例如,上面的表达式可以编写如下:

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

    或者,如果要在表达式中使用参数,则可以按以下方式工作:

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

    多个参数的lambda表达式

    您可以使用具有多个参数的lambda表达式:

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

    返回值不是void

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

    如果由于null 对象而无法对表达式求值,则数据绑定将返回该类型的默认值。 例如,对于引用类型,为null,对于int为0,对于布尔值为false,等等。

    其他

    • l 如果需要将表达式与谓词(例如,三元)一起使用,则可以使用void作为符号。
    android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
    • l 尽量不要把业务逻辑都写在布局文件中,应该保持布局文件的简洁,而把具体的复杂的业务逻辑写在具体的类的方法中。

    import, variable, include,ViewStub

    import

    • l import类似于java代码中的import,
    • l 默认导入java.lang下的一些包

    DataBinding已经默认帮我们导入了String类,Integer类等java.lang下的基本数据类型

    • l import的类可以用在布局中,也可以用在variable
    <data>
        <import type="android.view.View"/>
    </data>
    
    <TextView
       android:text="@{user.lastName}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

    导入的类还可以用于强转

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

    导入的类还可以使用其静态常量和静态方法

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

    类型别名

    如果导入的类名有重复的,则可以进行别名来避免混淆:

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

    variable

    <data>
        <variable name="user" type="com.example.User"/>
        <variable name="image" type="Drawable"/>
        <variable name="note" type="String"/>
    </data>
    • l 当针对各种配置(例如,横向或纵向)有不同的布局文件时,将合并变量。 这些布局文件之间不得有冲突的变量定义。
    • l 对于每个所描述的变量,生成的绑定类都有一个setter和getter。 变量将使用默认的托管代码值,直到调用setter为止,引用类型为null,int为0,布尔值为false,等等。

    Context对象

    l databinding会自动导入Context对象,context的值是根视图的getContext()方法获取的,所以我们可以在布局中直接使用context,而不用导入或声明。

    但是如果我们自己通过variable标签定义了一个name为context的对象,那么会覆盖掉系统提供的context。

    include

    数据传递

    include类似于普通布局中使用的include,但有一点不同是,

    如果include的也是一个绑定布局,并且有一个参数的类型和第一个布局一样,那么我们把参数传递进去,只需要把实际数据设置给第一个绑定类,那么include进来的布局也会显示对应的数据。

    • !ViewStub也支持!
    • 另外,include不支持自定义BindingMethods,BindingAdapter。

    例子

    activity_main2.xml

    <?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"
        xmlns:tools="http://schemas.android.com/tools">
    
        <data>
    
            <variable
                name="user"
                type="com.example.jetpackdemo.databingding.User" />
    
            <variable
                name="index"
                type="int" />
    
            <variable
                name="handler"
                type="com.example.jetpackdemo.databingding.MyHandlers" />
    
        </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/test(index)}" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{user.name,default=1}" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{String.valueOf(user.age)}" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{user.sex}" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="@{(view)->handler.onClickFriend(view)}"
                android:text="Hello World!" />
    
            <include
                app:user2="@{user}"
                layout="@layout/activity_main3" />
        </LinearLayout>
    </layout>

    app:user2="@{user}"中的user2就是include的布局中定义的参数名,user是此布局定义的参数名。

    activity_main3.xml

    <?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"
        xmlns:tools="http://schemas.android.com/tools">
    
        <data>
    
            <variable
                name="user2"
                type="com.example.jetpackdemo.databingding.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="@{user2.name,default=1}" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{String.valueOf(user2.age)}" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{user2.sex}" />
    
        </LinearLayout>
    </layout>
    ActivityMain2Binding activityMain2Binding = DataBindingUtil.setContentView(this, R.layout.activity_main2);
    User user = new User("haha", 18, "女");
    activityMain2Binding.setUser(user);

    最终我们只需要把数据设置给ActivityMain2Binding ,那么这两个xml都会显示对应的数据。

    wps65

    include上加上id

    还用上边例子,如果在include上加上id,那么就可以直接访问include的绑定布局对象,而不需要再去用DataBingdingUtil去获取。

    <include
        android:id="@+id/databinding2"
        layout="@layout/databinding2"
        app:user2="@{user}" />
    databinding1Binding.databinding2

    分析一下生成的代码就明白了:

    Databinding1BindingImpl继承自Databinding1Binding

    public Databinding1BindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        this(bindingComponent, root, mapBindings(bindingComponent, root, 9, sIncludes, sViewsWithIds));
    }
    private Databinding1BindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        super(bindingComponent, root, 2
            , (android.widget.Button) bindings[1]
            , (com.example.jetpackdemo.databinding.Databinding2Binding) bindings[8]
            , (android.widget.LinearLayout) bindings[0]
            , (android.widget.TextView) bindings[5]
            , (android.widget.TextView) bindings[4]
            , (android.widget.TextView) bindings[6]
            );

    Databinding1Binding是绑定布局生成的绑定类

    protected Databinding1Binding(Object _bindingComponent, View _root, int _localFieldCount,
        Button btRefresh, Databinding2Binding databinding2, LinearLayout root, TextView tvAge,
        TextView tvName, TextView tvSex) {
      super(_bindingComponent, _root, _localFieldCount);
      this.btRefresh = btRefresh;
      this.databinding2 = databinding2;
      setContainedBinding(this.databinding2);
      this.root = root;
      this.tvAge = tvAge;
      this.tvName = tvName;
      this.tvSex = tvSex;
    }

    调用构造函数时,传递的root view布局肯定是inflate过的,也就是说布局的层级已经有了。

    可以看到对在构造函数中直接对Databinding2Binding 进行了赋值,那么这个对象是从哪创建的,

    关键就在mapBindings这个方法,这个方法就是解析view及其子view上设置的tag(这个tag是自动添加上的),并把解析出来的对象放到bindings数组的合适的位置。

    其中就会调用DataBindingUtil.bind方法(不管include的布局是否是绑定布局),如果include的布局是绑定布局,那么就会获得绑定对象。

    ViewStub

    在绑定布局中使用ViewStub,会在生成的绑定类中用ViewStubProxy来代替ViewStub,

    因为ViewStub本质上是个不可见的view,所以ViewStub引用的布局会在ViewStub inflate后才生成绑定对象。

    注意:

    • ViewStub也支持 数据传递

    只是需要注意的是,是在ViewStub inflate之后才支持,也就是说需要在inflate之后才可以设置要传递的值。

    • ViewStub不支持自定义BindingMethods,BindingAdapter

    和可观察的对象一起使用

    可观测性是指对象通知其他人其数据变化的能力。 数据绑定库 使您可以观察对象,字段或集合。

    任何bean数据对象都可以用于数据绑定,但是修改对象不会自动导致UI更新。 数据绑定可用于使您的数据对象在数据更改时通知其他对象(称为侦听器)。

    有三种不同类型的可观察类:对象object,字段field 和集合collection。

    当这些可观察数据对象之一绑定到UI且数据对象的属性更改时,UI将自动更新。

    Observable fields

    注意:Android Studio 3.1及更高版本允许您用LiveData对象替换Observable fields,这为您的应用程序提供了更多好处。

    下边是一些基本数据类型对应的Observable类,

    • l ObservableBoolean
    • l ObservableByte
    • l ObservableChar
    • l ObservableShort
    • l ObservableInt
    • l ObservableLong
    • l ObservableFloat
    • l ObservableDouble
    • l ObservableParcelable

    可观察字段是具有单个字段的自包含可观察对象。 原始版本避免在访问操作期间装箱和拆箱。 要使用此机制,请使用Java编程语言创建一个public final属性,或者使用Kotlin创建一个只读属性,如以下示例所示:

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

    要访问该字段值,请使用set()和get()访问器方法,如下所示:

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

    Observable collections

    一些应用程序使用动态结构来保存数据。 可观察的集合Observable collections允许使用key(键)访问这些结构。

    ObservableArrayMap

    当key(键)是引用类型(例如String)时,ObservableArrayMap类非常有用,如以下示例所示:

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

    在布局中,可以使用字符串键找到map中的值,如下所示:

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

    ObservableArrayList

    当键为整数时,ObservableArrayList类非常有用,如下所示:

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

    在布局中,可以通过索引访问列表,如以下示例所示:

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

    Observable objects

    实现Observable接口的类允许注册要在可观察对象上进行属性更改通知的侦听器。

    Observable接口具有添加和删除侦听器的机制,但是您必须确定何时发送通知。 为了简化开发,数据绑定库提供了BaseObservable类,该类实现了侦听器注册机制。

    实现BaseObservable的数据类负责在属性更改时发出通知。这是通过为getter分配Bindable注解并在setter中调用notifyPropertyChanged()方法来完成的,如以下示例所示:

    public 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(com.eallcn.mlw.rentcustomer.BR.firstName);
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
            notifyPropertyChanged(com.eallcn.mlw.rentcustomer.BR.lastName);
        }
    }
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{user.firstName}" />
    • l 数据绑定在模块包中生成一个名为BR的类,其中包含用于数据绑定的资源的ID。

    Bindable注解在编译期间会在BR类文件中生成一个条目。可以在xml中使用。

    • l 如果不能更改数据类的基类,那么我们可以impletement Observable接口,并自己通过使用PropertyChangeRegistry对象来以高效地注册和通知侦听器。

    Bindable注解

    @Target({ElementType.FIELD, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME) // this is necessary for java analyzer to work
    public @interface Bindable {
        String[] value() default  {};
    }

    Bindable 注解应该应用于一个Observable 类的任何getter访问方法。Bindable注解在编译期间会在BR类文件中生成一个条目。

    Bindable注解可以设置一个依赖属性列表。如果列出的属性中有一个更改通知,那么Bindable注解的这个值也会被认为是dirty的,并会被刷新。例如:

    @Bindable
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(com.example.jetpackdemo.BR.name);
    }
    
    @Bindable
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(com.example.jetpackdemo.BR.age);
    }
    
    @Bindable({"name", "age"})
    public String getNameAndAge() {
        return name + "-" + age;
    }

    在BR中会生成一个名为nameAndAge的item,那么在xml中就也可以使用。

    每当age或name有更改通知时,nameAndAge也会被认为是dirty的。但这并不意味着Observable.OnPropertyChangedCallback.onPropertyChanged(Observable, int)将得到BR.nameAndAge的通知,只有包含nameAndAge的绑定表达式将被刷新。

    和LiveData一起使用

    LiveData是生命周期感知的对象,所以如果在xml的绑定表达式中引入了LiveData,那么就必须在生成的绑定对象里设置LifecycleOwner,否则LiveData不能被观察,对LiveData的数据更新也不会传播到UI。

    @MainThread
    public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) 

    此方法是ViewDataBinding类的成员方法。

    自定义生成的绑定类

    生成的绑定类将布局变量与布局中的视图链接。 绑定类的名称和包可以自定义。 所有生成的绑定类都从ViewDataBinding类继承。

    xml中带有id的view

    对于有id的view,会在生成的绑定类里生成一个id名字的view的引用,我们可以直接通过此引用直接操作此view。

    例如:

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

    那么在生成的绑定类里,就会生成两个TextView的变量,一个叫firstName,一个叫lastName,他们都是public final修饰的。

    立即绑定executePendingBindings

    当变量或可观察对象发生更改时,绑定将安排在下一帧之前更改。

    但是,有时绑定必须立即执行。 要强制执行,请使用executePendingBindings()方法。

    在RecyclerView的adapter中的onBindViewHolder使用时

    在RecyclerView的adapter的onBindViewHolder中一般需要使用executePendingBindings()

    由于不使用 executePendingBindings()时,数据设置到ui上需要等到下一帧,而此时当要绑定一个数据的时候,RecyclerView 会调用 onBindViewHolder,让你去准备测量这个布局,而我们的ui还没设置数据,所以此时ui可能就会闪烁一下。

    不使用executePendingBindings()有可能还会导致图片闪烁,

    原因可能是 由于不使用 executePendingBindings()时,数据设置到ui上需要等到下一帧,而如果在此期间设置数据多次,那么最后ui显示时会突然显示几次。

    Dynamic Variables

    生成的绑定类是继承自ViewDataBinding,

    有时并不知道绑定对象是哪个具体的绑定类的对象,那么此时可以使用ViewDataBinding的一个方法setVariable,这个方法是每个生成的绑定类都会实现的,那么调用此方法就相当于调用具体的绑定类的对象的某个set方法。

    public boolean setVariable(int variableId, @Nullable Object variable)

    variableId:可以使用生成的BR类中的某个id。

    Background Thread

    您可以在后台线程中更改数据模型,但前提是这个模型不是集合。数据绑定会在求值过程中对每个变量/字段进行本地化,以避免出现并发问题。

    自定义生成的绑定类名

    默认情况下,将基于布局文件的名称生成绑定类,该绑定类以大写字母开头,去掉下划线(_)大写下划线之后的字母并在后边加上Binding后缀。

    该类位于模块包下的databinding包中。 例如,布局文件contact_item.xml生成ContactItemBinding类。 如果模块包是com.example.my.app,则将绑定类放置在com.example.my.app.databinding包中。

    可以通过绑定布局中的data的class属性来修改类名或包名。

    <data class="ContactItem">
        …
    </data>

    这种方式可以修改类名,包还是com.example.my.app.databinding

    <data class=".ContactItem"></data>

    这种方式可以修改类名,包变成com.example.my.app

    <data class="com.example.ContactItem"></data>

    这种方式可以修改包名和类名,变成com.example包下的ContactItem类

    Binding adapters

    Binding adapters负责进行适当的框架调用以设置值。

    • l 一个示例是设置属性值,例如调用setText()方法。
    • l 另一个示例是设置事件侦听器,例如调用setOnClickListener()方法。

    数据绑定库 允许您指定调用的方法来设置值,提供自己的绑定逻辑 以及 使用适配器指定返回对象的类型。

    指定属性setter方法

    每当绑定值更改时,生成的绑定类都会 使用绑定表达式 调用View对应的setter方法。 您可以允许数据绑定库自动确定方法,也可以显式声明该方法,或提供自定义逻辑来选择方法。

    !ViewStub不支持、include不支持!

    自动选择setter方法

    1. 对于名为example的属性,databinding库会自动尝试查找setExample(arg)方法,查找时会考虑 在布局中写的绑定表达式返回的类型 是否和setExample接收的参数类型是否匹配。

    在搜索方法时不考虑属性的名称空间,在搜索方法时仅使用属性名称和类型。

    例如:现有一个绑定表达式 android:text="@{user.name}",那么绑定库就会去查找此view的setText方法,

    如果user.getName()返回的是String,那么会去找接收的参数和String兼容的setText方法;

    而如果user.getName()返回的是int,那么会去找接收的参数和int兼容的setText方法;

    当然我们可以通过强转来指定调用的方法。

    2. 即使没有给定名称的view属性,数据绑定也可以工作,前提是此view得有此名称的setter方法。

    比如support库中的Drawerlayout,它是没有什么自定义的属性的,但有很多setter方法,那么如下使用:

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

    那么会自动的查找到对应的setScrimColor(int) 和 setDrawerListener(DrawerListener)。

    指定自定义的方法名BindingMethods

    一些view的属性的setter方法可能和属性名并无关联,这种情况下就可以使用@BindingMethods 注解来处理。

    @Target({ElementType.TYPE})
    public @interface BindingMethods {
        BindingMethod[] value();
    }
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface BindingMethod {
    
        /**
         * 属性关联的view的class
         */
        Class type();
    
        /**
         * 如果属性是android自带的属性,那么此字符串就是android:xxx;
    如果是自定义的属性,那么字符串就是属性名即可,xxx,但在xml中app:xxx
    !可以没有定义过!
         */
        String attribute();
    
        /**
         * @return The method to call to set the attribute value.
         */
        String method();
    }

    BindingMethods是可以添加到应用程序中任何类的注解

    例如:

    @BindingMethods({
           @BindingMethod(type = ImageView.class,
                          attribute = "android:tint",
                          method = "setImageTintList"),
    })

    大部分 官方的view的属性对应的setter不匹配的属性,databinding库已经添加了很多BindingMethods注解,比如TextViewBindingAdapter对textview的一些不匹配的属性添加了BindingMethods注解。

    指定自定义方法BindingAdapter

    和上边的指定方法名不一样,此方式是静态方法,而上边的是成员方法。

    比如有些属性是没有setter方法的,就是说没有 方法名匹配 和 参数兼容的方法,比如android:paddingLeft,但是view是有setPadding(left, top, right, bottom)方法的,那么此时就可以在一个静态方法上使用BindingAdapter注解。

    @Target(ElementType.METHOD)
    public @interface BindingAdapter {
    
        /**
         * @return The attributes associated with this binding adapter.
    接收的参数列表,如果是Android自带的需要android:xxx;
    如果是自定义的属性,则直接写属性,但在xml中app:xxx
    !可以没有定义过!
         */
        String[] value();
    
        /**
         * 是否需要每个属性都需要有绑定表达式。
    当false时,属性列表中至少有一个是有绑定表达式的,那么此注解的方法将被调用,那些没有绑定表达式的属性会传递java的默认值。
    必须注意确保默认值不要与有效的XML值混淆。
         *
         * @return whether or not every attribute must be assigned a binding expression. The default value is true.
         */
        boolean requireAll() default true;
    }
    @BindingAdapter("android:paddingLeft")
    public static void setPaddingLeft(View view, int padding) {
        view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom());
    }

    第一个参数View 是与属性关联的视图的类型。

    第二个参数确定给定属性在绑定表达式中接受的类型。

    databinding库已经提供了很多默认的BindingAdapter,比如ViewBindingAdapter中的,TextViewBindingAdapter中的,

    当发生冲突时,您定义的BindingAdapter将覆盖databinding库提供的默认适配器。

    requireAll

    <ImageView 
    app:imageUrl="@{venue.imageUrl}" 
    app:error="@{@drawable/venueError}" />
    @BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
    public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
      if (url == null) {
        imageView.setImageDrawable(placeholder);
      } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
      }
    }

    oldValue

    BindingAdapter可以选择在其处理程序中采用旧值。 采用旧值和新值的方法:应首先声明属性的所有旧值,然后再声明新值,如以下示例所示:

    @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());
       }
    }

    Event handlers一起使用

    事件处理程序 只能与具有一种抽象方法的接口或抽象类一起使用,如以下示例所示:

    <View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>
    @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);
        }
      }
    }

    当一个侦听器有多种方法时,必须将其拆分为多个侦听器。

    例如 View.OnAttachStateChangeListener 有两个方法: onViewAttachedToWindow(View) 和onViewDetachedFromWindow(View).

    那么就需要把这两个方法拆分出来:

    @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
    public interface OnViewDetachedFromWindow {
      void onViewDetachedFromWindow(View v);
    }
    
    @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
    public interface OnViewAttachedToWindow {
      void onViewAttachedToWindow(View v);
    }
    @BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
    public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
            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);
                        }
                    }
                };
            }
    
            OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener);
            if (oldListener != null) {
                view.removeOnAttachStateChangeListener(oldListener);
            }
            if (newListener != null) {
                view.addOnAttachStateChangeListener(newListener);
            }
        }
    }

    对象类型转换

    这里所说的转换是指使用绑定表达式时,存在的类型不一致的处理。而普通布局是不存在这种问题的。

    自动转换

    从绑定表达式返回Object时,库将选择用于设置属性值的方法。 将对象强制转换为所选方法的参数类型。

    在使用ObservableMap类存储数据的应用程序中,此行为很方便,如以下示例所示:

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

    自定义转换BindingConversion

    在某些情况下,需要在特定类型之间进行自定义转换。

    例如,视图的android:background属性需要Drawable,但是指定的颜色值为整数。 以下示例显示了一个期望使用Drawable的属性,但是提供了一个整数:

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

    每当需要Drawable并返回整数时,应该将int转换为ColorDrawable。

    那么就可以使用带有BindingConversion注解的静态方法完成转换,如下所示:

    @BindingConversion
    public static ColorDrawable convertColorToDrawable(int color) {
        return new ColorDrawable(color);
    }
    @Target({ElementType.METHOD})
    public @interface BindingConversion {
    }

    但是,绑定表达式中提供的值类型必须一致。 不能在同一表达式中使用不同的类型,如以下示例所示:

    <View
       android:background="@{isError ? @drawable/error : @color/white}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
  • 相关阅读:
    结构体作为函数参数
    自定义子窗口与主窗口通信
    Qt性能问题
    后缀表达式、中缀表达式
    QMap的使用
    自定义QSS
    Qt查找孩子findChild
    ThinkPHP 3.2.3 数据缓存与静态缓存
    Hadoop生态上几个技术的关系与区别:hive、pig、hbase 关系与区别  Pig
    Hadoop生态上几个技术的关系与区别:hive、pig、hbase 关系与区别  Pig
  • 原文地址:https://www.cnblogs.com/muouren/p/12368354.html
Copyright © 2020-2023  润新知