DataBinding 使用介绍

4,306 阅读8分钟

DataBinding 是 Google 在 Jetpack 中推出的一款数据绑定的支持库,利用该库可以实现在页面组件中直接绑定应用程序的数据源。使其维护起来更加方便,架构更明确简介。

简介

那么 DataBinding 本质上到底是个什么呢?它在开发正扮演着什么样的角色?我们应该如何使用? DataBinding 名为数据绑定,他的功能很简单,就是将数据绑定在 UI 页面上(这不是废话吗),明白这一点很重要,DataBinding 唯一的作用,也是他的使命,就是绑定数据,后面的一切,以及所有的支持库,本质上都是为了支持这个功能。

绑定一词有两种解释,第一是将数据绑定在 UI 元素上;第二是将 UI 上的数据绑定到对应的数据模型中;此外,除了将数据与 UI 绑定在一起,还要支持对数据及 UI 的变动观察,其中一个发生变动就需要同步到另一个上去,也就是同步。

那么 DataBinding 对于整体架构、对于我们开发者来说到底意味着什么呢?由于 Android 开发语言的限制,最早期的 Android 开发都用 MVC 架构,导致代码臃肿不堪,难以维护,后来出现了 MVP 架构,代码倒是清晰了起来,但同样也存在过度设计、各种接口方法满天飞、内存泄漏的问题,导致很多人很难准确的使用 MVP。无论是 MVC 还是 MVP 或多或少都存在一些问题,始终无法找到一个完美的解决方案,其根本上是由于 Android 开发的模式本身导致的,我们需要先监听数据的变化,然后再将变化后的数据同步更新到 UI 上,这样的步骤我们一直在重复,MVC/MVP 本质上也没有解决这个问题,这样的重复性代码我们写了一次又一次。而 DataBinding 就是为了解决这个问题而存在的,我们只需要将数据绑定到 UI 元素上,更新数据时 UI 就会跟着改变,反之亦然,大大节省了我们的代码。 下面就来讲讲他是如何实现的。

启用 DataBinding

首先设置使用 Databinding,在 app module 的 build.gradle 中添加如下代码即可:

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

布局绑定

在使用 DataBinding 时就不能按照之前的方式来编写布局文件了,布局文件的跟布局应该是 layout,layout 中同时存放要绑定的数据及布局,如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"><data>
        <variable name="title" type="java.lang.String" />
    </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="@{title}" />
    </LinearLayout>
</layout>

layout 为跟布局,data 节点中存放数据,下面就是我们常见的布局文件。 data 中的 variable 标签为变量,类似于我们定义了一个变量,name 为变量名,type 为变量全限定类型名,包括包名。布局中通过 @{} 来引用这个变量的值,{} 中可以是任意 Java 表达式,但不推荐使用过多的代码。

我们可以使用 import 语法来导入类,以及使用 alias 设置别名:

    <data>
        <import type="java.lang.String"/>
        <import type="com.zhangke.demo.jetpack.entity.User"
                alias="ZKUser"/>
        <variable name="title" type="String" />
        <variable name="User" type="ZKUser" />
    </data>

我们也可以通过 default 字段设置默认值:

<TextView android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{title, default=my_default}"/>

绑定数据

我们再新建一个类似上述示例代码中的 DataBinding 布局文件 activity_main.xml 之后,Gradle 会根据布局创建一个 ActivityMainBinding 类,我们需要获取该对象来绑定数据。 使用 DataBinding 时,我们不需要再按照之前的 setContentView 的方式来设置布局到 Activity 中,应该通过 DataBindingUtil#setContentView 来设置,该方法会返回对应的 DataBinding 对象,例如我们创建的布局文件为 activity_main,那么生成的就是 ActivityMainBinding,我们可以通过在 data 节点使用 class 关键字更改这种默认的名字:

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

前面的 . 号表示使用当前包名,也可以使用全限定包名指定。 然后是获取绑定类:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    binding.lifecycleOwner = this
    binding.title = "Title"
}

我们先获取 DataBinding 对象,然后设置 variable 数据,lifecycleOwner 适用于管理生命周期的方法,设置后 Databinding 可以感知到 Activity 的生命周期,保证数据在可见时才会更新,不可见时不会更新数据。 如果是在 Fragment/ListView/RecyclerView 中,我们可以通过下面的方法获取 DataBinding:

binding = ActivityMainBinding.inflate(layoutInflater, null, false)

绑定普通数据

DataBinding 可以绑定普通数据对象(非 Observable/LiveData),例如上述例子中绑定了一个 String 类型的数据。绑定普通数据我们只需要按照上述的代码设置即可。

绑定可观察数据

绑定可观察数据意味着当数据变化时 UI 会跟着一起变化,绑定可观察数据有三种方式:objects、fields 和 collections.

对单个变量的绑定:fields

对于一些数据类,如果我们不想继承 BaseObservable 或者只需要其中几个字段支持可观察,那么可以使用这种方式来创建可观察数据:

class User {
    var age = ObservableInt()
    var name = ObservableField<String>()
}

对于基本类型和 Parcelable 我们可以直接使用对应的包装类:

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

引用类型使用带有泛型参数的 ObservableField 类来创建:

var name = ObservableField<String>()

泛型参数为数据类型。

对集合的绑定:collections

我们同样可以在布局中绑定集合中的某个元素,当集合中的数据发生变化后会同步更新到 UI。

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <import type="androidx.databinding.ObservableMap"/>
        <import type="androidx.databinding.ObservableList"/>
        <variable name="map" type="ObservableMap&lt;String, Object>"/>
        <variable name="list" type="ObservableList&lt;Object>"/>
    </data>
...
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{String.valueOf(map.count)}" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{String.valueOf(list[0])}" />
</layout>

绑定数据到 UI 中:

val map = ObservableArrayMap<String, Any>().apply { put("count", 0) }
binding.map = map
val list = ObservableArrayList<Any>().apply { add(0) }
binding.list = list

对于 List 来说,可以直接使用 [] 运算符( list[0] )获取对应位置的元素。 而 Map 就很有趣了,可以使用 . 运算符直接获取 key 对应的 value:map.count,这还是很有意义的,我们如果不想定义一个数据实体,可以直接使用 Map 来替代。

绑定对象:objects

这种是最常用的一种方式,需要绑定的数据实体类继承 BaseObservable:

class Person : BaseObservable() {
    @get:Bindable
    var country: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.country)
        }
    
    @get:Bindable
    var sex: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.sex)
        }
}

首先要在需要支持可观察的数据上添加 @get:Bindable 注解,然后重写 set 方法,在其中调用 notifyPropertyChanged 方法表示更新该数据,BR 是自动生成的,包名跟当前包名一致,会根据 Bindable 注解的变量生成对应的值;也可以调用 notifyChange() 方法更新所有数据。

绑定 LiveData

LiveData 目前也支持数据绑定,绑定方式跟上述介绍的一样:

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable name="desc"
            type="androidx.lifecycle.MutableLiveData&lt;String>" />
    </data>
...
    <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
           android:layout_gravity="center_vertical|end"
           android:textSize="18sp"
           android:text="@{desc}" />
</layout>

我们可以直接将 LiveData 赋值给 text,然后绑定数据:

val desc = MutableLiveData<String>()
binding.desc = desc

需要注意的是,老版本的 Gradle 是不支持 LiveData 的绑定的,需要更新到 Gradle 3.4 及以上。 另外,使用 LiveData 有个显示,更新 LiveData 数据时必须在主线程才行。

双向绑定

上述的单向绑定是指数据变化后更新 UI,而双向绑定是指其中任意一个变化后都会同步更新到另一个。 双向绑定使用 @={} 表达式来实现:

    <data>
    ...
        <variable
            name="input"
            type="androidx.databinding.ObservableField&lt;String>" />
    </data>
...
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={input}"/>

事件绑定

事件绑定其实跟数据绑定一样,本质上就是将监听器对象绑定到 UI 元素上:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
    <data class="">
    ...
        <variable
            name="handler"
            type="com.zhangke.demo.jetpack.MainActivity.EventHandler" />
    </data>
...
    <Button
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginTop="10dp"
       android:onClick="@{handler::onToastBtnClick}"
       android:text="ToastClick"/>
</layout>

然后我们写好监听事件,绑定到 binding 中即可:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        ...
        binding.handler = EventHandler()
    }

    ...
    inner class EventHandler {
        fun onToastBtnClick(v: View) {
            Toast.makeText(this@MainActivity, "Click", Toast.LENGTH_SHORT)
                .show()
        }
    }
}

自定义参数绑定:BindingAdapter

目前已经支持的双向绑定的参数列表如下:

除了上述的参数外,我们也可以使用 BindingAdapter 创建自定义参数。

例如我们需要使用 Glide 加载网络图片,可以先创建一个使用了 BindingAdapter 注解的函数,注解中的字段为参数名,函数的第一个参数必须为目标 View 或者其子类,因此使用 Kotlin 时我们可以定义为扩展函数,这样使用很方便。

@BindingAdapter("imageUrl")
fun ImageView.loadImage(url: String) =
    Glide.with(this.context).load(url).into(this)

然后我们就可以在布局代码中直接使用该参数加载图片:

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_marginTop="10dp"
    imageUrl="@{imageUrl}"
    android:layout_gravity="center_horizontal"/>

好了,DataBinding 差不多就讲到这里了,上面的全部代码已经放到了我的 Github 上,需要的自取,欢迎 star~

github.com/0xZhangKe/J…

如果觉得还不错的话,欢迎关注我的个人公众号,我会不定期发一些干货文章~

公众号