天天看点

Android DataBinding双向绑定使用

  Android双向绑定的好处就不再介绍了,直接上使用方法的代码。以下展示Demo1-3,使用方式从简到繁。

双向绑定使用Demo1:简单双向绑定

  我们使用ObservableField与EditView展示双向绑定。使用方式如下,将此ObservableField<String>与一个EditView进行双向绑定(使用@={}),当视图EditView中的文字变化时,ObservableField中数据也会随着变化。ObservableField中的数据变化,也会造成EditView中文字的变化。在此,ObservableField中的数据与EditView中的文字是相同的,不好展示ObservableField中发生的变化,所以,我们使用一个额外的TextView展示ObservableField中发生变化的数据

  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"
    xmlns:MyView="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="userVM"
            type="com.example.jetpacklearn.viewModel.UserViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <EditText
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:id="@+id/userName"
            android:text="@={userVM.name}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/showName"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/showName"
            android:text="@{userVM.name}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toEndOf="@+id/userName"
            app:layout_constraintHorizontal_chainStyle="spread"
            app:layout_constraintEnd_toEndOf="parent"/>


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

  在这里,使用@={}进行双向绑定,即EditView中文字的变化会同步到数据源userVM.name,userVM.name中数据的变化会同步到EditView。以下为UserViewModel的代码:

class UserViewModel:ViewModel() {
    var name:ObservableField<String> = ObservableField("")
    var age:ObservableInt = ObservableInt(10)
}
           

Activity的代码如下:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        var layoutInflater = LayoutInflater.from(this)
        var dataBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(dataBinding.root)

        var userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        dataBinding.userVM = userViewModel
    }
}
           

运行结果如图:

Android DataBinding双向绑定使用

双向绑定使用Demo2:自定义数据结构的双向绑定

  在实际中,我们使用的可能是自定义的数据,而非基本类型数据,这样就不能直接使用ObservableInt等基本数据类。因此,在自定义数据类时,需要使数据类继承BaseObservable,以便在数据变化时通知观察者。下面以一个Person数据类进行展示,首先,定义一个Person的data类。然后,定义一个PersonViewModel,其内部含有Person对象,并继承BaseObservable,以便在数据变化时,可以发送通知。

Person数据类

data class Persion(var name:String,var age:Int) {
}
           

PersonViewModel类

class PersionViewModel : BaseObservable(){
    var persion = Persion("Jack",19)

    @Bindable
    fun getPersionName():String{
        return persion.name
    }

    fun setPersionName(name:String){
        if(name!=persion.name){
            //避免设置数据时的无限循环
            persion.name = name
            //发送数据变化通知
            notifyPropertyChanged(BR.persionName)
        }
    }

    @Bindable
    fun getPersionAge():Int{
        return persion.age
    }

    fun setPersionAge(age:Int){
        if(age!=persion.age){
            persion.age = age
            notifyPropertyChanged(BR.persionAge)
        }
    }
}
           

  在这个类中,要使用@Bindable注解get方法,并且要在set方法中调用notifyPropertyChanged,以在数据变化时,通知观察者。BR.persionName是编译之后生成的。@Bindable的作用是告诉数据绑定系统如何读取PersonViewModel中的数据。PersonViewModel的使用代码如下:

<?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"
    xmlns:MyView="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="userVM"
            type="com.example.jetpacklearn.viewModel.UserViewModel" />

        <variable
            name="persionVM"
            type="com.example.jetpacklearn.viewModel.PersionViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <EditText
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:id="@+id/userName"
            android:text="@={userVM.name}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/showName"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/showName"
            android:text="@{userVM.name}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toEndOf="@+id/userName"
            app:layout_constraintHorizontal_chainStyle="spread"
            app:layout_constraintEnd_toEndOf="parent"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/persionLinearLayout"
            android:orientation="vertical"
            app:layout_constraintTop_toBottomOf="@+id/userName"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

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

                <EditText
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/persionName"
                    android:text="@={persionVM.persionName}"/>

            </LinearLayout>

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

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/showPersionName"
                    android:text="@{persionVM.persionName}"/>
            </LinearLayout>

        </LinearLayout>
</layout>
           

  Activity类如下:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        var layoutInflater = LayoutInflater.from(this)
        var dataBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(dataBinding.root)

        var userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        dataBinding.userVM = userViewModel

        var persionVM = PersionViewModel()
        dataBinding.persionVM = persionVM
    }
}
           

  运行结果如下:

Android DataBinding双向绑定使用

双向绑定Demo3:自定义视图的双向绑定

  在实际中,可能需要自定义View,这时,可能想对自定义View中的某个属性进行双向绑定。例如,定义一个记录点击次数的MyTextView,此视图中包含一个用来记录点击次数的变量。视图代码如下:

  MyTextView代码

class MyTextView: androidx.appcompat.widget.AppCompatTextView {

    //记录点击次数
    private var clickTime:Int = 0

    constructor(context:Context):super(context){}

    constructor(context: Context,attributeSet: AttributeSet):super(context,attributeSet){}

    constructor(context: Context,attributeSet: AttributeSet,defStyleAttr:Int):super(context,attributeSet,defStyleAttr){}

    fun setClickTime(newValue:Int){
        if(clickTime!=newValue){
            clickTime = newValue
        }
    }

    fun getClickTime():Int{
        return clickTime
    }
}
           

  首先,需要设置clickTime属性的get和set方法,因为在双向绑定中既要读数据,又要写数据。然后,使用注解告诉数据绑定系统如何从MyTextView中读写数据。代码如下:

//告知数据绑定系统如何写clickTime属性
@BindingMethods(value = [BindingMethod(type = MyTextView::class, attribute = "clickTime",method = "setClickTime") ])
//告知数据绑定系统调用哪个方法读取clickTime属性。此注解需要一个method,默认是get+属性名的方法,在此使用默认值
//此注解需要一个event,默认为属性名+AttrChanged,在此使用默认值
@InverseBindingMethods( value = [ InverseBindingMethod(type = MyTextView::class,attribute = "clickTime") ] )
class MyViewBindingAdapter {
}

//BindingAdapter、InverseBindingAdapter与上面的BindingMethods、InverseBindingMethods作用相同,
//都是告知数据绑定系统如何读写MyTextView中的clickTime,所以,只需设置一组就好

//@BindingAdapter("clickTime")
//fun setClickTime(myTextView: MyTextView,clickTime:Int){
//    myTextView.setClickTime(clickTime)
//}
//@InverseBindingAdapter(attribute = "clickTime",event = "clickTimeAttrChanged")
//fun getClickTime(myTextView: MyTextView):Int{
//    return myTextView.getClickTime()
//}

//通过此注解,来为属性添加一个监听器listener,监听属性的变化,此listener是编译器自动生成的。
//总的来说,最终还是要通过一些onClick、onTouch、TextWatcher等监听方法来监听自定义属性的变化。
@BindingAdapter("app:clickTimeAttrChanged")
fun setClickTimeAttrChanged(myTextView: MyTextView, listener: InverseBindingListener){
    //监听属性clickTime的变化
    myTextView.setOnClickListener {
        var clickTime = myTextView.getClickTime() + 1
        myTextView.setClickTime(clickTime)
        //使用listener发送属性变化通知
        listener.onChange()
    }
}
           

  为了在xml文件中使用clickTime属性,需要先在values文件夹下建立一个styleable文件,文件如下:

<resources>
    <declare-styleable name="MyTextView" >
        <attr name="clickTime" format="integer"></attr>
    </declare-styleable>
</resources>
           

  这样就可以在xml文件中对MyTextView的clickTime进行双向绑定了,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"
    xmlns:MyView="http://schemas.android.com/apk/res-auto">

    <data>
        <import type="com.example.jetpacklearn.BindingKt"/>
        <variable
            name="userVM"
            type="com.example.jetpacklearn.viewModel.UserViewModel" />

        <variable
            name="persionVM"
            type="com.example.jetpacklearn.viewModel.PersionViewModel" />

        <variable
            name="clickNum"
            type="androidx.databinding.ObservableInt" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <EditText
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:id="@+id/userName"
            android:text="@={userVM.name}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/showName"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/showName"
            android:text="@{userVM.name}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toEndOf="@+id/userName"
            app:layout_constraintHorizontal_chainStyle="spread"
            app:layout_constraintEnd_toEndOf="parent"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/persionLinearLayout"
            android:orientation="vertical"
            app:layout_constraintTop_toBottomOf="@+id/userName"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

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

                <EditText
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/persionName"
                    android:text="@={persionVM.persionName}"/>

            </LinearLayout>

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

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/showPersionName"
                    android:text="@{persionVM.persionName}"/>
            </LinearLayout>

        </LinearLayout>

        <com.example.jetpacklearn.view.MyTextView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:id="@+id/myTextView"
            android:text="@{BindingKt.intToString(clickNum)}"
            android:gravity="center"
            MyView:clickTime = "@={clickNum}"
            app:layout_constraintTop_toBottomOf="@+id/persionLinearLayout"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
           

  使用MyView:clickTime="@={clickNum}",进行双向绑定,并使用text="@{BindingKt.intToString(clickNum)}"来展示点击的次数。

  Activity类如下:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        var layoutInflater = LayoutInflater.from(this)
        var dataBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(dataBinding.root)

        var userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        dataBinding.userVM = userViewModel

        var persionVM = PersionViewModel()
        dataBinding.persionVM = persionVM

        //设置MyTextView的clickTime的数据源
        var clickNum = ObservableInt(0)
        dataBinding.clickNum = clickNum
    }
}
           

  这样,当我们点击MyTextView时,通过之前设置的点击监听器,使clickTime加一。然后,发出属性变化通知,使clickNum(ObservableInt)的数据也加一。最后,使用android:text属性,展示clickNum中的数据。

  运行结果如下:

Android DataBinding双向绑定使用

  总结,在上面,分别展示了简单的数据双向绑定、自定义数据结构的双向绑定、自定义视图属性的双向绑定。

  相关博客:

DataBinding双向绑定注解

DataBinding双向绑定原理