本节学习使用可观察的数据对象进行单向和双向数据绑定
使用DataBinding,我们可以使用之前我们已知的原始的基本类型、引用类型数据,但这些数据的改变不会使UI自动更新。
DataBinding为我们提供了数据驱动视图的可观察数据对象: objects(对象), fields(字段), collections(集合)。使用它们绑定UI,当这些对象的属性发生改变,UI会自动更新。
DataBinding为我们包装好的基本类型的可观察字段:
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
我们自己也可通过ObservableField 泛型来申明其他类型:
class User {
// 普通类型
var firstName: String? = null
// 使用ObservableField自己封装
var lastName: ObservableField<String>? = null
// 使用官方提供已包装的
var age: ObservableInt? = null
}
class MyHandler(private val mContext: Context) {
// 伴生对象,相当于Java中的静态成员
companion object {
private val TAG = "MyHandler"
}
fun onClick(user: User) {
Toast.makeText(mContext, "点击了:" + user.firstName, Toast.LENGTH_SHORT).show()
user.firstName = "你好"
// 注意调用,不是= ,而是相当于Java中断getLastName().set(String)
// 直接=,相当于setLastName(),不会自动刷新的
user.lastName!!.set("DataBinding")
// !!非空断言运算符将任何值转换为非空类型,若该值为空则抛出异常,因为user.age是可空对象,所以需要使用
user.age!!.set(2)
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.databindingsample.User" />
<variable
name="user"
type="User" />
<variable
name="myHandler"
type="com.example.databindingsample.MyHandler" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{user.lastName}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{String.valueOf(user.age)}" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> myHandler.onClick(user)}"
android:text="属性更改" />
LinearLayout>
layout>
class MainActivity : AppCompatActivity() {
private var binding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val user = User()
user.firstName = "helllo"
user.lastName = ObservableField("databinding")
user.age = ObservableInt(1)
binding!!.user = user
binding!!.myHandler = MyHandler(this)
}
}
效果图:
可以看到,我们使用ObservableField和ObservableInt的字段属性值改变时,视图也随之改变了,而原始的数据类型是属性值改变是不会引起视图更新的。
ObservableArrayMap
ObservableArrayList
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.databindingsample.User" />
<variable
name="list"
type="androidx.databinding.ObservableList<String>" />
<variable
name="map"
type="androidx.databinding.ObservableMap<String,String>" />
<variable
name="myHandler"
type="com.example.databindingsample.MyHandler" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{map[`first`]}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text='@{map["second"]}' />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{list[0]}" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> myHandler.onClick(map,list)}"
android:text="属性更改" />
LinearLayout>
layout>
class MyHandler(private val mContext: Context) {
companion object {
private val TAG = "MyHandler"
}
fun onClick(map: ObservableMap<String,String>,list: ObservableList<String>) {
Toast.makeText(mContext,"点击了",Toast.LENGTH_SHORT).show()
map.put("first","android")
map.put("second","iOS")
list[0] = "GO"
}
}
class MainActivity : AppCompatActivity() {
private var binding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val observableMap = ObservableArrayMap<String, String>().apply {
put("first","kotlin")
put("second","java")
}
val observableList = ObservableArrayList<String>().apply {
add("kotlin")
add("java")
}
binding!!.map = observableMap
binding!!.list = observableList
binding!!.myHandler = MyHandler(this)
}
}
效果如下:
DataBinding给我们提供了Observable,让类继承Observable后,当类对象属性变化时通过notifyPropertyChanged
或notifyChange
来通知UI刷新。
class User : BaseObservable() {
// 将@Bindable注解用于属性getter
// @Bindable注解应该应用于一个可观察类的任何getter访问方法。Bindable将在BR类中生成一个字段来标识该字段,用于当该属性发生改变时
@get:Bindable
var firstName: String = ""
set(value) {
field = value
// 只更新本字段
// notifyPropertyChanged(BR.firstName)
// 更新所有字段
notifyChange()
}
@get:Bindable
var lastName: String = ""
set(value) {
field = value
// notifyPropertyChanged(BR.lastName)
}
@get:Bindable
var age: Int = 0
set(value) {
field = value
// notifyPropertyChanged(BR.age)
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.databindingsample.User" />
<variable
name="user"
type="User" />
<variable
name="myHandler"
type="com.example.databindingsample.MyHandler" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{user.lastName}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{String.valueOf(user.age)}" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> myHandler.onClick(user)}"
android:text="属性更改" />
LinearLayout>
layout>
class MainActivity : AppCompatActivity() {
private var binding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding!!.user = User().apply {
firstName = "1"
lastName = "2"
age = 3
}
binding!!.myHandler = MyHandler(this)
}
}
class MyHandler(private val mContext: Context) {
companion object {
private val TAG = "MyHandler"
}
fun onClick(user: User) {
Toast.makeText(mContext,"点击了",Toast.LENGTH_SHORT).show()
user.apply {
firstName = "4"
lastName = "5"
age = 6
}
}
}
效果:
注意:
- 在Kotlin中使用注解,需要在app build.gradle中添加Kotlin注解插件:
apply plugin: 'kotlin-kapt'
, 否则会编译失败:compileDebugKotlin FAILED- BR类似于R文件,DataBinding自动生成的一个资源ID文件;另外这个id值并不固定,每次build可能都不一样
以上我们学习的DataBinding使用,都是使用的单向绑定,也就是数据驱动视图,下面我们来学习,数据与视图双向驱动,数据改变可以使UI刷新,UI改变也可以让数据更新,也就是DataBinding的双向数据绑定。
先看下面这样一个小栗子:
Model类和UI中的Checkbox,我们希望Checkbox的选中状态可以根据Model的isChecked属性刷新,同时Checkbox选中状态主动改变时,也更新Model的isChecked属性
我尝试用之前单向绑定的方式去做了一下,如下代码:
class Model : BaseObservable(){
@get:Bindable
var isChecked: Boolean = false
set(value) {
field = value
notifyChange()
}
}
class MyHandler(private val mContext: Context) {
fun onCheckedChanged(isChecked: Boolean,model: Model){
if(model.isChecked != isChecked){
model.isChecked = isChecked
Toast.makeText(mContext,"视图驱动数据:" + model.isChecked,Toast.LENGTH_SHORT).show()
}
}
fun onClick(model: Model){
model.isChecked = !model.isChecked
Toast.makeText(mContext,"数据驱动视图:" + model.isChecked,Toast.LENGTH_SHORT).show()
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data class="MainActivityBinding">
<variable
name="model"
type="com.example.databindingsample.Model" />
<variable
name="myHandler"
type="com.example.databindingsample.MyHandler" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<CheckBox
android:id="@+id/cb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@{model.isChecked}"
android:onCheckedChanged="@{(buttonView,isChecked) -> myHandler.onCheckedChanged(isChecked,model)}"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Model更改属性值"
android:onClick="@{() -> myHandler.onClick(model)}"/>
LinearLayout>
layout>
class MainActivity : AppCompatActivity() {
private var binding: MainActivityBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding!!.model = Model()
binding!!.myHandler = MyHandler(this)
}
}
之所以采用数据类继承BaseObservable方式实现,是因为我在尝试使用ObservableBoolean如下方式时,却出现了让我费解的情况,代码如下:
class Model{
var isChecked: ObservableBoolean = ObservableBoolean(false)
}
class MyHandler(private val mContext: Context) {
fun onCheckedChanged(isChecked: Boolean,model: Model){
if(model.isChecked.get() != isChecked){
// 关键问题在这行代码
model.isChecked = ObservableBoolean(isChecked)
Toast.makeText(mContext,"视图驱动数据:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
}
}
fun onClick(model: Model){
model.isChecked.set(!model.isChecked.get())
Toast.makeText(mContext,"数据驱动视图:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
}
}
就这样其他代码不变,我的本意是在视图驱动视图时,更新Model的isChecked属性通过model.isChecked = ObservableBoolean(isChecked) ,使用 = 直接赋值,不调用set()刷新了,就出现了如下的情况,单独点击CheckBox状态更新没有问题,单独点击Button更新CheckBox也没问题,但是只要点击了CheckBox再去点击Button就无法再刷新视图了,如下效果图:
model.isChecked = ObservableBoolean(isChecked) 替换成 model.isChecked.set(isChecked) 就正常了
这个问题我搞不懂,知道原因的还请为我解惑,先行谢过了!
进入正题,下面来看这个让我浪费很长时间的问题,DataBinding怎么使用双向绑定来搞定的,更改代码如下:
<CheckBox
android:id="@+id/cb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={model.isChecked}"
android:onCheckedChanged="@{(buttonView,isChecked) -> myHandler.onCheckedChanged(isChecked,model)}"/>
class MyHandler(private val mContext: Context) {
// 这个监听只为输出属性值
fun onCheckedChanged(model: Model){
Toast.makeText(mContext,"视图驱动数据:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
}
fun onClick(model: Model){
model.isChecked.set(!model.isChecked.get())
Toast.makeText(mContext,"数据驱动视图:" + model.isChecked.get(),Toast.LENGTH_SHORT).show()
}
}
效果如下:
如上代码MainActivity代码不变,使用@={}
符号即可完成双向绑定,就代替了我之前的onCheckedChanged监听,和其中的一些处理,如果不是为了Toast属性值,xml中的onCheckedChanged是不用设置的
细心的话会发现,点击CheckBox时输出的Model.isChecked是反的,其实没问题,是因为CheckBox的onCheckedChanged回调在Model.isChecked属性改变之前,拿到的还是之前的值