Android JetPack之DataBinding
谷歌在今年的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)}
是将teacher
中age
属性绑定到第二个TextView上。由于age
是Int
类型,所以需要把它转化成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
import
与variable
中的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<30 ? View.VISIBLE : View.GONE}"
android:text="@{String.valueOf(teacher.age)}"
android:gravity="center"/>
特别需要注意的是这里的<
:由于xml没有使用转义,部分字符需要转义才能正常通过编译,所以才将<
写成<
。而我们想表达的意思本来应该是这样的:
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)}"
- 集合的使用
这里演示了如何将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"/>
- 本地资源的引用
本地资源可以直接使用
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@{courses.course[teacher.name].age>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}"
- 给属性添加变化监听
当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之后我改变了teacher
中age
的值
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
的基础上,只是添加了@Bindable
和notifyPropertyChanged
。在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也提供了ObservableArrayMap
,ObservableArrayList
自定义属性
这是我觉得DataBinding提供的一个很强大的功能。
想想我们使用自定义属性的场景:先通过自定义View来定义自定义属性,自定义属性值在TypedArray
中进行获取,用这个值完成一系列功能;定义完成之后在布局文件中使用,例如使用Fresco的SimpleDraweeView时候,要分别添加图片加载成功、图片加载失败、图片加载中等不同状态下的属性。因此总的来说步骤还是很多的,但在DataBinding中自定义属性设计的就没那么复杂,你可以以最简单的方式来定义任何名称与类型的属性。
我们来看两种使用场景
- 通过自定义View实现自定义属性
在这个例子中,我在TextView中定义了toast
与textshow
两个属性,分别实现TextView初始化后显示Toast以及setText的功能。
来看下代码的写法。自定义属性的信息在BindingMethod
注解中,BindingMethod
被包裹在BindingMethods
里。进入BindingMethod
注解,属性名称写在attribute
里,属性对应实现的方法写在method
里。这里我们定义的两个新属性所对应的方法分别是show
与showText
,我们只需要在类里面提供相应方法名的方法即可。自定义的属性在AppCompatTextView
上添加,所以类型type
是AppCompatTextView
@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`}"/>
- 通过静态方法来实现自定义属性
自定义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"/>
来看看效果
多属性设置
默认情况下多个属性需要全部实现,如果你不需要强制属性都实现的话只需设置requireAll
为false
即可
@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
}
它只包含attribute
和event
两个属性,功能与@InverseBindingMethod
一样,它的调用时机由inverseBindingListener
所在的@BindingAdapter
决定,所以@BindingAdapter
的value
值与InverseBindingAdapter
的event
值必须一样,才能匹配上
@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 ****
最后还有几个小点要注意一下:
- 如果你用了
@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) {
}
}
- 注意
attribute
的对应以及@BindingAdapter
的value
值与InverseBindingAdapter
的event
值的对应
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”
- 可能会出现死循环绑定。在
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
东西有点多,不过都是一些基础,应该很好理解