Android JetPack之DataBinding

作者: 皮球二二 | 来源:发表于2018-07-09 17:26 被阅读154次

    谷歌在今年的I/O大会上推出了Jetpack的概念,意图在于统一框架与UI组件。所以我也将项目架构往这一概念上靠齐。

    Jetpack
    那我们就一个个的来研究,先从最基本的DataBinding开始
    本文涉及到的代码已上传到Github,欢迎star、fork

    基本配置

    DataBinding的配置很简单,只需在相应模块的build.gradle中配置一下即可

    dataBinding {
        enabled = true
    }
    

    记住这个配置在任何你需要使用DataBinding的模块中都要添加,不然会提示你找不到DataBinding相应的类

    如果你不使用Kotlin语言进行开发,可能这么做就够了,但是遇到Kotlin,各种坑就来了。这里就不花时间谈这些了,你就在build.gradle中添加kapt就没这些事了

    apply plugin: 'kotlin-kapt'
    
    kapt {
        generateStubs = true
    }
    

    还有就是在当前测试版as(Android Studio 3.2 Beta 2)中,默认使用androidx包来替代support包,但是这个做法有坑,不推荐使用。倒不是我怕麻烦不想解决,怎么说这玩意都没有发布正式版,各种离奇的问题多的是,你今天改好了明天可能又有新的问题出现,印象最深的就是as Beta1版本中Kotlin1.2.50和DataBinding不兼容,呵呵呵了

    简单示例

    Kotlin的数据类一般都以data class来标记,它不影响DataBinding的使用。你可以反编译字节码看看,跟Java里的类几乎都是一样的

    data class Teacher(var name: String, var age: Int)
    

    一般情况下xml布局文件是这样的

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"/>
    </LinearLayout>
    

    但是如果使用DataBinding,布局文件就需要稍加改造

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable
                name="teacher"
                type="com.example.administrator.databindingdemo2.model.Teacher"></variable>
        </data>
        <LinearLayout
            android:orientation="vertical" android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="@{teacher.name}"
                android:gravity="center"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="@{String.valueOf(teacher.age)}"
                android:gravity="center"/>
        </LinearLayout>
    </layout>
    

    最外层根布局为layout标签。layout标签里有2个节点,分别为data标签和我们之前的布局。data标签下的 variable标签是用来定义数据绑定所用的实体类,其中type是完整的带包名的类,name是给type所代表的类自定义的一个名称,在布局文件中如果使用该类的属性以及方法时需要使用这个名称

    我们一般使用@{}将Model绑定到View上。这里@{teacher.name}是将teacher中的name属性绑定到第一个TextView上;@{String.valueOf(teacher.age)}是将teacherage属性绑定到第二个TextView上。由于ageInt类型,所以需要把它转化成String类型才可以设置到TextView的text

    通过DataBinding处理过的Layout文件会自动生成一个数据绑定类,就是这个类完成Model与View绑定工作的。这个数据绑定类的默认名称与该Layout文件的命名格式有关,比如activity_basesample.xml所生成的数据绑定类为ActivityBasesampleBinding

    public abstract class ActivityBasesampleBinding extends ViewDataBinding
    

    来看看activity是如何得到这个关联关系的

    class BaseSampleActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val viewDataBinding = DataBindingUtil.setContentView<ActivityBasesampleBinding>(this, R.layout.activity_basesample)
            viewDataBinding.teacher = Teacher("renyu", 30)
        }
    }
    

    效果其实没什么可说的,很简单

    DataBinding基本使用

    Fragment中的写法与Activity稍有区别,用的不是setContentView而是inflate,通过viewDataBinding.root来得到绑定后的视图

    class BaseSampleFragment : Fragment() {
        override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
            val viewDataBinding = DataBindingUtil.inflate<ActivityBasesampleBinding>(inflater, R.layout.activity_basesample, container, false)
            viewDataBinding.teacher = Teacher("renyu", 31)
            return viewDataBinding.root
        }
    }
    

    额外的知识点

    import

    importvariable中的type在功能上有点接近,都是用来声明类的。一般情况下,我们必须要对所有使用到的类进行声明,就像刚才Model类一样。例如我们想控制View的显示和隐藏,要先声明android.view.View类,只要这样import就行

    <data>
        <import type="android.view.View"></import>
        .......
    </data>
    

    后面就可以直接使用了

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:visibility="@{teacher.age&lt;30 ? View.VISIBLE : View.GONE}"
        android:text="@{String.valueOf(teacher.age)}"
        android:gravity="center"/>
    

    特别需要注意的是这里的&lt;:由于xml没有使用转义,部分字符需要转义才能正常通过编译,所以才将<写成&lt;。而我们想表达的意思本来应该是这样的:

    android:visibility="@{teacher.age<30 ? View.VISIBLE : View.GONE}"
    

    更多涉及到的此部分的内容可以参考HTML转义字符

    variable也可以直接使用import好的类型可。将之前的声明部分改造一下

    <data>
        <import type="com.example.administrator.databindingdemo2.model.Teacher"></import>
        <variable
            name="teacher"
            type="Teacher">
    
        </variable>
    </data>
    

    回忆一下刚才我们在将Int转化为String的时候,直接用了String.valueOf将数字转成String。读者们肯定会问,为什么这里可以直接使用String对象而不用来声明?原来只有java.lang之外的类才必须要在data标签下使用import进行导入。有点尴尬。。。

    include

    variable也可以将对象传到include的Layout文件中继续使用

    来看看被include的view

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android" >
        <data>
            <variable
                name="student"
                type="com.example.administrator.databindingdemo2.model.Student"></variable>
        </data>
        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="@{student.name}"/>
        </android.support.constraint.ConstraintLayout>
    </layout>
    

    使用的时候直接把声明好的student传进去即可

    <include
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        layout="@layout/view_basesample"
        bind:student="@{student}">
    
    </include>
    
    空安全

    DataBinding是空安全的,如果之前的android:text="@{student.name}"为null也不会造成空指针异常

    表达式

    DataBinding有几种常见的表达式,除了刚才提到的三目运算符来控制View的显示和隐藏以外,再来看看其他的

    1.??操作符

    我们来改造一下之前被include的视图

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="@{student.name ?? String.valueOf(student.age)}"/>
    

    什么意思呢?只要左边的student.name值不为null,就使用其值,反之则使用右边String.valueOf(student.age)的值
    这段代码相当于android:text="@{student.name!=null ? student.name : String.valueOf(student.age)}"

    1. 集合的使用

    这里演示了如何将map中的数据通过静态方法转换后绑定到View上。这里不管是List还是Map,都是使用[]来填写索引或者是键

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:text="@{String.valueOf(courses.course[teacher.name].age)}"
        android:gravity="center"/>
    
    1. 本地资源的引用

    本地资源可以直接使用

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:src="@{courses.course[teacher.name].age&gt;10 ? @drawable/ic_launcher : @drawable/ic_launcher_round}"/>
    

    比较尴尬的是我发现mipmap类型不能使用,也不知道为什么

    另外格式化的字符串也可以使用,比如下面的string

    <string name="teacher_age">老师的年龄是%s</string>
    

    可以使用String.format传入参数

    android:text="@{String.format(@string/teacher_age, teacher.age)}"
    

    或者这样

    android:text="@{@string/teacher_age(teacher.age)}"
    

    或者不像样

    app:textshow="@{`点击`+@string/app_name}"
    
    1. 给属性添加变化监听

    当Model的值发生变化时,我们可以通过addOnPropertyChangedCallback捕捉到属性的改变

    viewDataBinding.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
        override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
            Log.d(this@BaseSampleFragment::class.simpleName, "$propertyId")
        }
    })
    

    当绑定发生改变的时候,可以使用addOnRebindCallback监听绑定周期的变化

    viewDataBinding.addOnRebindCallback(object : OnRebindCallback<ActivityBasesampleBinding>() {
        override fun onBound(binding: ActivityBasesampleBinding?) {
            super.onBound(binding)
            Log.d(this@BaseSampleFragment::class.simpleName, "onBound")
        }
    
        override fun onCanceled(binding: ActivityBasesampleBinding?) {
            super.onCanceled(binding)
            Log.d(this@BaseSampleFragment::class.simpleName, "onCanceled")
        }
    
        override fun onPreBind(binding: ActivityBasesampleBinding?): Boolean {
            Log.d(this@BaseSampleFragment::class.simpleName, "onPreBind")
            return super.onPreBind(binding)
        }
    })
    

    事件处理

    事件处理也算表达式使用的其中一个环节,这里单独拿出来说是为了给读者加深印象,因为这个知识点使用相当频繁。
    我们一般通过方法引用或者接口绑定两种途径来实现
    先来看看方法引用,两个方法,参数不同

    class MyHandlers {
        fun onClick(view: View) {
            Toast.makeText(view.context, "MyHandlers onClick", Toast.LENGTH_SHORT).show()
        }
    
        fun onClick3(view: View, str: String) {
            Toast.makeText(view.context, str, Toast.LENGTH_SHORT).show()
        }
    }
    

    当只有一个参数View的时候,可以这样实现

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:onClick="@{handlers::onClick}"
        android:gravity="center"
        android:text="点击1"/>
    

    也可以通过lambda的形式,遵循原方法的签名实现

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:onClick="@{(view)->handlers.onClick(view)}"
        android:gravity="center"
        android:text="点击2"/>
    

    再多的参数也一样

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:onClick="@{(view)->handlers.onClick3(view, String.valueOf(`234`))}"
        android:gravity="center"
        android:text="点击3"/>
    

    我个人比较倾向于接口实现的方式,因为这样各个页面就可以分别实现自己定制的功能,更灵活一些

    定义一个接口即可

    interface ClickEventImpl {
        fun clickEvent(view: View, string: String)
    }
    

    xml布局中的写法与之前相比没两样

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:onClick="@{(view)->impl.clickEvent(view, String.valueOf(`234`))}"
        android:gravity="center"
        android:text="点击4"/>
    

    在使用的时候千万不要忘记给这个接口绑定进行赋值,我就掉这个坑里面去过了。。。

    class EventSampleActivity : AppCompatActivity(), ClickEventImpl {
        override fun clickEvent(view: View, string: String) {
            Toast.makeText(view.context, "EventSampleActivity : $string", Toast.LENGTH_SHORT).show()
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val viewDataBinding = DataBindingUtil.setContentView<ActivityEventsampleBinding>(this, R.layout.activity_eventsample)
            viewDataBinding.handlers = MyHandlers()
            viewDataBinding.impl = this
        }
    }
    
    点击事件效果

    观察者

    通常情况下,Model中的数据会随时发生改变,比如页面在初始化的时候数据从本地缓存中加载,随后通过网络请求刷新内容。既然数据绑定到View上了,那么我就希望View的状态应该随着Model的改变而改变。DataBinding是支持这一功能的,这种通过Model的刷新以达到View刷新的功能,叫单向绑定。
    我们来测试一下这个功能,5s之后我改变了teacherage的值

    Handler().postDelayed({
        viewDataBinding.teacher?.age = 31
    }, 5000)
    

    奇怪的是页面并没有发生什么变化,你是不是在骗我?没有,有一点我们不能忽略:必须要将相应的Model继承BaseObservable之后才可以实现单向绑定的功能。
    来看看代码如何调整

    class Teacher3 : BaseObservable() {
        var name: String? = null
            @Bindable
            get() = field
            set(value) {
                field = value
                notifyPropertyChanged(BR.name)
            }
        var age: Int? = null
            @Bindable
            get() = field
            set(value) {
                field = value
                notifyPropertyChanged(BR.age)
            }
    }
    

    在原有set/get的基础上,只是添加了@BindablenotifyPropertyChanged。在set方法中使用notifyPropertyChanged来通知View刷新,notifyPropertyChanged只会刷新具体的值,而notifyChange方法则会刷新所有的值。刷新所用的BR的域默认名称是该变量的名称,它是通过get方法上@Bindable注解生成的

    这样,来看看刷新效果

    BaseObserval

    继承自BaseObservable的做法有点重复劳动的感觉,因此DataBinding还提供了一种更为简单的写法——ObservableField。来看看它是如何使用的

    data class Teacher2(var name: ObservableField<String>?, var age: ObservableField<Int>?)
    

    View层的使用方式基本没有变化,改动的地方是赋值和获取值的时候要通过set/get去实现

    class BaseObservableSampleActivity : AppCompatActivity() {
    
        private val teacher2: Teacher2 by lazy {
            Teacher2(ObservableField("renyu"), ObservableField(30))
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            val viewDataBinding = DataBindingUtil.setContentView<ActivityBaseobservablesampleBinding>(this, R.layout.activity_baseobservablesample)
            viewDataBinding.teacher2 = teacher2
    
            Handler().postDelayed({
                teacher2.name?.set("PQ")
            }, 4000)
        }
    }
    

    除了ObservableField,还可以使用ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, ObservableParcelable。如果使用Map、List等保存数据,DataBinding也提供了ObservableArrayMapObservableArrayList

    自定义属性

    这是我觉得DataBinding提供的一个很强大的功能。
    想想我们使用自定义属性的场景:先通过自定义View来定义自定义属性,自定义属性值在TypedArray中进行获取,用这个值完成一系列功能;定义完成之后在布局文件中使用,例如使用Fresco的SimpleDraweeView时候,要分别添加图片加载成功、图片加载失败、图片加载中等不同状态下的属性。因此总的来说步骤还是很多的,但在DataBinding中自定义属性设计的就没那么复杂,你可以以最简单的方式来定义任何名称与类型的属性。

    我们来看两种使用场景

    1. 通过自定义View实现自定义属性

    在这个例子中,我在TextView中定义了toasttextshow两个属性,分别实现TextView初始化后显示Toast以及setText的功能。
    来看下代码的写法。自定义属性的信息在BindingMethod注解中,BindingMethod被包裹在BindingMethods里。进入BindingMethod注解,属性名称写在attribute里,属性对应实现的方法写在method里。这里我们定义的两个新属性所对应的方法分别是showshowText,我们只需要在类里面提供相应方法名的方法即可。自定义的属性在AppCompatTextView上添加,所以类型typeAppCompatTextView

    @BindingMethods(BindingMethod(type = AppCompatTextView::class, attribute = "toast", method = "showToast"),
            BindingMethod(type = AppCompatTextView::class, attribute = "textshow", method = "showText"))
    class CustomerTextView : AppCompatTextView {
        constructor(context: Context) : super(context)
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    
        fun showToast(value: String) {
            Toast.makeText(context, value, Toast.LENGTH_SHORT).show()
        }
    
        fun showText(value: String) {
            text = value
        }
    }
    

    在使用中没有什么特别需要注意的地方,直接把需要传入的值传进去即可

    <com.example.administrator.databindingdemo2.ui.view.CustomerTextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        app:textshow="@{`点击`}"
        app:toast="@{`CustomerTextView`}"/>
    
    1. 通过静态方法来实现自定义属性

    自定义View还是太麻烦,不可能每需要一个功能都自定义一下View,所以DataBinding提供BindingAdapter来方便我们通过静态方法来实现自定义属性
    来看看一个例子,任何一个TextView都可以使用自定义的text属性完成文本的设置

    class ViewUitls {
        companion object {
            @JvmStatic
            @BindingAdapter(value = "text")
            fun addLog(textView: TextView, string: String) {
                textView.text = "这是来自自定义的text:$string"
            }
        }
    }
    

    注意这里函数签名,第一个参数一定要是View的类型,你给哪个类型的View添加自定义属性就得写哪个类型,第二个参数是通过xml传入的值,同时标记方法必须为公共静态方法。使用的时候就像这样,我传入一个叫text的文本

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:text="@{`text`}"
        android:layout_weight="1"/>
    

    来看下效果


    自定义属性

    当然同一个方法可以同时设置多个属性。这里我模拟一个网络请求过程:初始化的时候TextView显示一个字符串,过3s之后TextView又显示另外一个字符串

    @JvmStatic
    @BindingAdapter(value = ["startText", "endText"])
    fun changeValue(textView: TextView, startText: String, endText: String) {
        textView.text = "这是来自自定义的text:$startText"
        Handler().postDelayed({
            textView.text = "这是来自自定义的text:$endText"
        }, 3000)
    }
    

    使用时候把两个值都定义一下

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:startText="@{`startText`}"
        app:endText="@{`endText`}"
        android:layout_weight="1"/>
    

    来看看效果


    多属性设置

    默认情况下多个属性需要全部实现,如果你不需要强制属性都实现的话只需设置requireAllfalse即可

    @BindingAdapter(value = ["startText", "endText"], requireAll = false)
    

    不仅仅对象可以作为自定义属性的值,接口同样也可以作为值进行添加,只不过传入的是用lambda表达式实现好的接口方法
    这里我包装一个点击事件,传入的是我实现的ClickEventImpl接口,并且用Toast显示inputValue的值

    @JvmStatic
    @BindingAdapter(value = ["clickEventImpl", "inputValue"], requireAll = false)
    fun setOnClickEventImpl(textView: TextView, clickEventImpl: ClickEventImpl, inputValue: Boolean) {
        textView.setOnClickListener {
            clickEventImpl.clickEvent(textView, "$inputValue")
        }
    }
    

    关键的就是注意xml布局里的lambda怎么写,只要遵照函数签名即可。避免使用复杂的表达式,逻辑尽量写到外部代码中

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:text="@{`接口`}"
        app:clickEventImpl="@{(view, string) -> handlers.onClick3(view, string)}"
        app:inputValue="@{dayNight.day}"
        android:layout_weight="1"/>
    

    除了自定义的属性外,原生属性也可以被替换。这里演示的是将android:background替换,本来这个属性传进去的值应该是Drawable类型,现在我直接用Boolean变量来作为属性参数了

    @JvmStatic
    @BindingAdapter(value = ["android:background"])
    fun changeSourceAttribute(textView: TextView, boolean: Boolean) {
        if (boolean) {
            textView.setBackgroundColor(Color.WHITE)
        }
        else {
            textView.setBackgroundColor(Color.BLACK)
        }
    }
    

    在使用的时候与之前也没区别

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:startText="@{`startText`}"
        app:endText="@{`endText`}"
        android:background="@{dayNight.day}"
        android:layout_weight="1"/>
    

    运行之后即可看出效果

    双向绑定

    之前我们介绍的都是单向绑定,早期的DataBinding也只支持以上介绍的功能,但是随着框架的日益完善,反向绑定也被添加进来。
    什么是反向绑定?举个例子就是你修改TextView的text文本后,DataBinding将这些文本送到Model去,Model的值发生变化,跟单向绑定流程正好相反。

    单向绑定与反向绑定合称双向绑定。双向绑定说起来容易做起来难,为此DataBinding提供了一系列以Inverse开头的注解来帮助开发者可以更好的控制和使用双向绑定

    双向绑定

    我们通过一个例子来体验一下什么是双向绑定。
    我将EditText与Model进行双向绑定,CheckBox、TextView与Model进行单向绑定。注意观察当EditText的Text值发生变化的时候,Model的值的变化情况

    先看看xml布局文件

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
        <data>
            <variable
                name="teacher2"
                type="com.example.administrator.databindingdemo2.model.Teacher2"></variable>
        </data>
        <LinearLayout
            android:orientation="vertical" android:layout_width="match_parent"
            android:layout_height="match_parent">
            <CheckBox
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:checked="@{teacher2.name.equals(``) ? true : false}"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dip"
                android:gravity="center"
                android:text="@{teacher2.name}"/>
            <EditText
                android:layout_width="match_parent"
                android:layout_height="50dip"
                android:text="@={teacher2.name}"/>
        </LinearLayout>
    </layout>
    

    先别在意xml里面没见过的内容,我们来直接看看结果

    双向绑定

    当我修改了EditText的值之后,Model值发生了改变,所以CheckBox与TextView的内容发生变化了。

    现在我们来分析一下xml里面的代码,主要看这部分@={teacher2.name}。这里的=就意味着EditText里的android:text属性开启了双向绑定。

    控件本身是不支持双向绑定的,官方提供了一部分双向绑定的功能,但是更多的地方需要我们自定义来完成。

    官方支持的双向绑定属性如下:

    • AbsListView android:selectedItemPosition
    • CalendarView android:date
    • CompoundButton android:checked
    • DatePicker android:year, android:month, android:day
    • NumberPicker android:value
    • RadioGroup android:checkedButton
    • RatingBar android:rating
    • SeekBar android:progress
    • TabHost android:currentTab
    • TextView android:text
    • TimePicker android:hour, android:minute

    那么如何来自定义呢?我们直接上代码解释

    @InverseBindingMethods(InverseBindingMethod(type = VisibleView4::class, attribute = "displayShow"))
    class VisibleView4 : View {
        constructor(context: Context) : super(context)
        constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
    
        var listener: OnChangeListener? = null
    
        var displayShow = false
    
        interface OnChangeListener {
            fun change()
        }
    
        override fun onVisibilityChanged(changedView: View?, visibility: Int) {
            super.onVisibilityChanged(changedView, visibility)
            listener?.change()
        }
    
        companion object {
            @JvmStatic
            @BindingAdapter("displayShow")
            fun changedisplayShow(view: VisibleView4, boolean: Boolean) {
                if (boolean) {
                    view.visibility = View.VISIBLE
                }
                else {
                    view.visibility = View.GONE
                }
            }
    
            @JvmStatic
            @BindingAdapter(value = ["displayShowAttrChanged"], requireAll = false)
            fun setListeners(view: VisibleView4, inverseBindingListener: InverseBindingListener) {
                view.listener = object : OnChangeListener {
                    override fun change() {
                        view.displayShow = view.visibility == View.VISIBLE
                        inverseBindingListener.onChange()
                    }
                }
            }
        }
    }
    

    不得不说我第一遍看这个代码的时候,整个人完全都不好了,因为我摸不出来双向绑定的流程到底是什么,相信读者们肯定也有这种体会。但是不要慌,听我分析一遍之后,大家应该会很清楚的明白它的流程了

    简单的说双向绑定的流程就是通过setter/getter来实现的。setter部分处理View的改变,getter部分得到刷新后的Model新数据。其中数据的刷新会通过专门的接口通知来完成

    粗的讲完再来说细的。先来看看类声明部分的注解。这个理解起来不难,告诉大家我在VisibleView4类里面定义了displayShow这个属性

    @InverseBindingMethods(InverseBindingMethod(type = VisibleView4::class, attribute = "displayShow"))
    

    @InverseBindingMethods没什么好说,其实就是@InverseBindingMethod的一个数组集合
    @InverseBindingMethod就厉害了,它是反向绑定方法,用来确定怎么去监听View状态的变化和回调哪一个getter方法使得Databinding获取新数据
    @InverseBindingMethod里面有4个属性:
    type:该attribute所属的View的类型
    attribute:支持双向绑定的属性
    event:可以省略,用来通知DataBinding当前attribute已经改变。不设定的话会自动寻找名称为attribute的值 + "AttrChanged"的方法
    method:可以省略,被通知当前attribute已经改变之后DataBinding获取新数据的入口,不设定的话会自动寻找名称为"is" 或 "get" + attribute的值的方法

    @Target(ElementType.ANNOTATION_TYPE)
    public @interface InverseBindingMethod {
        Class type();
        String attribute();
        String event() default "";
        String method() default "";
    }
    

    继续浏览companion object部分的代码

    @BindingAdapter("displayShow")这个没什么好说,单向绑定时候我们就讲过,声明一个自定义属性,它在双向绑定中相当于setter

    @BindingAdapter(value = ["displayShowAttrChanged"], requireAll = false)里面有一个InverseBindingListener。它是事件发生时触发的监听器。使用双向绑定时,它会在通过layout自动生成的Binding类中自动生成一个InverseBindingListener的实现。所有双向绑定最后都是通过这个接口来通知数据进行刷新的

    public interface InverseBindingListener {
        void onChange();
    }
    

    当View的visibility发生变化之后,就会修改displayShow的值,同时触发inverseBindingListener.onChange()inverseBindingListener.onChange()触发之后,就会通知Databinding获取刷新后的值

    不要忘记这里其实还有一个getter,它是InverseBindingAdapter,它是反向绑定适配器。

    @JvmStatic
    @InverseBindingAdapter(attribute = "displayShow")
    fun getVisib(view: VisibleView3) : Boolean {
        return view.displayShow
    }
    

    它只包含attributeevent两个属性,功能与@InverseBindingMethod一样,它的调用时机由inverseBindingListener所在的@BindingAdapter决定,所以@BindingAdaptervalue值与InverseBindingAdapterevent值必须一样,才能匹配上

    @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
    public @interface InverseBindingAdapter {
        String attribute();
        String event() default "";
    }
    

    之所以在这个地方可以忽略不写,是因为属性的名称与这个变量的名称相同。Kotlin里面属性的getter方法直接就会get + attribute,所以本身就存在getDisplayShow,就不需要我们再实现一遍了。
    如果变量的名称与属性的名称不一致,就会提示说找不到相应属性的getter方法

    [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
    ****/ data binding error ****msg:Cannot find the getter for attribute 'app:displayShow' with value type boolean on com.example.administrator.databindingdemo2.ui.view.VisibleView4.
    file:D:\workspace\android_demo\DataBindingDemo2\app\src\main\res\layout\activity_edittext.xml
    loc:27:8 - 31:46
    ****\ data binding error ****
    

    最后还有几个小点要注意一下:

    1. 如果你用了@InverseBindingAdapter,那完全可以不需要使用@InverseBindingMethods再次声明,比如这样
    companion object {
        @JvmStatic
        @BindingAdapter("displayShow")
        fun changedisplayShow(view: VisibleView3, boolean: Boolean) {
            
        }
    
        @JvmStatic
        @InverseBindingAdapter(attribute = "displayShow")
        fun getVisib(view: VisibleView3) : Boolean {
           
        }
    
        @JvmStatic
        @BindingAdapter(value = ["displayShowAttrChanged"], requireAll = false)
        fun setListeners(view: VisibleView3, inverseBindingListener: InverseBindingListener) {
            
        }
    }
    
    1. 注意attribute的对应以及@BindingAdaptervalue值与InverseBindingAdapterevent值的对应
    companion object {
        @JvmStatic
        @BindingAdapter("displayShow")
        fun changedisplayShow(view: VisibleView3, boolean: Boolean) {
            
        }
    
        @JvmStatic
        @InverseBindingAdapter(attribute = "displayShow", event = "displayShowAttrChanged_random")
        fun getVisib(view: VisibleView3) : Boolean {
           
        }
    
        @JvmStatic
        @BindingAdapter(value = ["displayShowAttrChanged_random"], requireAll = false)
        fun setListeners(view: VisibleView3, inverseBindingListener: InverseBindingListener) {
            
        }
    }
    

    以上流程如果你都理解了,那我就可以隆重的推出最简写法了

    @InverseBindingMethods(InverseBindingMethod(type = VisibleView2::class, attribute = "displayShow"))
    class VisibleView2 : View {
        constructor(context: Context) : super(context)
        constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
    
        var listener: OnChangeListener? = null
    
        var temp = false
    
        interface OnChangeListener {
            fun change()
        }
    
        override fun onVisibilityChanged(changedView: View?, visibility: Int) {
            super.onVisibilityChanged(changedView, visibility)
            listener?.change()
        }
    
        fun setDisplayShow(boolean: Boolean) {
            if (boolean) {
                this.visibility = VISIBLE
            }
            else {
                this.visibility = GONE
            }
        }
    
        fun getDisplayShow() : Boolean {
            return this.temp
        }
    
        fun setDisplayShowAttrChanged(inverseBindingListener: InverseBindingListener) {
            this.listener = object : OnChangeListener {
                override fun change() {
                    this@VisibleView2.temp = this@VisibleView2.visibility == View.VISIBLE
                    inverseBindingListener.onChange()
                }
            }
        }
    }
    

    这才是@InverseBindingMethods所应该出现在的最正确的地方,注意setter/getter的命名必须是set + attribute 和 get + attribute,InverseBindingListener所在的@BindingAdapter方法命名必须是set + event,由于event的命名规则,所以这里还可以写成set + attribute + “AttrChanged”

    1. 可能会出现死循环绑定。在setter的时候,要对新旧数据进行比较,如果View的状态与Model的值是匹配的,那就需要return,不能再继续设置

    这里我们总结一下双向绑定:

    • 只要自定义双向绑定,都必须要有@BindingAdapter注解的参与
    • @InverseBindingMethod与@InverseBindingMethods + @BindingAdapter可以实现双向绑定
    • @InverseBindingAdapter + @BindingAdapter也可以实现双向绑定

    实际场景的使用——RecyclerView

    用RecyclerView来演示DataBinding无疑是最合适的,它将向我们展示DataBinding是如何做到精简代码的。
    首先来看下adapter布局的代码,我在里面定义了一个实体类teacher与一个事件操作类handlers

    <layout  xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable
                name="teacher"
                type="com.example.administrator.databindingdemo2.model.Teacher2"></variable>
            <variable
                name="handlers"
                type="com.example.administrator.databindingdemo2.util.MyHandlers"></variable>
        </data>
        <LinearLayout
            android:orientation="horizontal" android:layout_width="match_parent"
            android:layout_height="50dip"
            android:onClick="@{(view) -> handlers.onClick3(view, teacher.name)}">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="@{teacher.name}"
                android:gravity="center"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="@{String.valueOf(teacher.age)}"
                android:gravity="center"/>
        </LinearLayout>
    </layout>
    

    来看看adapter的实现部分。你可以发现这里内容很少,这是因为这里只做了数据绑定而已,其他都交给各自的实现类了。注意View跟Model是通过DataBindingUtil.inflate方法来完成的,并且赋值的地方在onBindViewHolder里来完成

    class RecyclerViewAdapter(private val teachers: ArrayList<Teacher2>) : RecyclerView.Adapter<RecyclerViewAdapter.RecyclerViewHolder>() {
        override fun onCreateViewHolder(p0: ViewGroup, p1: Int): RecyclerViewHolder {
            val viewDataBinding = DataBindingUtil.inflate<AdapterRecyclerviewBinding>(LayoutInflater.from(p0.context), R.layout.adapter_recyclerview, p0, false)
            return RecyclerViewHolder(viewDataBinding)
        }
    
        override fun getItemCount() = teachers.size
    
        override fun onBindViewHolder(p0: RecyclerViewHolder, p1: Int) {
            p0.dataBinding?.setVariable(BR.teacher, teachers[p1])
            p0.dataBinding?.setVariable(BR.handlers, MyHandlers())
            p0.dataBinding?.executePendingBindings()
        }
    
        class RecyclerViewHolder(viewDataBinding: ViewDataBinding) : RecyclerView.ViewHolder(viewDataBinding.root) {
            var dataBinding: ViewDataBinding? = viewDataBinding
        }
    }
    

    注意executePendingBindings(),这是因为RecyclerView的特殊性。当数据改变时,DataBinding会在下一帧去改变数据,如果我们需要立即改变,就得去调用executePendingBindings()方法
    再来看看RecyclerView布局的代码。这里我定义了一个属性rvs

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
        <data>
            <variable
                name="adapter"
                type="com.example.administrator.databindingdemo2.ui.adapter.RecyclerViewAdapter"></variable>
        </data>
        <LinearLayout
            android:orientation="vertical" android:layout_width="match_parent"
            android:layout_height="match_parent">
            <android.support.v7.widget.RecyclerView
                android:id="@+id/rv"
                app:rvs="@{adapter}"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
            </android.support.v7.widget.RecyclerView>
        </LinearLayout>
    </layout>
    

    rvs属性所对应的setRecyclerViews方法只是一个通用RecyclerView属性设置的方法

    @JvmStatic
    @BindingAdapter(value = ["rvs"])
    fun <T : RecyclerView.ViewHolder> setRecyclerViews(recyclerView: RecyclerView, adapter: RecyclerView.Adapter<T>) {
        recyclerView.setHasFixedSize(true)
        recyclerView.layoutManager = LinearLayoutManager(recyclerView.context)
        recyclerView.adapter = adapter
    }
    

    这样在Activity中就可以这样使用了

    class RecyclerViewActivity : AppCompatActivity() {
    
        val teacher2: ArrayList<Teacher2> by lazy {
            ArrayList<Teacher2>()
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val viewDataBinding = DataBindingUtil.setContentView<ActivityRecyclerviewBinding>(this, R.layout.activity_recyclerview)
            viewDataBinding.adapter = RecyclerViewAdapter(teacher2)
    
            for (i in 0..30) {
                teacher2.add(Teacher2(ObservableField("Hello$i"), ObservableField(i)))
            }
            viewDataBinding.adapter?.notifyDataSetChanged()
        }
    }
    

    来看看效果


    RecyclerView

    东西有点多,不过都是一些基础,应该很好理解

    参考文章

    MVVM之DataBinding学习笔记
    DataBinding使用教程(四):BaseObservable与双向绑定

    相关文章

      网友评论

        本文标题:Android JetPack之DataBinding

        本文链接:https://www.haomeiwen.com/subject/xxikuftx.html