将布局中的界面组件绑定到应用的数据源中
使用步骤:
例子:
编译环境
在对应模块的build.gradle下添加dataBinding元素
android {
// DataBinding必须
dataBinding {
enabled = true
}
}
创建数据对象
这里我创建User类,用于绑定到xml文件上
data class User(val name: String)
创建布局文件
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.hhh.jetpacktest.databinding.User" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.name}" />
LinearLayout>
layout>
布局文件一定要以 layout 标签为顶层元素。
当前布局文件名称为 activity_data.xml ,build 一下,就会发现在 build/data_binding_base_class_source_out/debug/out/应用包名/databinding 目录下生成了
ActivityDataBinding.java 文件。
生成规则:根据 xml 文件的名称使用驼峰命名法,然后后面加上Binding生成。当然,也可以在 data 标签中使用 class 自定义名称
绑定数据
class DataBindingActivity : AppCompatActivity() {
yinyong
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 根据activity_data.xml名称自动生成了ActivityDataBinding类
val binding = ActivityDataBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.user = User("张三")
}
}
完成,运行一下发现,主界面中就一个TextView,显示着 User 的名称
数据绑定库会根据根标签为 layout 的布局生成一个类,该类是继承 ViewDataBinding ,绑定数据前必须要获取这个继承类,获取方法主要分为两类
以上面的简单使用中的xml文件为例子,可以这样获取
// 直接使用生成的类的名称ActivityDataBinding
val binding = ActivityDataBinding.inflate(layoutInflater)
setContentView(binding.root)
// 使用DataBindingUtil的获取
val binding2 = DataBindingUtil.setContentView<ActivityDataBinding>(this, R.layout.activity_data)
inflate重载的方法有很多,使用的时候根据ide提示即可。
就是布局xml中的语言,在View的属性里面使用 @{} 里面的就是表达式语言,基本上和外面的java语言类似,就是少了new,super,this 等,不建议表达式语言写复杂的,太复杂的直接在外面写个方法传递到 @{}里面,类似下面的事件处理的方法引用
当表达式求值结果为方法引用的时候,被引用的方法和所有者对象都会封装到监听器中,并在目标View中设置该监听器。如果表达式求值结果为null,相当于没有设置监听器。
方法引用实际监听器是在绑定数据的时候创建的,不是在事件出发的时候创建的。如果希望在事件发生时对表达式求值,应当使用监听器绑定。
举个例子:上面的简单示例中,给TextView设置监听,点击的时候弹出Toast
先创建一个类,定义方法,将来要被引用
class MyHandler {
fun toastText(view: View) {
if (view is TextView) {
Toast.makeText(
MyApp.appInstance().applicationContext// 获取的一个context对象,MyApp继承Application
, view.text, Toast.LENGTH_SHORT
).show()
}
}
}
当前定义的是一个普通的类,也可以使用对象申明使用单例,即object Myhander、
布局文件中使用
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.hhh.jetpacktest.dbinding.User" />
<variable
name="myHandler"
type="com.hhh.jetpacktest.dbinding.MyHandler" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{myHandler::toastText}"
android:text="@{user.name}" />
LinearLayout>
layout>
这里解释了为啥 MyHandler#toastText 方法的参数是 View, 而不是直接使用 TextView,因为 onClick方法的参数就是 View。
与在kotlin 代码中使用setOnclickListener 对比,使用方法引用的优点是表达式在编译时进行处理,所以,如果方法的参数或者签名不正确,编译的时候就无法通过。这里解释了方法引用的监听器是在绑定数据的时候创建的。
最后,一定不要忘记在Activity中传入MyHandler的示例哦
binding.myHandler = MyHandler()
是在事件发生时运行的绑定表达式。在监听器绑定中,方法的返回值必须和监听器预期的范志毅相匹配
来改造一下上面的,使用监听器来重写,直接把布局文件中的 TextView改变一下即可
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{(view)->myHandler.toastText(view)}"
android:text="@{user.name}" />
android:onClick 里面的表达式是一个 lambda 表达式,为啥能够这么写呢?
tv.setOnClickListener { view ->
myHandler.toastText(view)
}
看上面 setOnClickListener 里面的 lambda 表达是就知道了
<data>
<import
alias="CusView"
type="android.view.View" />
<variable
name="user"
type="com.hhh.jetpacktest.dbinding.User" />
data>
import标签中type表示类型,要包括包名,可以使用alias重命名。在导入中,注意某些特殊符号需要使用转义。例如 < 就需要转义。
在下面,就可以使用 CusView应用 android.view.View 这个类。
数据绑定支持 include 标签,但是不支持 include 作为 merge 标签的直接子元素
可观察对象,就是如果这个对象改变了,就会通知其他对象说自己变了,其实跟 LiveData 很相似,更推荐使用 LiveData
可观察字段都是继承 androidx.databinding.BaseObservable 的类
可观察的集合 ObservableList,ObservableMap ,主要就这两个,初始化的时候都是用他们的继承类
可观察字段和可观察集合的使用方法都是类似的
接下来举个例子,还是上面的例子,只是在点击TextView的时候修改User的属性,然后看看TextView会不会有什么变化
步骤如下:
修改数据对象,将其变为可观察的数据对象
data class User(val name: ObservableField<String>)
在 MyHandler 中定义一个方法,用于修改User的属性
class MyHandler {
fun changeName(user: User, newName: String) {
user.name.set(newName)
}
}
修改 Activity_data.xml 文件中 TextView 的属性
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick='@{()-> myHandler.changeName(user,"法外狂徒")}'
android:text="@{user.name}" />
其实这里并没有什么变化,只是给TextView设置了监听器绑定,在点击该 View 的时候就会调用 MyHandler#changeName 方法。
记住,一定要在这个 xml 文件中传入 MyHandler 实例
在 Activity 中传入数据
binding.user = User(ObservableField("张三"))
使用可观察数据对象的时候,都是使用 set ,get 方法来修改值
ok,步骤完成,安装,就会发现,点击了 TextView ,就会发现 TextView 的文字发生了改变了
只需要将一个类实现 androidx.databinding.Observable 接口即可,但是更方便的是继承 androidx.databinding.BaseObservable类。
如下所示:
class User : BaseObservable() {
var name: String = ""
@Bindable
get() = "法外狂徒-${field}"
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
@get:Bindable
var age: Int = 1
set(value) {
field = age
notifyPropertyChanged(BR.age)
}
}
对于一个属性,需要使用 @Bindable 注解在 get 方法上,就可以在 build/generated/source/kapt/debug/包名 目录下生成的 BR 对象中生成一个字段。比如当前 name 属性的 get 方法被注解了后,build 一下,就可以看到 BR 内生成了 name 字段。
对于某些字段,不需要重写 get 方法,用默认的即可,那就在属性上面 @get:Bindable 注解。 如上面的 age 属性
对于set 方法,需要调用 notifyPropertyChanged 方法通知发生了改变。
绑定适配器一般都会对一个属性自动选择方法,不需要我们自定义方法名称,一般就是调用的方法名称就是 set+属性名,参数就是在布局文件中传入的属性的值,必须方法名称与参数类型都对上,库才能够自动选择方法
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.name}" />
如上,text属性,传入的值 user.name 是 String 类型的,库就会查找 TextView#setText(String text) 方法,查找到就可以正常调用,否则编译时就出错了。一般我们不用去管
上面提到,必须要 TextView#setText(String text) 方法,上面代码才能正常,如果没有呢,就需要自己自定义方法,使用 BindingMethods 注解,但是不推荐,很少使用到这种方式,一般直接使用 BindingAdapter
重头戏,基本上都是使用这个注解自定义方法以及逻辑
例子:对 ImageView 设置两个自定义属性(不需要在res目录下attrs.xml创建),用 Glide 框架加载图片。
先自定义一个方法,使用该注解
@BindingAdapter(
value = ["imgUrl", "placeHolder"],
requireAll = true
)
fun loadCusImg(view: ImageView, url: String, placeHolder: Drawable) {
GlideApp.with(MyApp.appInstance().applicationContext)
.load(url)// 加载url
.placeholder(placeHolder)// url加载失败,就使用占位图
.into(view)
}
在布局文件中适应 BindingAdapter 中 value 的属性,注意,值一定要与上面自定的方法的参数的类型相同,否则出错:
<ImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:placeHolder="@{@drawable/test}"
app:imgUrl="@{user.url}" />
user.url 返回的是一个字符串。@drawable/test 返回的是一个 Drawable 图片。
BindingAdapter 中,value 有多个属性的时候,requireAll = true 则表明,在 ImageView 中,value 的多个属性都必须要求有,否则直接报错。比如上面的 ImageView 中如果只写了app:placeHolder ,没有 app:imgUrl 则直接报错。如果 requireAll = false,则不会报错。
类似room那样,将一个特定的类型转换为另一个类型
比如下面,将 String 类型转换为 Int
@BindingConversion
fun convertStringToInt(str: String): Int = str.length
接下下来实用一下,看看是否生效。先自定义一个方法:
@BindingAdapter("length")
fun setTextWithLength(view: TextView, length: Int) {
view.text = "长度为$length"
}
然后在TextView中实用自定义的属性:
<TextView
length="@{user.name}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
ok,完成,可以在 BindingConversion 注解的方法中加上注解,发现的确被调用了
待补充