天天看点

学习笔记:DataBinding(二): 数据绑定

本节学习使用可观察的数据对象进行单向和双向数据绑定

使用DataBinding,我们可以使用之前我们已知的原始的基本类型、引用类型数据,但这些数据的改变不会使UI自动更新。

DataBinding为我们提供了数据驱动视图的可观察数据对象: objects(对象), fields(字段), collections(集合)。使用它们绑定UI,当这些对象的属性发生改变,UI会自动更新。

Observable fields

DataBinding为我们包装好的基本类型的可观察字段:

  • ObservableBoolean

  • ObservableByte

  • ObservableChar

  • ObservableShort

  • ObservableInt

  • ObservableLong

  • ObservableFloat

  • ObservableDouble

  • ObservableParcelable

我们自己也可通过ObservableField 泛型来申明其他类型:

class User {
	// 普通类型
    var firstName: String? = null 
    // 使用ObservableField自己封装
    var lastName: ObservableField<String>? = null
    // 使用官方提供已包装的
    var age: ObservableInt? = null
}
           
class MyHandler(private val mContext: Context) {
    // 伴生对象,相当于Java中的静态成员
    companion object {

        private val TAG = "MyHandler"
    }

    fun onClick(user: User) {
        Toast.makeText(mContext, "点击了:" + user.firstName, Toast.LENGTH_SHORT).show()

        user.firstName = "你好"
        // 注意调用,不是= ,而是相当于Java中断getLastName().set(String)
        // 直接=,相当于setLastName(),不会自动刷新的
        user.lastName!!.set("DataBinding")
        // !!非空断言运算符将任何值转换为非空类型,若该值为空则抛出异常,因为user.age是可空对象,所以需要使用
        user.age!!.set(2)
    }

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

        <import type="com.example.databindingsample.User" />

        <variable
            name="user"
            type="User" />

        <variable
            name="myHandler"
            type="com.example.databindingsample.MyHandler" />
    </data>

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

        <TextView
            android:id="@+id/tv_name"
            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:layout_marginTop="10dp"
            android:text="@{user.lastName}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{String.valueOf(user.age)}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> myHandler.onClick(user)}"
            android:text="属性更改" />

    </LinearLayout>

</layout>
           
class MainActivity : AppCompatActivity() {

    private var binding: ActivityMainBinding? = null

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

        val user = User()
        user.firstName = "helllo"
        user.lastName = ObservableField("databinding")
        user.age = ObservableInt(1)
        binding!!.user = user

        binding!!.myHandler = MyHandler(this)
    }
}
           

效果图:

学习笔记:DataBinding(二): 数据绑定

可以看到,我们使用ObservableField和ObservableInt的字段属性值改变时,视图也随之改变了,而原始的数据类型是属性值改变是不会引起视图更新的。

Observable collections

  • ObservableArrayMap

  • ObservableArrayList

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

        <import type="com.example.databindingsample.User" />

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

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

        <variable
            name="myHandler"
            type="com.example.databindingsample.MyHandler" />
    </data>

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

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{map[`first`]}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text='@{map["second"]}' />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{list[0]}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> myHandler.onClick(map,list)}"
            android:text="属性更改" />

    </LinearLayout>

</layout>
           
class MyHandler(private val mContext: Context) {
    companion object {

        private val TAG = "MyHandler"
    }

    fun onClick(map: ObservableMap<String,String>,list: ObservableList<String>) {
        Toast.makeText(mContext,"点击了",Toast.LENGTH_SHORT).show()

        map.put("first","android")
        map.put("second","iOS")

        list[0] = "GO"
    }

}
           
class MainActivity : AppCompatActivity() {

    private var binding: ActivityMainBinding? = null

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

        val observableMap = ObservableArrayMap<String, String>().apply {
            put("first","kotlin")
            put("second","java")
        }

        val observableList = ObservableArrayList<String>().apply {
            add("kotlin")
            add("java")
        }
        
        binding!!.map = observableMap
        binding!!.list = observableList

        binding!!.myHandler = MyHandler(this)
    }
}
           

效果如下:

学习笔记:DataBinding(二): 数据绑定

Observable objects

DataBinding给我们提供了Observable,让类继承Observable后,当类对象属性变化时通过

notifyPropertyChanged

notifyChange

来通知UI刷新。

class User : BaseObservable() {

    // 将@Bindable注解用于属性getter
    // @Bindable注解应该应用于一个可观察类的任何getter访问方法。Bindable将在BR类中生成一个字段来标识该字段,用于当该属性发生改变时
    @get:Bindable
    var firstName: String = ""
        set(value) {
            field = value
            // 只更新本字段
//            notifyPropertyChanged(BR.firstName)

            // 更新所有字段
            notifyChange()
        }

    @get:Bindable
    var lastName: String = ""
        set(value) {
            field = value
//            notifyPropertyChanged(BR.lastName)
        }

    @get:Bindable
    var age: Int = 0
        set(value) {
            field = value
//            notifyPropertyChanged(BR.age)
        }
}
           
<?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>

        <import type="com.example.databindingsample.User" />

        <variable
            name="user"
            type="User" />

        <variable
            name="myHandler"
            type="com.example.databindingsample.MyHandler" />
    </data>

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

        <TextView
            android:id="@+id/tv_name"
            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:layout_marginTop="10dp"
            android:text="@{user.lastName}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@{String.valueOf(user.age)}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> myHandler.onClick(user)}"
            android:text="属性更改" />

    </LinearLayout>

</layout>
           
class MainActivity : AppCompatActivity() {

    private var binding: ActivityMainBinding? = null

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

        binding!!.user = User().apply {
            firstName = "1"
            lastName = "2"
            age = 3
        }

        binding!!.myHandler = MyHandler(this)
    }
}
           
class MyHandler(private val mContext: Context) {
    companion object {

        private val TAG = "MyHandler"
    }

    fun onClick(user: User) {
        Toast.makeText(mContext,"点击了",Toast.LENGTH_SHORT).show()

        user.apply {
            firstName = "4"
            lastName = "5"
            age = 6
        }

    }
}
           

效果:

学习笔记:DataBinding(二): 数据绑定

注意:

  • 在Kotlin中使用注解,需要在app build.gradle中添加Kotlin注解插件:

    apply plugin: 'kotlin-kapt'

    , 否则会编译失败:compileDebugKotlin FAILED
  • BR类似于R文件,DataBinding自动生成的一个资源ID文件;另外这个id值并不固定,每次build可能都不一样

Two-way data binding

以上我们学习的DataBinding使用,都是使用的单向绑定,也就是数据驱动视图,下面我们来学习,数据与视图双向驱动,数据改变可以使UI刷新,UI改变也可以让数据更新,也就是DataBinding的双向数据绑定。

先看下面这样一个小栗子:

Model类和UI中的Checkbox,我们希望Checkbox的选中状态可以根据Model的isChecked属性刷新,同时Checkbox选中状态主动改变时,也更新Model的isChecked属性

学习笔记:DataBinding(二): 数据绑定

我尝试用之前单向绑定的方式去做了一下,如下代码:

class Model : BaseObservable(){

    @get:Bindable
    var isChecked: Boolean = false
        set(value) {
            field = value
            notifyChange()
        }
}
           
class MyHandler(private val mContext: Context) {

    fun onCheckedChanged(isChecked: Boolean,model: Model){

        if(model.isChecked != isChecked){
            model.isChecked = isChecked
            Toast.makeText(mContext,"视图驱动数据:" + model.isChecked,Toast.LENGTH_SHORT).show()
        }
    }

    fun onClick(model: Model){
        model.isChecked = !model.isChecked

        Toast.makeText(mContext,"数据驱动视图:" + model.isChecked,Toast.LENGTH_SHORT).show()
    }

}
           
<?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 class="MainActivityBinding">

        <variable
            name="model"
            type="com.example.databindingsample.Model" />

        <variable
            name="myHandler"
            type="com.example.databindingsample.MyHandler" />

    </data>

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

        <CheckBox
            android:id="@+id/cb"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:checked="@{model.isChecked}"
            android:onCheckedChanged="@{(buttonView,isChecked) -> myHandler.onCheckedChanged(isChecked,model)}"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:text="Model更改属性值"
            android:onClick="@{() -> myHandler.onClick(model)}"/>

    </LinearLayout>

</layout>
           
class MainActivity : AppCompatActivity() {

    private var binding: MainActivityBinding? = null

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

        binding!!.model = Model()

        binding!!.myHandler = MyHandler(this)

    }
}
           

之所以采用数据类继承BaseObservable方式实现,是因为我在尝试使用ObservableBoolean如下方式时,却出现了让我费解的情况,代码如下:

class Model{
    var isChecked: ObservableBoolean = ObservableBoolean(false)
}
           
class MyHandler(private val mContext: Context) {

    fun onCheckedChanged(isChecked: Boolean,model: Model){

        if(model.isChecked.get() != isChecked){
            // 关键问题在这行代码
            model.isChecked = ObservableBoolean(isChecked)
            Toast.makeText(mContext,"视图驱动数据:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
        }
    }

    fun onClick(model: Model){
        model.isChecked.set(!model.isChecked.get())

        Toast.makeText(mContext,"数据驱动视图:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
    }

}
           

就这样其他代码不变,我的本意是在视图驱动视图时,更新Model的isChecked属性通过model.isChecked = ObservableBoolean(isChecked) ,使用 = 直接赋值,不调用set()刷新了,就出现了如下的情况,单独点击CheckBox状态更新没有问题,单独点击Button更新CheckBox也没问题,但是只要点击了CheckBox再去点击Button就无法再刷新视图了,如下效果图:

学习笔记:DataBinding(二): 数据绑定

model.isChecked = ObservableBoolean(isChecked) 替换成 model.isChecked.set(isChecked) 就正常了

这个问题我搞不懂,知道原因的还请为我解惑,先行谢过了!

进入正题,下面来看这个让我浪费很长时间的问题,DataBinding怎么使用双向绑定来搞定的,更改代码如下:

<CheckBox
    android:id="@+id/cb"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:checked="@={model.isChecked}" 
    android:onCheckedChanged="@{(buttonView,isChecked) -> myHandler.onCheckedChanged(isChecked,model)}"/>
           
class MyHandler(private val mContext: Context) {

    // 这个监听只为输出属性值
    fun onCheckedChanged(model: Model){
        Toast.makeText(mContext,"视图驱动数据:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
    }

    fun onClick(model: Model){
        model.isChecked.set(!model.isChecked.get())

        Toast.makeText(mContext,"数据驱动视图:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
    }

}
           

效果如下:

学习笔记:DataBinding(二): 数据绑定

如上代码MainActivity代码不变,使用

@={}

符号即可完成双向绑定,就代替了我之前的onCheckedChanged监听,和其中的一些处理,如果不是为了Toast属性值,xml中的onCheckedChanged是不用设置的

细心的话会发现,点击CheckBox时输出的Model.isChecked是反的,其实没问题,是因为CheckBox的onCheckedChanged回调在Model.isChecked属性改变之前,拿到的还是之前的值

继续阅读