jetpack-DataBinding学习
介绍
曾经我们经常遇到一个View和一个实体字段有绑定关系,View的属性随着实体字段相互依赖变化,我们经常的做法就是给这个View增加变化监听,然后修改对应属性实体。
这种方法很复杂,而且经常稍有不慎出现死循环的,我以前的公司就是这种方案,很难受。学了DataBinding我发现这种xml布局和实体对象绑定的功能,能完美的解决这个问题。
使用
启用dataBinding
android {
...
dataBinding {
enabled = true
}
}
修改布局文件
选择根布局,选择`Convert to data binding layout
生成的结果
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<ConstraintLayout... /> <!-- UI layout's root element -->
</layout>
发现跟布局使用了layout,并且多生成了data节点,其中的user变量data描述了可在此布局中使用的属性。
布局中的表达式使用“ @{}”语法写入属性属性中。例如TextView文本设置为变量的 user的firstName:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
到这里本布局就和User实体绑定了,并且一个TextView和user的firstName做了绑定,也就是这个view展示的内容就是firstName
当写完布局后,build下项目,你会多生成一个类,类名是:布局名字Binding
绑定数据
xml已经知道了,创建本布局View需要一个User对象了,哪这个对象多久设置了??该如何设置呢?
- 设置Activity的DataBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Test", "User")
}
- 通过LayoutInflater创建DataBinding对象
// ActivityMainBinding对应布局生成的Binding类,也就是有一个布局文件是activity_main,的根部局势使用了layout
val bindng: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())
- 通过DataBindlingUitl
// 传入对用的布局
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
// 如果需要使用view 调用listItemBinding.root
布局中的data节点
variable
需要绑定变量,例如当前布局需要一个com.example.User
类型的对象,name
这个对应下面引用的昵称(类似设置个昵称),每个name
对应在生成的DataBinding对象中生成了一个字段,将来需要赋值
<variable name="user" type="com.example.User"/>
import
导入,例如多个对象,都是list类型,可以用这种形式
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
# 集合使用可以使用这种方式
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
这里有个坑,有时候有泛型,怎么处理呢?如何你在xml中写
List<String>
,会报错,请使用<
代替
alias
重新命名,如果名字有冲突了,可以使用alias重新命名
# 重新将List命名成AliasList
<import type="java.util.List" alias="AliasList"/>
<variable name="list" type="AliasList<String>"/>
自定义绑定类名称
默认情况下,将根据布局文件的名称生成绑定类,以大写字母开头,删除下划线(_),大写以下字母,并为单词Binding添加后缀
。该类放在 databinding模块包下的包中。例如,布局文件 contact_item.xml
生成ContactItemBinding
类。如果模块包是com.example.my.app
,则绑定类放在 com.example.my.app.databinding
包中。
通过调整元素的class属性,可以重命名绑定类或将绑定类放在不同的包中 data。例如,以下布局在当前模块ContactItem
的databinding
包中生成绑定类:
<data class="ContactItem">
…
</data>
您可以通过在类名前加一个句点来为不同的包生成绑定类。以下示例在模块包中生成绑定类:
<data class=".ContactItem">
…
</data>
您还可以使用要在其中生成绑定类的完整包名称。以下示例ContactItem在com.example包中创建绑定类 :
<data class="com.example.ContactItem">
…
</data>
布局文件中可用的表达式
- 数学的
+ - / * %
- 字符串连接
+
- 逻辑
&& ||
- 二进制
& | ^
- 比较
== > < >= <=(注意<需要转义为<)
- 位移
>> >>> <<
- instanceof
- 三元运算符
?:
- 方法调用
- 数组访问
[]
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
这里说一个比较特殊的表达式 ??
# 如果displayName为null就使用lastName,否则就是用displayName
android:text="@{user.displayName ?? user.lastName}"
事件绑定
前面说了字段绑定,现在说下事件绑定
传入方法引用
<data>
。。。。
<variable
name="activity"
type="com.qihoo.jectpackdemo.ui.fragment.RegisterFragment"/>
</data>
# 对应按钮的点击事件,方法在activity对象的clear方法
<Button
android:text="clear"
android:onClick="@{activity::clear}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
对用类的方法这里要注意,形参必须要对应方法的参数,不然会报错
fun clear(view: View) {
register.user.set("")
register.pass.set("")
}
传入lambda表达式
# 这种方式比较灵活了,不需要填写对应参数的形参了,但是我如果需要形参呢?
# android:onClick="@{(view)->activity.changeRemark(view)} 在lambda填写参数即可,如果事件参数多个,一定要把lambda形参补全
<Button
android:text="修改备注"
android:onClick="@{()->activity.changeRemark()}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Includes布局
如果使用了includes呢?如何将数据导入呢?
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
# 关键点1
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<merge><!-- Doesn't work -->
<include layout="@layout/name"
# 关键点2
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>
可观察数据
通过上面的只能达到view创建的时候,对应内容展示对应字段。 当字段发生变化,view发生变化,这种的时候应该怎么处理呢?
创建可观察数据
class User {
val firstName = ObservableField<String>()
val lastName = ObservableField<String>()
val age = ObservableInt()
}
// 修改数据
fun clear(view: View) {
user.firstName.set("")
user.lastName.set("")
}
这样创建的变量,就可以达到字段发生变化,对应View的属性也发生变化。接下来问题,如何达到view属性变化,对应字段也发生变化呢?
# 其实很简单,只需要在引用的时候使用 @={} (注意有个 = )
<EditText
android:hint="请输入账号"
android:text="@={register.user}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
自定义可观察对象
class User : BaseObservable() {
@get:Bindable
var firstName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.firstName)
}
@get:Bindable
var lastName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.lastName)
}
}
立即绑定
当变量或可观察对象发生更改时,绑定计划在下一帧之前更改。但是,有时必须立即执行绑定。要强制执行,请使用该 executePendingBindings()
方法。
有时,特定的绑定类是未知的。例如,RecyclerView.Adapter
针对任意布局的操作不知道特定的绑定类。它仍然必须在调用onBindViewHolder()
方法期间分配绑定值。
在以下示例中,RecyclerView绑定的所有布局都具有 item变量。该BindingHolder
对象有一个getBinding()
返回ViewDataBinding
基类的方法 。
override fun onBindViewHolder(holder: BindingHolder, position: Int) {
item: T = items.get(position)
holder.binding.setVariable(BR.item, item);
holder.binding.executePendingBindings();
}
绑定适配器
基本用法
当一个view有setXXX方法设置属性,就都可以在DataBinding中配置。例如DrawerLayout
没有任何属性,但有很多setter
。以下布局分别自动使用 setScrimColor(int)
和setDrawerListener(DrawerListener)可以通过
app:scrimColor和
app:drawerListener`设置:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">
自定义适配器
举个简单的例子,例如一个imageview没有加载图片的功能。能不能自定义这样的一个熟悉,然后定义熟悉的方法实现,自动完成加载图片呢?
@BindingAdapter("loadImage")
fun ivLoadImage(view: ImageView, url: String) {
// 加载图片逻辑
}
<ImageView
# 关键点在这里,使用了loadImage属性
app:loadImage='@{register.iconUrl}'
android:src="@mipmap/ic_launcher"
android:layout_width="200dp"
app:paddingLeftAndRight="@{30}"
app:paddingTopAndBottom="@{60}"
android:layout_height="200dp"/>
当创建后,就会自动调用ivLoadImage方法了
这里有个坑,我使用@BindingAdapter,编译报错,提示我没有app:loadImage属性。最后原来没有使用kotlin的apt(注解处理器)
在项目的build,gradle加入apply plugin: 'kotlin-kapt'
配置多个适配器:
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
这样就配置了2个属性,一个imageUrl,一个error。注意:这种触发条件必须2个属性同时存在才会触发
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
有时候,我定义了多个。但是我想任意一个都触发,这时候只需要配置requireAll=false
,注意:如果这样一定要对形参做非null判断哦!
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
自定义转化
举个例子,一个ImageView的background属性,期望属性值是Drawable
,但是我传入了一个字符串,我们需要定义一个转换方法
<ImageView
android:background='@{"我专门传一个字符串"}'
android:layout_width="200dp"
app:paddingLeftAndRight="@{30}"
app:paddingTopAndBottom="@{60}"
android:layout_height="200dp"/>
形参String类型,返回值Drawable类型
@BindingConversion
fun convertColorToDrawable(colorStr: String): Drawable {
log("收到转换:$colorStr")
return ColorDrawable(Color.RED)
}
注意:但是,绑定表达式中提供的值类型必须一致。您不能在同一表达式中使用不同的类型,如以下示例所示:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>