DataBinding(含ViewBinding)(一)使用

1,250 阅读8分钟

使用了很久的DataBinding和ViewBinding,突然想看ViewBinding是怎样生成XxxBinding类,所以打算写两篇文章来,最后发现两篇文章不够,就写了三篇。

代码中用到的示例已经上传github:示例地址

DataBinding 包含三篇博客:

DataBinding(含ViewBinding)(二)绑定原理

DataBinding(三)构建过程分析

一、ViewBinding的使用

1.1 生成ViewBinding文件

ViewBinding是as3.6版本加入的,只需要在app.build中如下引入即可

android {
    buildFeatures{
        viewBinding = true
    }
}

同步之后即可在app/build/generated/data_binding_base_class_source_out/buildTypes/out/{buildTypes}/out/{包名}/databinding生成 ViewBinding文件

1.2 使用及封装
  1. Activity 中使用

     // MainActivity.kt
     class MainActivity : AppCompatActivity() {
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             val binding = ActivityMainBinding.inflate(layoutInflater)
             setContentView(binding.root)
    
             initView(binding)
         }
    
         private fun initView(binding: ActivityMainBinding) {
             binding.tvTitle.text = "viewBindingTitle"
    
             val fragmentManager = supportFragmentManager
             val fragmentTransaction = fragmentManager.beginTransaction()
             fragmentTransaction.replace(R.id.container, MainFragment())
             fragmentTransaction.commit()
         }
     }
    

通过 ActivityMainBinding.inflate() 获取到 ActivityMainBinding后,可以直接调用其属性来进行各个View的配置。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_title"/>

</androidx.constraintlayout.widget.ConstraintLayout>

2. 在Fragment中使用

    class MainFragment : Fragment() {
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            val binding = FragmentMainBinding.inflate(inflater)
            initView(binding)
            return binding.root
        }

        private fun initView(binding: FragmentMainBinding) {
            binding.tvShowText.text = "在MainFragment中设置的"
        }
    }

对应xml为

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_show_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="我是fragment" />

</androidx.constraintlayout.widget.ConstraintLayout>

如果不想使用ViewBinding可以在根ViewGroup中加入如下属性 tools:viewBindingIgnore="true" 就不会生成 XxxBinding文件

封装到BaseActivity中

abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {

    lateinit var binding: VB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val parameterizedType = this.javaClass.genericSuperclass as ParameterizedType
        val types = parameterizedType.actualTypeArguments
        val vb = types[0] as Class<VB>
        binding = vb.getMethod("inflate", LayoutInflater::class.java)
            .invoke(null, layoutInflater) as VB

        setContentView(binding.root)

        initView()
        initData()
    }

    protected abstract fun initView()
    protected abstract fun initData()
}

继承BaseActivity就可以直接使用binding或者各个View了。同样的还有BaseFragment:

abstract class BaseFragment<VB : ViewBinding> : Fragment() {

    protected lateinit var binding: VB

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val parameterizedType = javaClass.genericSuperclass as ParameterizedType
        val types = parameterizedType.actualTypeArguments
        val vb = types[0] as Class<VB>
        binding = vb.getMethod("inflate", LayoutInflater::class.java)
            .invoke(null, layoutInflater) as VB
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView()
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        initData()
    }

    abstract fun initView()
    abstract fun initData()
}

DataBinding的使用

启用DataBinding的方法是在相应module的build.gradle中加入如下代码,同步后就能引入对DataBinding的支持:

buildFeatures{
    dataBinding = true
}

二、基础入门

打开布局文件,选中根布局 ViewGroup,按按住 Alt + 回车键,点击 Convert to data binding layout,或者鼠标右键选择 Show Context Action 选择 Convert to data binding就可以生成 DataBinding 需要的布局规则:
布局文件

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

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

和原始布局的区别在于多出一个 layout 标签将原布局包裹了起来,data 标签用于声明要用到的变量以及变量类型,data 标签的作用就像一个桥梁搭建了 View 和 Model 之间的通道,将数据(Model)与UI(View)进行绑定。

先来创建一个model

data class EmployeeBean(val id: String, val name: String, var email: String) 

在xml中使用

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

    <data>
        <import type="com.zbt.databinding.EmployeeBean"/>
        <variable
            name="employeeBean"
            type="EmployeeBean" />
    </data>

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

        <TextView
            android:id="@+id/tv_name"
            style="@style/TvStyle"
            android:text="@{employeeBean.name, default=姚dae}" />

        <TextView
            style="@style/TvStyle"
            android:text="@{employeeBean.email}" />

    </LinearLayout>
</layout>

其中导入数据,可以有多种方式,使用 import或者type中直接导入类包含包名。而 java.lang.* 包中的类会被自动导入,可以直接使用

上面的代码中生命了一个EmployeeBean类型变量 employeeBean。可以在控件中直接使用该变量。通过 @{employeeBean.name}使TextView引用到相关的变量,DataBinding会将 employeeBean.name映射到相应的 getter方法。

Activity中通过 DataBindingUtil设置布局文件,并且获取 XxxBinding文件,然后通过 XxxBinding设置变量 employeeBean,我们可以看到显示的数据

protected lateinit var dataBinding: DB

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    dataBinding = DataBindingUtil.setContentView(this, getLayoutId())

    initData()
}

override fun getLayoutId(): Int {
    return R.layout.activity_test1
}

override fun initData() {
    dataBinding.employeeBean = EmployeeBean("12345", "张三", "zhangsan@163.com")
    dataBinding.tvName.text = "yao da e"
}

employeeBean还没有赋值过,所以我们可以给个默认值。需要注意的是,默认值在代码中是不会显示的。

android:text="@{employeeBean.name, default=姚dae}"

也可以在代码中对单个控件赋值,需要主要的是, 下面的赋值不能和 android:text="@{employeeBean.email}"同时使用。

 dataBinding.tvEmail.text = "xxx@163.com"

当数据变化,需要重新对ViewDataBinding变量值进行重新赋值,UI才能刷新

三、单向数据绑定

上面提到,数据改变后需要重新赋值,那么如何实现自动数据更新呢。实现数据变化自动刷新UI的方式有三种:BaseObservableObservableFieldObservableArray

3.1 BaseObservable

BaseObservable 提供了 notifyChange()notifyPropertyChanged(int fieldId)两个方法,前者会刷新所有的值,后者则只更新对应 BR的flag,该 BR 的生成通过注释 @Bindable 生成,可以通过 BR notify 特定属性关联的视图,如下model

class WorkBean : BaseObservable() {
    //如果是 private 修饰符,则在成员变量的 get 方法上添加 @Bindable 注解
    @Bindable
    var workName: String = ""
        get() = field
        set(value) {
            field = value
            notifyPropertyChanged(com.zbt.databinding.BR.workName)
        }

    var workContent: String = ""
        //如果是 private 修饰符,则在成员变量的 get 方法上添加 @Bindable 注解,在kotlin中属性的get()使用get()是不对在BR中生成id
        @Bindable
        get() = field
        set(value) {
            field = value
            /**
             * 如果是notifyChange(),可以不用@Bindable注解,@Bindable主要是在BR文件中生成相应的id,id可以关联相应属性的视图
             */
            // 更新所有字段
            notifyChange()
        }

    var workTime: String = ""
}

对于 name字段的 set()只更新 name字段,对于 workContent字段其 set()会更新所有的字段。

看一下演示示例,三个按钮,分别更改相应数据

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

    <data>

        <variable
            name="workBean"
            type="com.zbt.databinding.WorkBean" />
    </data>

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

        <TextView
            style="@style/TvStyle"
            android:text="@{workBean.workName}" />

        <TextView
            style="@style/TvStyle"
            android:text="@{workBean.workContent}" />

        <TextView
            style="@style/TvStyle"
            android:text="@{workBean.workTime}" />

        <Button
            style="@style/BtnStyle"
            android:onClick="changeNameAndTime"
            android:text="改变属性 name = 写代码 time = 4小时" />

        <Button
            style="@style/BtnStyle"
            android:onClick="changeContentAndTime"
            android:text="改变属性 content = 加快速度 time = 8小时" />

        <Button
            style="@style/BtnStyle"
            android:onClick="changeTime"
            android:text="改变属性 time = 2个工作日" />
    </LinearLayout>
</layout>

class Test2Activity :

BaseActivity<ActivityTest2Binding>() {

    private val workBean = WorkBean()

    override fun getLayoutId(): Int {
        return R.layout.activity_test2
    }

    override fun initData() {
        workBean.workName = "dataBinding"
        workBean.workContent = "数据刷新"
        workBean.workTime = "1个小时"

        /**
         * 注册实现androidx.databinding.Observable中的OnPropertyChangedCallback监听器,当可观察对象的数据发生更改时,
         * 监听器就会收到通知,其中 propertyId 就是@Bindable 编译后生成的字段
         */
        workBean.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
            override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
                when (propertyId) {
                    BR.workName -> println("BR.workName changed")
                    BR._all -> println("all changed")
                    else -> println("other changed")
                }
            }
        })

        dataBinding.workBean = workBean
    }

    fun changeNameAndTime(view: View) {
        workBean.workName = "写代码"
        workBean.workTime = "4小时"
    }

    fun changeContentAndTime(view: View) {
        workBean.workContent = "加快速度"
        workBean.workTime = "8小时"
    }

    fun changeTime(view: View) {
        workBean.workTime = "2个工作日"
    }
}

可以看到,更改 workName只更新了 workName对应的视图;更改 workContent则刷新了所有视图;对于只更改 workTime字段,则不回更新 TextView

3.2 ObservableField

继承 Observable需要设置 notifyXxx()才能更新UI,那么 ObservableField则更为简单,不需要notify操作即可更新UI。ObservableField 也是间接继承自 Observable,那么可以理解为其是对 BaseObsevable中字段的注解和刷新等操作的封装。官方提供了对八大基本类型数据的封装,如 ObservableByteObservableInt等及 ObservableParcelable,也可以通过 Observable<T>泛型来声明其他类型数据。

在Model对每个字段都使用相应的 Obserable,如下:

class ObservableWorkBean(name: String, card: Boolean) {
    var name: ObservableField<String> = ObservableField(name)
    var card: ObservableBoolean = ObservableBoolean(card)
}

在使用时,用 ObservableFiled.set()赋值即可刷新UI

override fun initData() {
    val workBean = ObservableWorkBean("张三", false)
    dataBinding.workBean = workBean
    dataBinding.clickHandler = ObservableWorkBeanChangeHandler(workBean)
}

class ObservableWorkBeanChangeHandler(val workBean: ObservableWorkBean) {
    fun changeName() {
        /**
         * ObservableField 中的set()方法
         */
        workBean.name.set("李四 ${Random.nextInt(1000)}")
    }

    fun changeCard() {
        workBean.card.set(true)
    }
}
3.3 ObservableArray

DataBinding提供了替换 ListMap的字段,分别为 ObservableArrayListObservableArrayMap,所以我将其命名为 ObservableArray

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

    <data>

        <variable
            name="strs"
            type="androidx.databinding.ObservableList&lt;String&gt;" />

        <variable
            name="map"
            type="androidx.databinding.ObservableArrayMap&lt;String,String&gt;" />

        <variable
            name="index"
            type="int" />
        
        <variable
            name="key"
            type="String" />
    </data>

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

        <TextView
            style="@style/TvStyle"
            android:text="@{strs[index]}" />

        <TextView
            style="@style/TvStyle"
            android:text="@{map[key]}" />

        <Button
            style="@style/BtnStyle"
            android:onClick="changeIndexAndKey"
            android:text="改变index和key" />

        <Button
            style="@style/BtnStyle"
            android:onClick="changeValue"
            android:text="改变value" />

    </LinearLayout>
</layout>

Test4Activity

override fun initData() {
    val list = ObservableArrayList<String>()
    list.add("12345")
    list.add("67890")

    val map = ObservableArrayMap<String, String>()
    map.put("map", "key shi map")
    map.put("list", "key shi list")

    dataBinding.strs = list
    dataBinding.map = map
    dataBinding.index = 0
    dataBinding.key = "map"
}

fun changeIndexAndKey(view: View) {
    dataBinding.index = 1
    dataBinding.key = "list"
}

fun changeValue(view: View) {
    dataBinding.strs?.set(1, "abc")
    dataBinding.map?.put("list", "key shi list value changed")
}

可以看到改变index和改变value都会刷新UI,需要注意的是要判断下标越界等情况。

DataBinding中只提供了ObservableArrayListObservableArrayMap。如若要使用HashMap或者是其结构的ObservableList或者是ObservableMap可以如下:

class ObservableHashMap<K, V> : HashMap<K, V>(), ObservableMap<K, V> {
    override fun addOnMapChangedCallback(callback: ObservableMap.OnMapChangedCallback<out ObservableMap<K, V>, K, V>?) {
        TODO("Not yet implemented")
    }

    override fun removeOnMapChangedCallback(callback: ObservableMap.OnMapChangedCallback<out ObservableMap<K, V>, K, V>?) {
        TODO("Not yet implemented")
    }
}

class ObservableLinkedList<T> : LinkedList<T>(), ObservableList<T> {
    override fun addOnListChangedCallback(callback: ObservableList.OnListChangedCallback<out ObservableList<T>>?) {
        TODO("Not yet implemented")
    }

    override fun removeOnListChangedCallback(callback: ObservableList.OnListChangedCallback<out ObservableList<T>>?) {
        TODO("Not yet implemented")
    }
}

四、双向数据绑定

双向数据绑定的意思是当数据改变时视图会刷新,当视图改变时也可以同时改变数据

4.1 双向数据绑定

如下例,当 EditText输入的内容变化时,会同步到变量 workBean中,绑定方式比单向绑定中多了一个等号:android:text="@={workBean.name}"

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

    <data>

        <variable
            name="workBean"
            type="com.zbt.databinding.ObservableWorkBean" />
    </data>

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

        <TextView
            style="@style/TvStyle"
            android:text="model - View : 双向数据绑定" />

        <TextView
            style="@style/TvStyle"
            android:text="@{workBean.name}" />

        <EditText
            style="@style/EtStyle"
            android:layout_marginTop="10dp"
            android:text="@={workBean.name}" />

    </LinearLayout>
</layout>
4.2 事件绑定

事件绑定也是一种变量绑定,只不过设置的变量是回调接口,事件回调多用于以下事件:

  • android:onclick
  • andorid:onLongClick
  • android:onTextChanged
    ……

如设置 onClickafterTextChanged

<TextView
    style="@style/TvStyle"
    android:layout_marginTop="60dp"
    android:text="事件绑定" />

<TextView
    style="@style/TvStyle"
    android:onClick="@{()->eventBinding.onNameClick(workBean)}"
    android:text="@{workBean.name}" />

<EditText
    style="@style/EtStyle"
    android:layout_marginTop="10dp"
    android:afterTextChanged="@{eventBinding.onAfterTextChanged}"
    android:hint="@{workBean.name}" />

Test5Activity

class Test5Activity : BaseActivity<ActivityTest5Binding>() {

    override fun getLayoutId(): Int {
        return R.layout.activity_test5
    }

    override fun initData() {
        val workBean = ObservableWorkBean("张三", true)
        dataBinding.workBean = workBean
        dataBinding.eventBinding = EventBinding(this, workBean)
    }

    class EventBinding(private val context: Context, private val workBean: ObservableWorkBean) {
        fun onNameClick(observableWorkBean: ObservableWorkBean) {
            Toast.makeText(context, "card: ${observableWorkBean.card.get()}", Toast.LENGTH_LONG).show()
        }

        fun onAfterTextChanged(s: Editable) {
            workBean.name.set(s.toString())
        }
    }
}

演示如下:

事件绑定中方法的引用可以使用,可以和保持事件回调方法一致:android:afterTextChanged="@{eventBinding.onAfterTextChanged}",也可以使用lambda不遵循默认的方法签名 android:onClick="@{()->eventBinding.onNameClick(workBean)}"

五、运算符的使用

5.1 基础运算符

DataBinding支持在布局文件中使一些基础的运算符、表达式和关键字:

  • 算数运算+ - / * %
  • 字符串合并 +
  • 逻辑 && ||
  • 二元 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <=
  • Instanceof
  • character, String, numeric, null
  • Cast
  • 方法调用
  • Field 访问
  • Array 访问 []
  • 三元运算符 ?:
5.2 ??运算符

空合并运算符 ?? 会取第一个不为 null 的值作为返回值,前面的不为 null则取前面的值,否则取后面的值

<TextView
    android:id="@+id/tv_email"
    style="@style/TvStyle"
    android:text="@{employeeBean.email??employeeBean.name}" />
5.3 属性控制

例如控制 View的显隐

<import type="android.view.View" />
<TextView
            style="@style/TvStyle"
            android:text="view 可见测试"
            android:visibility="@{workBean.card? View.VISIBLE : View.INVISIBLE}" />
避免空指针异常

DataBinding 也会自动帮助我们避免空指针异常 例如,如果 "@{workBean.name}"workBeannull 的话,workBean.name 会被赋值为默认值 null,而不会抛出空指针异常

六、集合的使用

DataBinding支持在布局文件中使用数据、List、Set和Map,通过如array[index]获取元素

为了和 varible的<>标签做区分,在使用泛型的<>需要转义

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

    <data>

        <variable
            name="array"
            type="String[]" />

        <variable
            name="list"
            type="java.util.List&lt;String&gt;" />

        <variable
            name="map"
            type="java.util.Map&lt;String, String&gt;" />

        <variable
            name="set"
            type="java.util.Set&lt;String&gt;" />

        <variable
            name="sparse"
            type="android.util.SparseArray&lt;String&gt;" />

        <variable
            name="index"
            type="int" />

        <variable
            name="key"
            type="String" />
    </data>

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

        <TextView
            style="@style/TvStyle"
            android:text="集合的使用" />

        <TextView
            style="@style/TvStyle"
            android:text="@{array[index]}" />

        <TextView
            style="@style/TvStyle"
            android:text="@{sparse[index]}" />

        <TextView
            style="@style/TvStyle"
            android:text="@{list[index]}" />

        <TextView
            style="@style/TvStyle"
            android:text="@{map[key]}" />

        <TextView
            style="@style/TvStyle"
            android:text='@{set.contains(key)?key:"不包含"}' />

    </LinearLayout>
</layout>

Test6Activity:

class Test6Activity : BaseActivity<ActivityTest6Binding>() {

    override fun getLayoutId(): Int {
        return R.layout.activity_test6
    }

    override fun initData() {

        dataBinding.array = arrayOf("array")

        val sparseArray = SparseArray<String>()
        sparseArray[0] = "sparseArray"
        dataBinding.sparse = sparseArray

        dataBinding.list = listOf("list")

        val map = mutableMapOf<String, String>()
        map.put("databinding", "map")
        dataBinding.map = map

        val set = mutableSetOf<String>()
        set.add("set")
        dataBinding.set = set

        dataBinding.key = "databinding"
        dataBinding.index = 0
    }
}

七、标签include和viewStub中使用

对于 include的布局文件,一样是支持通过 DataBinding来进行数据绑定的。

在布局文件中引用 viewStub 布局

view_include.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">

    <data>

        <variable
            name="workBean"
            type="com.zbt.databinding.WorkBean" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/teal_200"
            android:gravity="center"
            android:padding="20dp"
            android:text="@{workBean.workName}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

view_stub.xml:

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

    <data>

        <variable
            name="workBean"
            type="com.zbt.databinding.WorkBean" />
    </data>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#42A5F5"
        android:gravity="center"
        android:padding="20dp"
        android:text="@{workBean.workContent}" />

</layout>

使用include和viewStub

<?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="workBean"
            type="com.zbt.databinding.WorkBean" />
    </data>

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

        <Button
            android:id="@+id/btn_show"
            style="@style/BtnStyle"
            android:text="显示viewStub" />

        <include
            layout="@layout/view_include"
            bind:workBean="@{workBean}" />

        <ViewStub
            android:id="@+id/view_stub"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout="@layout/view_stub"
            bind:workBean="@{workBean}" />

    </LinearLayout>
</layout>

如果在 xml 中没有使用 bind:workBean="@{workBean}"ViewStub 进行数据绑定,则可以使用 setOnInflateListener回调函数,在回调函数中进行数据绑定

八、注解

DataBinding提供了如 BindableBindingAdapterBindingConversionBindingMethodInverseMethod等注解,其中Bindable在数据绑定中已经使用过了,是用于将数据与视图绑定然后通知。

BindingAdapter注解用于修改原有属性或者自定义属性。注解的对象可以是已有的属性,如 android:textandroid:layout_width等,也可以自定义属性然后在布局文件中使用。

BindingConversion注解用于对数据进行转换,或者类型转换

BindingMethod DataBinding默认可以在布局中使用setter方法作为自定义属性, 但是如果不是setter格式的方法就要使用BindingMethod注解了。通过创建一个自定义属性来关联一个类中已有的方法.

首先我们在代码中使用者这几个注解

class Test8Activity : BaseActivity<ActivityTest8Binding>() {

    companion object {
        /**
         * 更改原生的,这种情况下,整个报下的TextView的android:text都会跟更改
         */
        @JvmStatic
        @BindingAdapter("android:text")
        fun setText(tv: TextView, text: String) {
            tv.text = "$text 是大佬"
        }

        /**
         * 自定义
         */
        @JvmStatic
        @BindingAdapter("text")
        fun printText(tv: TextView, text: String) {
            println("获取到的text:$text")
        }
//        // 放在Companion object 会报这个错误:@BindingConversion is only allowed on public static methods conversionString(java.lang.String)
//        @BindingConversion
//        fun conversionString(text: String): String? {
//            return "$text-conversionString"
//        }
    }

    override fun getLayoutId() = R.layout.activity_test8

    override fun initData() {
        val employeeBean = EmployeeBean("12345", "张三", "12345@163.com")
        dataBinding.employeeBean = employeeBean

        dataBinding.tvtToast.setOnClickListener {
            val workBean = WorkBean()
            dataBinding.workBean = workBean
            workBean.workName = "码字"
        }
    }
}

@BindingConversion
fun convertStringToColor(str: String): Int {
    return when (str) {
        "红色" -> Color.parseColor("#FF1493")
        "橙色" -> Color.parseColor("#0000FF")
        else -> Color.parseColor("#FF4500")
    }
}

@BindingMethods(
    BindingMethod(
        type = TextView::class,
        attribute = "showToast",
        method = "showToast"
    )
)
class TextViewToast(context: Context, attrs: AttributeSet) :
    androidx.appcompat.widget.AppCompatTextView(context, attrs) {
    fun showToast(s: String?) {
        if (TextUtils.isEmpty(s)) {
            return
        }
        Toast.makeText(context, s, Toast.LENGTH_LONG).show()
    }
}

然后在 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">

    <data>

        <import type="com.zbt.databinding.Test8Activity.Companion" />

        <variable
            name="employeeBean"
            type="com.zbt.databinding.EmployeeBean" />

        <variable
            name="workBean"
            type="com.zbt.databinding.WorkBean" />
    </data>

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

        <TextView
            style="@style/TvStyle"
            android:text="@{employeeBean.name}"
            app:text="@{employeeBean.email}" />

        <TextView
            android:id="@+id/textView1"
            style="@style/BtnStyle"
            android:layout_marginTop="20dp"
            android:padding="20dp"
            android:text="纯蓝"
            android:textColor='@{"蓝色"}' />

        <TextView
            style="@style/BtnStyle"
            android:layout_marginTop="20dp"
            android:padding="20dp"
            android:text="橙色"
            android:textColor='@{"橙色"}' />

        <TextView
            style="@style/BtnStyle"
            android:layout_marginTop="20dp"
            android:padding="20dp"
            android:text="红色"
            android:textColor='@{"红色"}' />

        <com.zbt.databinding.TextViewToast
            android:id="@+id/tvt_toast"
            style="@style/BtnStyle"
            android:layout_marginTop="20dp"
            android:padding="20dp"
            android:text="点我"
            app:showToast="@{workBean.workName}"
            android:background="@color/teal_200"/>

    </LinearLayout>
</layout>
  • BindAdapter 更改原生属性,整个包下的属性都会被更改
  • BindAdapter 自定义属性,则在使用的地方更改
  • BindingConversion 需要写成顶层函数,或者是object类中定义
  • BindingMethod、BindingMethods用于类的注释

这里的例子可以查看代码中的 Test8ActivityTest9Activity

RecyclerView中使用

在代码中 RecyclerView是经常使用的,结合DataBinding可以使代码的结构更为简洁。

因为 RecyclerView是通过 adapter 设置数据的,所以没法通过 DataBinding设置数据,那么接下来我们看一下使用的。

item_view.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">

    <data>

        <variable
            name="employeeBean"
            type="com.zbt.databinding.EmployeeBean" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{employeeBean.name}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{employeeBean.id}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_email"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{employeeBean.email}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@id/tv_name"
            app:layout_constraintRight_toLeftOf="@id/tv_id"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

adapter

class EmployeeAdapter(private val employeeBeans: List<EmployeeBean>) :
    RecyclerView.Adapter<EmployeeAdapter.EmployeeViewHolder>() {

    inner class EmployeeViewHolder(val binding: ItemViewBinding) :
        RecyclerView.ViewHolder(binding.root) {
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmployeeViewHolder {
        return EmployeeViewHolder(
            DataBindingUtil.inflate(
                LayoutInflater.from(parent.context),
                R.layout.item_view,
                parent,
                false
            )
        )
    }

    override fun onBindViewHolder(holder: EmployeeViewHolder, position: Int) {
        holder.binding.employeeBean = employeeBeans[position]
    }

    override fun getItemCount(): Int {
        return employeeBeans.size
    }
}

Activity中设置数据:

class Test10Activity : AppCompatActivity() {

    private val employeeObservableList = ObservableArrayList<EmployeeBean>().apply {
        for (i in 0..32) {
            add(
                EmployeeBean(
                    "${Random().nextInt()}",
                    "张三${Random().nextInt(100)}",
                    "${Random().nextInt(1000)}@163.com}"
                )
            )
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test10)

        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        recyclerView.layoutManager = LinearLayoutManager(this)
        val employeeAdapter = EmployeeAdapter(employeeObservableList)
        employeeAdapter.notifyDataSetChanged()
        employeeObservableList.addOnListChangedCallback(
            DynamicDataChangeCallback<ObservableArrayList<EmployeeBean>>(
                employeeAdapter
            )
        )
        recyclerView.adapter = employeeAdapter
    }

    fun addItem(view: View) {
        if (employeeObservableList.size >= 1) {
            val employeeBean = EmployeeBean(
                "${Random().nextInt()}",
                "张三${Random().nextInt(100)}",
                "${Random().nextInt(1000)}@163.com}"
            )
            employeeObservableList.add(0, employeeBean)
        }
    }

    fun addItemList(view: View) {
        if (employeeObservableList.size >= 2) {
            val employeeBeans: MutableList<EmployeeBean> = ArrayList<EmployeeBean>()
            for (i in 0..4) {
                val employeeBean = EmployeeBean(
                    "${Random().nextInt()}",
                    "张三${Random().nextInt(100)}",
                    "${Random().nextInt(1000)}@163.com}"
                )
                employeeBeans.add(employeeBean)
            }
            employeeObservableList.addAll(0, employeeBeans)
        }
    }

    fun removeItem(view: View) {
        if (employeeObservableList.size >= 2) {
            employeeObservableList.removeAt(1)
        }
    }

    fun updateItem(view: View) {
        if (employeeObservableList.size >= 2) {
            val employeeBean: EmployeeBean = employeeObservableList[1]
            employeeBean.name = "张三是大爷"
            employeeObservableList[1] = employeeBean
        }
    }
}

可以看到在 item_view.xml中绑定了数据,adapter要整洁很多。

其中 ObservableList中有 OnListChangedCallback对数据的List数据增删的监听回调。这样可以ObservableArrayList数据改变时,能直接修改 界面

public interface ObservableList<T> extends List<T> {

    void addOnListChangedCallback(OnListChangedCallback<? extends ObservableList<T>> callback);

    void removeOnListChangedCallback(OnListChangedCallback<? extends ObservableList<T>> callback);

    abstract class OnListChangedCallback<T extends ObservableList> {

        public abstract void onChanged(T sender);

        public abstract void onItemRangeChanged(T sender, int positionStart, int itemCount);

        public abstract void onItemRangeInserted(T sender, int positionStart, int itemCount);

        public abstract void onItemRangeMoved(T sender, int fromPosition, int toPosition,
                int itemCount);

        public abstract void onItemRangeRemoved(T sender, int positionStart, int itemCount);
    }
}

总结

上面大概总结了现阶段 ViewBinding和DataBinding的使用方法,注意事项。系统提供的修改方法基本够用,其他的我们可以自己通过 @BindingMethods 方法去自定义属性,做操作,比如:可以通过 @BindingMethods 自定OnClick实现防双击等等

参考文章

Android DataBinding 从入门到进阶