Android技术知识Android开发经验谈Android开发

Android JetPack之DataBinding

2018-07-09  本文已影响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属性开启了双向绑定。

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

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

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

@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,不能再继续设置

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

实际场景的使用——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与双向绑定

上一篇下一篇

猜你喜欢

热点阅读