DataBinding 的简单使用

先吐槽下,不说不爽,不说不通达

不吐不快,集合我这几天学习 DataBinding 的经历说几句。DataBinding 这东西也不是 android 的专利,android 引入这个功能是后知后觉的,一步一趋的跟着别的系统脚步发展的。纵观全局,14-16年是 android 技术大爆发的年头,各种新技术层出不穷,目不暇接,到17年中呢android 技术的进步就停下来了,android 开发呢也是进入生命周期内最辉煌的时候,技术已经非常成熟了,可以遇见的未来,android 的技术短期内没有什么大的进步,改变了。作为一个普通的 android 开发者,我们要在这个时间点上努力的吸收之前几年 android 开发技术的精髓,和各种优秀的思路,思想,手段和套路,我觉得这就是我们做 android 开发应该详细了解,举一反三的,这些也移动开发的核心精髓,换个平台,换个系统,除了基础的开发语言和构建工具,和系统知识体系的变化外,剩下的都是要重复或者再走 android 这些年这些技术发展的老路,我认为相同领域技术思路都是趋同的,区别是不同平台,不同语言的具体实现罢了。


什么是 DataBinding

什么是 DataBinding 呢,简单说来就是帮我们实现 view 和 data 绑定的工具,把数据映射到 view 的 xml中,可以在 xml 布局文件中实现 view 的赋值,方法调用。使用 DataBinding 后,我们不同再写 findViewById,不用再获取控件对象,不用再设置监听,可以节省我们 activity 中的很多获取控件,赋值,添加监听所需要的代码。

DataBinding 是个好东西啊,15年 google IO 大会就开始推了,最直接的变化就是催生了 android 中 MVVM 的出现,MVVM = MVP + DataBinding 。一线公司早就普及 MVVM 了,大伙作为一个普普通通的 andoid 开发者,一定要身上时代,什么是时代,大厂就是时代,大厂都在干什么,我们就干什么,不求跟上大厂,但求不落后太多,所以小伙伴们走起,MVVM 作为 MVP 的进阶,我们一定要学好,这期中的重中之重 DataBinding 一定不要落下,其实没多难,DataBinding 初步学习半天就可以,其中涉及到列表和多module的部分是比较复杂 的,我们多看几个开源的 app 例子就行,这里我也尽量详细的说一说。经验是干什么的,就是带着我们少走弯路,快速学习的,有的可以快,有的不能快,必须去体会,恰巧 DataBinding 就是可以快起来的部分。


DataBinding 的初步使用

写了一段时间的博客后,我是深深体会到了,一不论多复杂的事一定要按步骤拆解成一段段简单的步奏,这样才能学的快,学的明白,中间断掉了,之后也好捡起来继续学。

千里之行,始于足下,所以呢,我们先来把 DataBinding 集成进来,做个最简单的实现。

先导入 DataBinding 功能

因为 DataBinding 是 google 强推的嘛,所以 1.5版以上 gradle 就自带 DataBinding 库了,只要我们在 gradle 配置中打开 DataBinding 这个功能就可以了,不用再引入远程依赖了。哪个 module 需要就在哪个 module 的 build.gradle 编译配置文件中声明启动 dataBinding 就可以了。注意 Gradle 最低要 1.5 alpha 版。

android {
    ...
    dataBinding {
        enabled = true
    }
    ...
}

这里我在 app 这个 module 里面声明的。DataBinding 在 gradle 中的表现形式就是 DataBinding 相关的 task 编译任务了,我们 enabled = true 之后,在编译时就会执行 DataBinding 的 task 了。

在 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>
        <import type="com.bloodcrown.bwdatabindingdemo.Book"/>
        <variable name="book" type="Book"></variable>
    </data>

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{book.name}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_editor_absoluteY="20dp"
            tools:text="name"/>

        <TextView
            android:id="@+id/tv_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="@{book.price}"
            app:layout_constraintLeft_toLeftOf="@id/tv_name"
            app:layout_constraintTop_toBottomOf="@id/tv_name"
            tools:text="price"/>

    </android.support.constraint.ConstraintLayout>
</layout>

注意 dataBinding 在 xml 中的使用规则:

  • 使用 DataBinding 必须要使用 <layout> 这个标签作为 xml 的根部局
  • 所有 DataBinding 的内容都写在 <data >标签内
  • <import > 是导入所使用的对象的类型
  • <variable > 是具体声明一个数据对象,<name> 是这个对象的名字,<type> 是这个对象的类型,<variable > 可以有多个
  • 使用 @{ } / @{book.price} 的方式可以把这个声明出来的数据对象的莫个数据设置给我们的 view
java 代码的使用部分
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        Book book = new Book("AAA", "35");
        mainBinding.setBook(book);
    }
}
  • 首先我们使用 DataBinding 的方式替换我们的 setContentView,使用 DataBindingUtil 这个类的 setContentView 方法,里面传入activity 和 layout 布局
  • 然后 DataBinding 的 setContentView 方法会根据类名给我们返回一个 DataBinding 的辅助类,这里就是这个 ActivityMainBinding 了。名字规则是 Activity 类型名在前 + 类中其他的名字,这个 ActivityMainBinding 辅助类我们不用编译 AS 也会自动帮我们生成,但是呢要是你在这里有问题,那就手动编译一下。
  • 我们拿到辅助类之后,new 一个数据对象出来,然后把这个数据对象设置给辅助类 ActivityMainBinding,因为我们在 xml 中的<data>中声明几个数据对象,那我们在 activity onCreate 中就得给 DataBinding 的辅助类 ActivityMainBinding,设置几个数据对象进去,然后 DataBinding 才可以根据我们设置进来的数据对象,给相关 view 设值。

DataBinding 的数据更新

view 在本质的职责是反应数据的变化,那么我们就来说说在 DataBinding 中我们怎么更新数据。DataBinding 的数据本质是我饿们在 xml 中 <data> 标签中声明的数据对象,我们在java 代码中把相关的数据对象设置给 DataBinding 的辅助类,那么我们有一下几种方式:

  • 整体再设置一次数据 bean 对象,这个适合更新整体数据
  • 使用 BaseObservable ,操作数据 bean 的 set 方法更新字段数据,适合更新部分数据
  • 使用 ObservableFields ,只更新有需求的数据字段,这个不够灵活
  • 使用 DataBinding 也有的集合类型:ObservableArrayMap , ObservableArrayList
  • 双向数据绑定
更新整体数据

这个本质上就是重复一次我们给 DataBinding 辅助类设置数据的过程,使用很简单

 Book book = new Book("BBBB", "65");
 mainBinding.setBook(book);

这样就可以了,说实话,我更倾向于这样方式,因为大多数时候,数据都是休要整体更新的

使用 BaseObservable

BaseObservable 是一个基类,需要我们的数据 bean 继承这个基类,然后给属性的 get 方法
添加@Bindable 这个注解,然后在属性的 set 方法中添加上 DataBinding 更新某个字段的方法

public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    } 

这样做呢,其实是在 DataBinding 的辅助类中把相关属性的更新和 view 的赋值方法关联在一起,完整的数据 bean 如下:

public class Book extends BaseObservable {

    public String name;
    public String price;

    public Book() {
    }

    public Book(String name, String price) {
        this.name = name;
        this.price = price;
    }

    @Bindable
    public String getName() {
        return name;
    }

   public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price;
        notifyPropertyChanged(BR.price);
    }
}
在 activity 中操作数据 bean 的 set 方法就可以同步把数据更新到 view 中了
mBook.setName("CCC");

有个更好的解释:

Obserable接口有一个自动添加和移除监听器的机制,但是通知数据更新取决于开发者。为了使开发变得简单,谷歌创建了BaseObserable这个基础类来集成监听器注册机制。通过给getter方法添加Bindable注解,通知setter方法。

使用 ObservableFields

ObservableFields 是一个对属性添加 DataBinding 更新功能的代理类,针对不同的数据类型有不同类型的 ObservableFields :ObservableBoolean、 ObservableByte ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble、 ObservableParcelable 等。

这种方式不是主流类型,使用不便,不能扩展属性,所以这里我简单的放个例子:

// 数据 bean 中声明一个属性
 public ObservableField<String> age = new ObservableField<>(); 

// activity 中更新数据
 mBook.age.set("ABABA");
ObservableArrayMap , ObservableArrayList

这2个集合类型是 DataBinding 为了方便数据刷新提供的,不用我们再手动通知集合的变化,只要我们更新了集合的某些数据,就能自动更新相关的 view 的数据

简单举个例子:

// 声明一个集合对象
private ObservableArrayMap<String, Object> mapUser = new ObservableArrayMap<>();

// 这样直接更新集合数据就可以了
mapUser.put("firstName", "zhu");
mapUser.put("lastName", "chen");
mapUser.put("age", 27);
双向数据绑定

双向数据绑定就是当 view 改变时,data 会跟着改变;data 改变时,view 会跟着改变,核心就是给 view 指定 :@={book.name},这里用 EditText 做个例子

  <EditText
            android:text="@={book.name}"
/>

这样当输入不同的内容时,数据就会同步更新到绑定的 book 这个数据对象中。


DataBinding 的更丰富使用

上文书书说 DataBinding 的精髓都在布局的 xml 文件中,其中有着丰富的操作,DataBinding 在xml 有越多的用法,那么就会越多的替代我们 java 中的代码,越多的减少 activity 的代码量,而且使用 DataBinding 后,页面的赋值逻辑在 xml 也会显得刚加清晰,可见。那么现在我们就来看看 DataBinding 都有那些玩法。

1. variable 标签支持的数据类型

java 能使用的 variable 标签当然都能

  • 基本数据类型,我们按照 java 的写法即可,比如 String,int
  • 若是引入了名字相同的类,可以给类添加别名
  • 引用类型使用 import 引入全包名即可
  • list,map 结合类型同引用类型 import 导下包名就行
// 引用类型
<import type="com.bloodcrown.bwdatabindingdemo.Book"/>
<variable name="book" type="Book"></variable>

android:text="@{book.name}"

------------------------------------

// 基本数据类型
// 基本数据类型不用 import 导入,直接在 type 类型里写就行qw,写 int 可能会报错,忽略就行,写相应的包装类型就得引入包了
<variable name="name" type="String"></variable>
<variable name="price" type="int"></variable>

// 在使用 int 等基本数据类型时,注意转成字符串再赋值,DataBinding 不会帮我们做类型转换的
android:text="@{String.valueOf(price)}"

------------------------------------

// 集合数据类型
// 集合的使用方式包括 [] 和 get 2种方式
<import type="java.util.List"/>
<import type="java.util.Map"/>
<variable name="bookList" type="List&lt;Book&gt;"></variable>
<variable name="bookMap" type="Map&lt;String,Book&gt;"></variable>

android:text="@{bookList.get(1).name}"  
android:text="@{bookList[1].price}"  
android:text='@{bookMap["111"].name}'
android:text='@{bookMap.get("111").price}'
// 注意其中特殊特好的使用,集合枚举的 <> 符号直接写xml 不认,需要用转移符号,写 map 时,key 要是 String 的,那么你可以再里面用 " " ,但是这行的 xml 外面就得用 ' ' 才行,这点注意啊,要不 xml 总是报错

------------------------------------

// 设置别名
// 我们引入的不同包的类可能重名,那么自然我们需要加一个别名来加以区分了,用alias表示
<import type="com.zx.databindingdemo.bean.UserBean" />
<import type="com.zx.databindingdemo.bean.user.UserBean" alias="UserBean2"/>

<variable  name="user"  type="UserBean" />
<variable  name="user2"  type="UserBean2"/>

常用的转义字符表

2. 如何调用,注册各种方法

大家想啊,既然在 xml 中都可以直接操作属性值了,那么我调用一个方法还不是妥妥的啊。这里要区分方法的调用和监听方法的注册

对于一个 button 的点击事件来说,我们可以走下面2中方式:

  • DataBinding 会根据 id 名,生成相应的 view 对象,然后我们给这个 view 设置监听
 mainBinding.btnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText( MainActivity.this,"AAA",Toast.LENGTH_SHORT ).show();
            }
        });
  • 在 xml 中声明一个点击事件的对象,然后设置进 android:onClick 属性里
// 先声明点击事件对象
 <import type="android.view.View.OnClickListener"/>
 <variable name="testClick" type="OnClickListener"></variable>

// 再使用
 <Button  android:onClick="@{testClick}"  />

// activity 里填充这个点击事件对象进去
mainBinding.setTestClick(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText( MainActivity.this,"BBB",Toast.LENGTH_SHORT ).show();
            }
        });

总体感觉和原来的注册方式没啥区别

除了各种事件的注册外,我们在利用 DataBinding 可以在 XML 中使用各种方法,静态方法,和对象中的方法都是可以直接调用的,注意是在给 view 属性赋值时可以直接调用

// Utils 里面有一个静态方法

// 导入包含静态方法的类,DataBinding 中要想使用任何类型,除了基本数据类型,都得导包
 <import type="com.bloodcrown.bwdatabindingdemo.Utils"/>
<import type="com.bloodcrown.bwdatabindingdemo.MainPersenter"/>
<variable name="persenter" type="MainPersenter"></variable>

// 方法直接使用即可,直接传参也是可以的
 <Button
            android:text="@{Utils.getName(book.name)}"  

 <Button
            android:text="@{  android:text="@{persenter.getPrice()}"}"  
3. DataBinding 对 lambda 表达式的支持

DataBinding 支持我们直接在 xml 书写 lambda 表达式,常用的就是注册 click 点击事件了,在 view 的 onClick 属性中我们需要传入一个对象,通过上面的内容学习,我们是声明了一个 ClickListener 类型的对象数据出来,然后在 java 中传入这个对象的方式做的。但是我们在这里可以直接实现 lambda 表达式书写一个匿名实现类对象出来,这样就省了我们在 java 中传入对象的代码了。这里说一下 lambda 表达式就是对匿名实现类对象的简写,所以我们虽然看着像是调用了方法的样子,其实我们是写了一个匿名实现类对象出来,本质上还是传入了一个对象。不熟悉 lambda 表达式的看这里:Lambda表达式以及AS 对其的支持 ,还是推荐大家去学习一下 Lambda的,现在各大语言都在往这种函数式编程上靠,像 lambda 表达式这种从函数式编程上借鉴过来的东西,以后只会越来越多的,抗拒这种变化是不明智的。

大家可能会这么写 Lambda 表达式

 <Button
            android:onClick="@{() -> persenter.speak(book.name)}"

但是很遗憾,你这么写会报错,一个类型转换异常的错误,知道为什么吗?这个还是要从 Lambda表达式说起。Lambda 的特征是隐藏实现类的 类名方法名,因为 java 8可以知道从上下文(方法中对于参数的限定)知道类的类型,并且规定了类的里面只能有一个方法,那么这里我们使用的就是这个方法:

 public void onClick(View v) {
                ......
           }

注意我们使用 Lambda 重写的就是这个 onClick 方法,onClick 方法要求传入一个 view 的参数的,上面我们没有传这个参数,所以报错了。那么我们把 Lambda 表达式修改一下:

 <Button
            android:onClick="@{(view) -> persenter.speak(book.name)}"

view 表示点击事件的 view 对象,这样 Lambda 就可以跑了。不过呢,使用 Lambda 写点击事件对象是属于动态类型的,因为每点击一次都会从心生成一个 点击的 匿名实现类 设置 view,所以注意这个动态的特性,合理利用,因为这样点击事件的数据也是可以使用新的了。

4. 其他一些监听器
除了 onClick 之外,还提供了一些特定的点击事件,这里需要注意,下面几个我没用到过,也是从别人那里摘过来的:

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

这里不知道为啥出现2个相同的表格出来,郁闷啊

5. 支持表达式语言

表达式语言和java语法很相似。以下是相同的:

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

6. 不支持的操作

java中的一些特性在DataBinding语法中不支持

  • this
  • super
  • new
  • Explicit generic invocation

7. Null Coalescing 空运算符

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

就等价于

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

8. 关于字符串的符号再次说明一下

可以在属性值使用单引号,在表达式中字符串的值使用双引号:

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

也可以在属性值使用双引号,表达式中的值字符串的值应该使用单引号或者是"`"。

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

9. 自定义 DataBinding 辅助类类名

默认情况下,binding 类的名称取决于布局文件的命名,以大写字母开头,移除下划线,后续字母大写并追加 “Binding” 结尾。这个类会被放置在 databinding 包中。举个例子,布局文件 contact_item.xml 会生成 ContactItemBinding 类。如果 module 包名为 com.example.my.app ,binding 类会被放在 com.example.my.app.databinding 中。

通过修改 data 标签中的 class 属性,可以修改 Binding 类的命名与位置。

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

以上会在 databinding 包中生成名为 CustomBinding 的 binding 类。如果需要放置在不同的包下,可以在前面加 “.”

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

这样的话, CustomBinding 会直接生成在 module 包下。如果提供完整的包名,binding 类可以放置在任何包名中

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

10. DataBinding 中使用资源文件

资源 id 方面我们同传统方式去写就行,但是还是稍有些差别的。不如看这个例子:

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

运行起来,可能有的版本会报错误,因为这是 DataBinding 的 bug,有人说修了,有人说还有,碰上的朋友这样改

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

因为 DataBinding 生成数据的数据格式可能和我们实际需要的不同,注意这点。

其他的例子我们举一个,替换 String.xml 中的占位符:

// 先声明字符串资源
<string name="nameFormat">Full Name: %1$s:%2$s</string>

// DataBinding 在 xml 中可以直接引用使用
android:text="@{@string/nameFormat(firstName, lastName)}"

DataBinding 对于资源的文件头命名可能和 android 传统的有些地方不一样,下面的表是官方文档上的,找资料的话都是这张表,没有其他的资料,大家实际要到问题可以参考这样表,但是优先还是按照 android 原先的资源引用方式来:


简书这几天写列表有问题,截图顶一下

11. DataBinding 中使用资源文件


DataBinding 使用技巧

1. 对 include 的支持

xml 布局中不可避免的要使用 include 标签,那么 DataBinding 怎么兼容这个 include 标签呢。其实只要 外层的 xml 把 include 声明的 DataBinding 数据对象传给 include 就行,但是要注意 DataBinding 不支持 include 的 merge 标签

include 的布局 name.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="user"
            type="com.liangfeizc.databinding.model.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

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

</layout>

总体的布局文件

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <import type="com.liangfeizc.databinding.model.User" />
        <variable
            name="user"
            type="User" />
        <variable
            name="listener"
            type="com.liangfeizc.databinding.listener.OkListener" />
        <variable
            name="okText"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <include
            android:id="@+id/layout_input"
            layout="@layout/layout_input" />

        <include
            layout="@layout/user"
            app:user="@{user}" />

        <include
            layout="@layout/layout_btn_ok"
            app:okText="@{okText}"
            app:listener="@{listener}"/>
    </LinearLayout>
</layout>

另一个 include 的 xml ,layout_input.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <EditText
            android:id="@+id/et_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

</layout>

注意啊,如何调 include 布局的某一个组件呢

  binding.layoutInput.etName.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                User user = new User(s.toString(), "Liang");

                binding.setUser(user);
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });

2. 对 fragment 的支持

上面我们演示的 activity,是通过 DataBinding 通过 setContentView 方法实现 DataBinding 和 activity xml 布局实现绑定的,那么问题来了 fragment 怎么办

activity 我们这么写

private ActivityDemoBinding mBinding;
protected void onCreate(Bundle savedInstanceState) {
    mBinding = DataBindingUtil.setContentView(this, R.layout.activity_demo);
 }

Fragment 我们这么写:

private FragmentHomeBinding mBinding;
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
    mBinding = DataBindingUtil.inflate(inflater, R.layout.homepage_fragment, container, false);
    return mBinding.getRoot();
}

3. 对 ViewStub 的支持

// 我们给 ViewStub 设置初始化函数
 mainBinding.bookStub.setOnInflateListener(new ViewStub.OnInflateListener() {
            @Override
            public void onInflate(ViewStub stub, View inflated) {
                bookStubBinding = DataBindingUtil.bind(inflated);
                bookStubBinding.setBook(mBook);
            }
        });

// 模拟一个点击事件加载 ViewStub 
 mainBinding.setTestClick(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!mainBinding.bookStub.isInflated()) {
                    mainBinding.bookStub.getViewStub().inflate();
                } else {
                    mBook.setName("stub");
                    mBook.setPrice("888");
                }
            }
        });

4. DataBinding 中的注解

DataBinding 可以使用的注解:

  • @BindingMethod :修改view 某个方法名字
  • @BindingAdapter : 给 view 添加set/get 方法,相当于添加自定义属性
  • @BindingConversion : 提供数据类型转换方法

@BindingMethod 用的不多,这里就不说了,我们来看下 @BindingAdapter 这个注解,可以给 view 添加自定义属性,相当的好用啊,我们不用再定义复杂的 view 自定义属性了,@BindingMethod 书写简单,功能强大,xml 中的自顶提示很友好。

先定义给 view 设置这个自定义属性的具体执行方法

public class TextBingUtils {

    @BindingAdapter("info")
    public static void setInfo(TextView view, String info) {
        view.setText(info);
    }

}

注意 @BindingAdapter 注解里面的参数就是这个自定义属性的名字,在 xml 里 app:xx 就可以使用了。另外这个方法写在哪里都没事,但是方法必须是静态的,DataBinding 框架在编译时会自动检索 @BindingAdapter 这个注解的所有方法。

然后在就可以在 xml 中使用了

 <TextView
            app:info='@{"info"}'
/>

@BindingConversion 用的不多,但是也得说一下,这个注解会给 DataBinding 提供默认的2个数据类型之间的转换方法,可以放置一些类型转换错误,写法上和 @BindingAdapter 相同。但是使用这转换器属性时我们必须要小心,因为DataBinding是不懂得区分是否真的需要使用整个转换器的。比方说我们创建两个相同类型的转换方法,DataBinding 就只会使用第一个

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}
<View
    android:background="@{isError.get() ? @color/red : @color/white}"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_height="@{height}" />

DataBinding 对 RecyclerView 的优化

为啥要单开一章说呢,因为的确是太重要了,DataBinding 对 RecyclerView 支持真的是一大亮点啊,直接的就是使用 DataBinding ,我们就不用再写 Viewholder 了。Viewholder 的工作就是 findviewbyid 持有相应的 view 的引用好让我们来设置数据,DataBinding 会自动根据 view 的 id 生成相关的对象,至少从这点出来,DataBinding 对我们都是非常有意义的了。好了,来看看在 RecyclerView 中使用 DataBinding 的基础方式。

新的 ViewHolder 书写方式,每个 ViewHolder 只需要持有相应 view 对应的 DataBinding 辅助类对象,通过他恩那个找到所有的子 view 引用。

public class BookBindingViewHolder extends RecyclerView.ViewHolder {

    private ItemListBinding t;

    public BookBindingViewHolder(ItemListBinding t) {
        super(t.getRoot());
        this.t = t;
    }

    public ItemListBinding getBinding() {
        return t;
    }

    public void setT(ItemListBinding t) {
        this.t = t;
    }
}

item 的布局文件,要相生成 DataBinding 辅助类,必须在 xml 中显示使用 DataBinding

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
>

    <data>
        <import type="com.bloodcrown.bwdatabindingdemo.Book"/>
        <variable name="book" type="Book"></variable>
    </data>

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="10dp"
        android:text="@{book.name}"
        android:textColor="@color/colorPrimary"
        android:textSize="22sp"
    />
</layout>

新的 adapter 通过 DataBinding 类刷新数据

public class BookBindingAdapter extends RecyclerView.Adapter<BookBindingViewHolder> {

    private List<Book> data;

    public BookBindingAdapter(List<Book> data) {
        this.data = data;
    }

    @Override
    public BookBindingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new BookBindingViewHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_list, parent, false));
    }

    @Override
    public void onBindViewHolder(BookBindingViewHolder holder, int position) {
        Log.d("AAA", "position:" + position);
        holder.getBinding().setBook(data.get(position));
        holder.getBinding().setVariable(BR.book, data.get(position));
        holder.getBinding().executePendingBindings();
    }

    @Override
    public int getItemCount() {
        return data == null ? 0 : data.size();
    }

    public void setData(List<Book> data) {
        this.data = data;
    }
}

holder.getBinding().executePendingBindings() 这句话是刷新界面,否则可能会出现这个问题: RecyclerView使用databinding出现数据闪烁问题

另外注意列表在更新数据时也可以使用 holder.getBinding().setVariable(BR.book, data.get(position)); 这个方法是更新xml 里面定义的数据对象的,BR.book 是个 int 值,指向xml 中声明的数据对象的id地址

当然上面这是最简单的 DataBinding 列表实现,甚至多 itemType 都不支持,下面优化下多 itemType 的问题

// 写一个返回 itemType 的接口,然后数据 bean 实现这个接口

public interface IBaseBindingAdapterItemProvide {
    int getItemType();
}

// 然后处理一下 adapter ,能够兼容多类型的 item

public class CatBindingAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private List<IBaseBindingAdapterItemProvide> data;

    public CatBindingAdapter(List<IBaseBindingAdapterItemProvide> data) {
        this.data = data;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == R.layout.item_list) {
            return new BookBindingViewHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_list, parent, false));
        }
        if (viewType == R.layout.item_cat) {
            return new CatBindingViewHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_cat, parent, false));
        }
        return new BookBindingViewHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_list, parent, false));
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        if (holder instanceof BookBindingViewHolder) {
            Book book = (Book) data.get(position);
            ((BookBindingViewHolder) holder).getBinding().setBook(book);
            ((BookBindingViewHolder) holder).getBinding().executePendingBindings();
            return;
        }

        if (holder instanceof CatBindingViewHolder) {
            Cat cat = (Cat) data.get(position);
            ((CatBindingViewHolder) holder).getBinding().setCat(cat);
            ((CatBindingViewHolder) holder).getBinding().executePendingBindings();
        }
    }

    @Override
    public int getItemCount() {
        return data == null ? 0 : data.size();
    }

    @Override
    public int getItemViewType(int position) {
        return data.get(position).getItemType();
    }

    public void setData(List<IBaseBindingAdapterItemProvide> data) {
        this.data = data;
    }
}

恩,现在可以支持多类型的列表了,但是要是我们在实际开发中对这段原始代码不经任何雕琢(封装,设计),那么说明我们做开发真的是没长进。能够初步封装根据效果可以认为是初中级水平,能够写封装出一个易于扩展的优秀的库出来,可以视为高级水平了。期望大家多多用心,在封装自己的库时,是水平提升最快的时候了,知识点的学习是偏记忆,理解,考研智商和记忆能力。那么封装原始代码为库就是考研我们的基础代码水平了,设计到的都是硬知识点,也是最难以提升的部分,需要大毅力才行,但也是最重要的代码技能了。

本文先于篇幅,对于用 DataBinding 来优化 RecyclerView 就写到这里了,更多更优秀的内容我会开单章的,放到通过 MVP 学习代码封装的那部分里。


最后

  1. 不要拒绝 findViewById
    DataBinding 和 findViewById() 并不是互斥的,DataBinding的源码里面也是用到了findViewById()。如果某些情况真的不适合使用DataBinding,那就用回findViewById吧。

  2. xml中的表达式尽量简单
    xml 文件中不要出现过于复杂业务逻辑,只出现简单的 UI 相关的表达式。不要以为Data Binding是万能的,而想尽办法把逻辑写在xml中。往往这个时候你就应该将逻辑写在绑定的ViewModel里面。

  3. 注意clean
    有时候因为修改接口等原因要修改绑定的bean类,这时候偶尔会遇到一些神奇的bug。不要慌张,直接clean一下项目再make project一次往往就好了

  4. 使用BindingConversion注解时需要慎重
    原因上面已经说了。但是它也不失为一个很好用的注解,比方说使用它来进行字体的自定义。具体可以参照下面的文章:使用DataBinding来进行字体的自定义


kotlin 支持

在 kotlin 中使用,还要在 gradle.properties 中配置一个参数

android.databinding.enableV2=true

接下来,在布局文件当中,选中根布局的ViewGroup,然后按住 「Alt + 回车键」 如图


16abb5f5b0765477.png

参考资料

这里有一个 DataBinding 的使用规范的文章,不是赞同所有内容,但还是推荐大家看看