在传统的开发模式中,我们实现交互页面时,需要在Activity或者fragment等UI组件所对应的XML布局文件中,除了按设计约束摆放各控件之外,还需要对这些与交互相关的控件设置id,然后在代码中进行findViewById操作,将这些控件对象进行实例化,再进行逻辑控制,如setText、setImage或者setOnClickListener等等。这样的话需要在UI组件中实现大量的逻辑处理,使得Activity或者Fragment等显得臃肿不堪,维护起来很困难,为了减轻页面的工作量,Google提出了DataBiiding。
DataBinding能够分担Activity等组件中的属于页面交互的工作。DataBinding主要有以下特点:代码更加简介,可读性高,能够在XML文件中实现UI控制,不再需要findViewById操作,能够与Model实体类直接绑定,易于维护和扩展。
要想在项目中接入DataBinding其实非常简单,只需要在module的build.gradle文件中加入以下配置即可:
android {
dataBinding {
enabled = true
}
}
DataBinding简单使用
1、修改布局文件
使用DataBinding的第一步,就是先改造XML文件,其实改造布局文件也特别简单,只需要在原来文件内容的基础上,最外层改为<layout>标签,其他内容不变:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
除了手动加layout标签外,也可以借助Android Studio的提示,选择Convert to data binding layout选项,自动添加layout标签。
在布局最外层加layout标签后,重新build项目,DataBinding库就会生成对应的Binding类,该类用来实现XML布局文件与Model类的绑定,如下:
public class ActivityDataBindingBindingImpl extends ActivityDataBindingBinding {
@Nullable
private static final androidx.databinding.ViewDataBinding.IncludedLayouts sIncludes;
@Nullable
private static final android.util.SparseIntArray sViewsWithIds;
static {
sIncludes = null;
sViewsWithIds = null;
}
// views
@NonNull
private final androidx.constraintlayout.widget.ConstraintLayout mboundView0;
// variables
// values
// listeners
// Inverse Binding Event Handlers
public ActivityDataBindingBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
this(bindingComponent, root, mapBindings(bindingComponent, root, 1, sIncludes, sViewsWithIds));
}
private ActivityDataBindingBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 0
);
this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
this.mboundView0.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
@Override
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x1L;
}
requestRebind();
}
@Override
public boolean hasPendingBindings() {
synchronized(this) {
if (mDirtyFlags != 0) {
return true;
}
}
return false;
}
@Override
public boolean setVariable(int variableId, @Nullable Object variable) {
boolean variableSet = true;
return variableSet;
}
@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
}
return false;
}
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
// batch finished
}
// Listener Stub Implementations
// callback impls
// dirty flag
private long mDirtyFlags = 0xffffffffffffffffL;
/* flag mapping
flag 0 (0x1L): null
flag mapping end*/
//end
}
生成Binding类的名字很特殊,它与XML布局文件的名字有对应关系,具体的联系就是,以XML布局文件为准,去掉下划线,所有单次以大驼峰的形式按顺序拼接,最后再加上Binding,如我的XML布局文件名是activity_data_binding,生成的Binding类名就是ActivityDataBindingBinding。
2、绑定布局
没有用DataBinding时,为了将XML布局文件与Activity进行绑定,需要调用Activity的setContentView()方法,或者是在Fragment中调用LayoutInflate的inflate()方法,将XML在R文件中的映射传入进行绑定。如果使用了DataBinding之后,就需要使用DataBindingUtil类,如:
var binding = DataBindingUtil.setContentView<ActivityDataBindingBinding>(
this@DataBindingActivity,
R.layout.activity_data_binding
)
使用DataBindingUtil类的setContentView()方法对Activity进行绑定,其返回值就是工具生成的Binding类。
在Fragment会稍有不同:
class DataBindingFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
var fragmentDataBindingBinding = FragmentDataBindingBinding.inflate(inflater)
return fragmentDataBindingBinding.root
}
}
在Fragment中不是使用DataBindingUtil类来进行绑定,而是通过生成的Binding类的inflate()方法inflate并返回一个Binding类实例,最终调用getRoot()方法返回对应的View。
3、添加data标签
之前的步骤,已经使用DataBinding将XML文件与UI组件进行了绑定,然后需要在XML文件中接受Model数据,这里就用到了data标签与variable标签。
在XML文件的layout标签下,创建data标签,在data标签中再创建variable标签,variable标签主要用到的就是name属性和type属性,类似于Java语言声明变量时,需要为该变量指定类型和名称,这里的type就是该属性的类型,这里需要设置Model类的全类名,name就是属性的名称,供在XML文件的其他位置引用,表示一个Model类实例:
其中Person定义如下:
class Person {
var isMen = true
var age: Int = 0
var name: String
var address: String
constructor(age: Int, name: String, address: String) {
this.age = age
this.name = name
this.address = address
}
public fun getFullAddress(): String {
return "China $address"
}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="person"
type="com.jia.demo.jetpack.databinding.Person" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
4、传递variable属性
在定义好XML布局文件之后,我们就需要在Activity或者Fragment中将Model实体类对象传递给XML文件了,这里就用到了自动生成的Binding类了,Binding类为每一个variable标签对应的type都创建了set方法用以传递数据:
binding.person = Person(20, "js", "beijing")
variable标签的type,不仅可以是我们定义的数据类型,也可以是基本数据类型,如String,Integer等等。
5、使用variable
我们已经在XML文件中声明好了一个variable属性,接下来就可以使用了。
这里需要用到一个新的概念叫做布局表达式: @{ }
可以在布局表达式@{ }中,传入variable对象的各属性,也可以是方法,最终将完整的布局表达式设置给控件的对应属性即可:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="person"
type="com.jia.demo.jetpack.databinding.Person" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv1"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@{person.address}"
app:layout_constraintBottom_toTopOf="@id/tv2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv2"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@{person.name}"
app:layout_constraintBottom_toTopOf="@id/tv3"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv1" />
<TextView
android:id="@+id/tv3"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@{person.fullAddress}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv2" />
<CheckBox
android:layout_width="50dp"
android:checked="@{person.men}"
android:layout_height="50dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
上述示例中,我们将Person对象的各个属性分别设置给了三个TextView,也就是将布局表达式设置给TextView的text属性,可以看到,布局表达式中,不仅可以传入variable对象的属性,也可以调用variable对象的方法。
由于在上一步中,已经把初始化好的Person对象设置给了Binding类,所以这里直接运行代码就可以看到效果了。
Person类中的age属性是一个int类型,如果在XML中直接使用@{person.age}设置给text属性,会导致程序奔溃,这里要注意,text属性传入的布局表达式中的属性或者是方法,必须是字符串类型。CheckBox的checked属性要求传入的是一个boolean类型值,而Person的isMen属性就是boolean属性,所以这里可以将checked属性设置为@{person.men}。也就是说,布局表达式中的值,需要满足控件属性的类型要求,否则会抛异常。
之前我们是将整个Person对象设置给了Binding类,我们也可以单独的修改其中的某个值,如:
var person = Person(20, "js", "beijing")
binding.person = person
person.address = "shanghai"
这样,当我们动态改变Person中的属性值时,XML布局中的内容也会自动随之改变,这正是DataBinding的一大特点,不再需要手动调用控件设置属性的方法去更新控件,实现了由Model类动态控制UI展示。
6、布局表达式中使用静态方法
在开发中经常需要编写一些工具类,对Model属性进行编辑和修改,当然,工具类中的方法需要是静态方法。DataBinding也支持在XML中使用静态方法。首先我们先定义一个静态方法:
public class PersonUtil {
public static String getFullInfo(Person person) {
return person.getName() + " " + person.getAddress();
}
}
接下来需要在data标签中使用import属性,意为导入类:
<data>
<import type="com.jia.demo.jetpack.databinding.PersonUtil" />
<variable
name="person"
type="com.jia.demo.jetpack.databinding.Person" />
</data>
然后就可以在布局表达式中调用其静态方法了:
<TextView
android:id="@+id/tv1"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@{PersonUtil.getFullInfo(person)}"
app:layout_constraintBottom_toTopOf="@id/tv2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
7、响应事件
使用DataBinding,不仅可以在布局文件中对控件某些属性进行赋值,使得Model类数据直接绑定在布局中,而且Model属性发生变化时,布局文件中的内容可以即时刷新。而且还可以使用DataBinding响应用户事件,如可以实现Button点击时的响应。
上文的介绍可以知道,布局表达式不仅可以传入对象的属性,也可以调用对象的方法。
首先创建一个工具类,在类中定义响应事件的方法,如:
class ButtonClickListener {
public fun onClick(view: View) {
println("click")
}
}
在布局文件中:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="ButtonClickHandler"
type="com.jia.demo.jetpack.databinding.ButtonClickListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
app:layout_constraintTop_toBottomOf="@id/tv3"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_width="0dp"
android:layout_height="50dp"
android:onClick="@{ButtonClickHandler.onClick}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
首先在data标签中为ButtonClickListener类声明对象,在Button的onClick属性中传入布局表达式,这里内容是声明的点击处理类对象的onClick方法。
之后在Activity中传入ButtonClickListener对象即可:
binding.buttonClickHandler = ButtonClickListener()
这样就点击Button的时候就可以响应点击事件了。
8、include标签
为了能够让布局文件得到复用,在编写布局的时候,会经常用的include标签,使得相同结构与内容的布局文件可以在多处使用。但是如果一个布局文件中使用了DataBinding,同时也使用了include标签,那么在include标签引入的布局文件中如何使用一级页面的数据呢。
这时,需要在一级页面的include标签中,通过命名控件xmlns:app来引入布局变量person,将数据对象传递给二级页面,如:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="person"
type="com.jia.demo.jetpack.databinding.Person" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_data_binding"
app:persondata="@{person}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
布局表达式中直接传入页面变量person,这里的include标签属性值可以任意取名,但是要注意的是,在二级页面的variable标签中的name属性,必须与一级页面中的include标签属性名一致,如这里的“persondata”:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="persondata"
type="com.jia.demo.jetpack.databinding.Person" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#567">
<TextView
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@{persondata.address}"
android:textColor="#fff"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
这样,在include所引入的layout文件中,也可以直接使用由一级页面传过来的数据了。
BindingAdapter
使用DataBinding库时,DataBinding会针对控件属性生成对应的XXXBindingAdapter类,如TextViewBindingAdapter类,其对TextView的每个可以使用DataBinding的属性都生成了对应的方法,而且每个方法都使用了@BindingAdapter注解,注解中的参数就是对应View的属性,如:
@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressWarnings({"WeakerAccess", "unused"})
@BindingMethods({
@BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
@BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
@BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),
@BindingMethod(type = TextView.class, attribute = "android:inputType", method = "setRawInputType"),
@BindingMethod(type = TextView.class, attribute = "android:scrollHorizontally", method = "setHorizontallyScrolling"),
@BindingMethod(type = TextView.class, attribute = "android:textAllCaps", method = "setAllCaps"),
@BindingMethod(type = TextView.class, attribute = "android:textColorHighlight", method = "setHighlightColor"),
@BindingMethod(type = TextView.class, attribute = "android:textColorHint", method = "setHintTextColor"),
@BindingMethod(type = TextView.class, attribute = "android:textColorLink", method = "setLinkTextColor"),
@BindingMethod(type = TextView.class, attribute = "android:onEditorAction", method = "setOnEditorActionListener"),
})
public class TextViewBindingAdapter {
private static final String TAG = "TextViewBindingAdapters";
@SuppressWarnings("unused")
public static final int INTEGER = 0x01;
public static final int SIGNED = 0x03;
public static final int DECIMAL = 0x05;
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
@BindingAdapter({"android:autoText"})
public static void setAutoText(TextView view, boolean autoText) {
KeyListener listener = view.getKeyListener();
TextKeyListener.Capitalize capitalize = TextKeyListener.Capitalize.NONE;
int inputType = listener != null ? listener.getInputType() : 0;
if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
capitalize = TextKeyListener.Capitalize.CHARACTERS;
} else if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
capitalize = TextKeyListener.Capitalize.WORDS;
} else if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
capitalize = TextKeyListener.Capitalize.SENTENCES;
}
view.setKeyListener(TextKeyListener.getInstance(autoText, capitalize));
}
}
BindingAdapter类中,所有的方法都是static方法,并且每个方法都使用了@BindingAdapter注解,注解中声明所操作的View属性,当使用了DataBinding的布局文件被渲染时,属性所对应的static方法就会自动调用。
除了使用库自动生成的BindingAdapter类之外,当然也可以自定义BindingAdapter类,供开发者来实现系统没有提供的属性绑定,可以借助自定义BindingAdapter实现一些比较复杂的属性操作。
自定义BindingAdapter类
我们假设来实现这样的需求,为TextView添加“fullText”属性,为该属性传入字符串内容,但是该字符串如果超过4个字符长度,超过部分直接截取,如果不够4个字符则末尾以“++”补齐。
object FullTextBindingAdapter {
@JvmStatic
@BindingAdapter("fullText")
fun setFullText(textView: TextView, content: String) {
var fullText: String? = when {
TextUtils.isEmpty(content) -> {
"****"
}
content.length <= 4 -> {
content
}
else -> {
content.substring(0, 4)
}
}
textView.text = fullText
}
}
定义FullTextViewBindingAdapter类,在其中创建静态方法“setFullText()”方法,并且使用@BindingAdapter注解修饰,在注解中设置我们为TextView新定义的属性。
该方法第一个参数必须是所操作的View类型,第二个参数是字符串类型,在方法中进行逻辑处理。接下来正在布局文件中使用:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="person"
type="com.jia.demo.jetpack.databinding.Person" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv1"
android:layout_width="match_parent"
android:layout_height="50dp"
app:fullText="@{person.name}"
app:layout_constraintBottom_toTopOf="@id/tv2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
可以看到,在布局文件中,还是如之前的普通属性一样,直接使用fullText属性即可。这里只是简单介绍了一个例子,我们还可以为ImageView设置网络图片,只需要传入图片地址url即可,在这里就不再详细介绍。
BindingAdapter中的静态方法是允许重载的,例如,我们需要为ImageView传入一个字符串类型的参数,在静态方法中加载网络图片并设置到ImageView上,当然,ImageView也可以设置int类型的数据,即加载本地资源图片,所以可以同时定义两个不同参数类型的重载方法,在布局中使用时传入两种类型的数据都是可以的。
BindingAdapter中的静态方法,一般都是接受两个参数,第一个必须是所操作的View类型变量,第二个控制属性值,当然,这些方法也可以传入多个属性值,如:
object FullTextBindingAdapter {
@JvmStatic
@BindingAdapter(value = ["shortText", "longText"], requireAll = false)
fun setFullText(textView: TextView, shortText: String, longText: String) {
textView.text = "$shortText $longText"
}
}
在@BindingAdapter注解中,第一个参数传入字符串数组,其中的内容就是各个新增的属性名,后面requireAll参数表示是否同时需要,这里设置为false。此方法的目的是将传入的两个字符串值拼接起来再设置给TextView。
这种静态方法多参数的情况下,如果注解中并没有指定value数组,而方法参数中除了第一个View参数,后面还有两个相同类型的参数,这时第一个参数为旧参数值,最后一个为新的参数值:
object FullTextBindingAdapter {
@JvmStatic
@BindingAdapter("fullText")
fun setFullText(textView: TextView, oldText: String, newText: String) {
var content: String = if (TextUtils.isEmpty(oldText)) {
newText
} else {
"$oldText $newText"
}
textView.text = content
}
}
双向绑定
通过DataBinding可将Model数据直接绑定在布局文件中,并且当数据发生变化时,布局中的内容可以动态随之变化,我们称之为单向绑定。试想这样的场景,如果布局中有一个EditText,当用户在输入框中输入内容时,我们希望对应的Model类能够实时更新,这就需要双向绑定,DataBinding同样支持这样的能力。
实现双向绑定,就需要用到ObservableField类,它能够将普通的数据对象包装成一个可观察的数据对象,数据可以是基本类型变量,可以是集合,也可以是自定义类型。我们依旧以Person类为例,有一个默认地址,直接展示到EditText中,当用户编辑输入框时,又能将输入框中的内容实时更新到对应的Person属性中。
首先定义一个ViewModel类,在其中声明一个ObservableField对象,其范型指定为Person,并针对Person的某个属性(如address),实现get和set方法,这样在DataBinding会在布局表达式中调用get和set方法,如下:
public class PersonViewModel {
var person: ObservableField<Person> = ObservableField()
constructor(p: Person) {
person.set(p)
}
fun getAddress(): String {
return person.get()!!.address
}
fun setAddress(address: String) {
person.get()!!.address = address
}
fun handleClick(view: View){
Toast.makeText(view.context,person.get()!!.address,Toast.LENGTH_LONG).show()
}
}
接下来在布局文件中使用DataBinding:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.jia.demo.jetpack.databinding.PersonViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".jetpack.databinding.TwoWayDataBindingActivity">
<EditText
android:layout_width="0dp"
android:layout_height="50dp"
android:text="@={viewModel.address}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:onClick="@{viewModel.handleClick}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
需要特别注意的是,不同于单向绑定,之前的布局表达式是@{},实现双向绑定使用的布局表达式是@={},多了一个等号,便可实现双向绑定。为了验证效果,摆放了一个Button,点击时将Person中的addrss信息展示出来。这样就实现了双向绑定,即根据Model数据主动更新布局,根据布局内容变化再主动通知更新Model数据。
ObservableField与LiveData非常相似,都是将普通的数据对象封装成了可观察对象,理论上二者是可以互相替代的,但LiveData具有生命周期感知能力,并且需要调用observe()方法进行监听,而双向绑定中更推荐使用ObservableField,不需要使用observe()方法,维护起来更加简单。
RecyclerView中使用DataBinding
列表布局在Android应用中非常常见,RecyclerView是使用频率很高的一个控件,DataBinding也支持在RecyclerViieew中实现数据绑定。
使用RcyclerView,就需要用到Adapter,在Adapter中实例化Item布局,然后将List中的数据绑定到布局中,DataBinding可以帮助开发者实例化布局并绑定数据,下面以一个简单例子进行介绍:
首先编写item布局,在item布局中使用DataBinding将person数据绑定:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="person"
type="com.jia.demo.jetpack.databinding.Person" />
</data>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="#1abc94"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:gravity="center"
android:text="@{person.name}" />
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:gravity="center"
android:text="@{person.address}" />
<TextView
android:layout_width="match_parent"
android:layout_height="30dp"
android:gravity="center"
android:text="@{person.getSex}" />
</LinearLayout>
</layout>
接下来编写Adapter类:
class PersonAdapter(private var persons: List<Person>) :
RecyclerView.Adapter<PersonAdapter.DataBindViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindViewHolder {
var binding = DataBindingUtil.inflate<ItemRecyclerDataBindingBinding>(
LayoutInflater.from(parent.context),
R.layout.item_recycler_data_binding,
parent,
false
)
return DataBindViewHolder(binding)
}
override fun onBindViewHolder(holder: DataBindViewHolder, position: Int) {
var person = persons.get(position)
holder.binding.person = person
}
override fun getItemCount(): Int {
return persons.size
}
class DataBindViewHolder : RecyclerView.ViewHolder {
var binding: ItemRecyclerDataBindingBinding
constructor(binding: ItemRecyclerDataBindingBinding) : super(binding.root) {
this.binding = binding
}
}
}
首先是ViewHolder类,不同于以往,之前都是需要在ViewHolder中进行findViewById对子控件进行实例化,由于我们使用了DataBinding,所以不再需要这些操作,这里只需要传入生成的Binding类就可以,在super中调用getRoot()方法返回根View。
Adapter中主要不同的是,首先通过DataBindingUtil的inflate()方法来渲染布局和创建Binding类,并将Binding对象传给ViewHolder。在onBindViewHolder中,将对应position的数据赋值给从ViewHolder的Binding类即可。
在RecyclerView中使用DataBinding就是如此简单,当List中的item数据发生变化时,列表中的内容也会随之更新。
从上面的代码中也可以发现,对RecyclerView设置LayoutManager和Adapter属于对View的一些复杂操作,我们可以通过自定义BindingAdapter的方式,定义一个新的属性,将数据List直接通过DataBinding在布局文件中绑定,将这些操作都封装到BindindAdapter中,在Activity中不再需要设置LayoutManager和Adapter操作,首先定义BindingAdapter:
object PersonRecyclerViewBindingAdapter {
@JvmStatic
@BindingAdapter("persons")
fun setPersons(recyclerView: RecyclerView, persons: List<Person>) {
recyclerView.layoutManager = LinearLayoutManager(recyclerView.context)
recyclerView.adapter = PersonAdapter(persons)
}
}
声明新的属性叫做“persons”,使用@BindingAdapter修饰静态方法,对应的需要设置List对象,在方法中对RecyclerView设置LayoutManager和Adapter。
在布局中使用DataBinding:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="com.jia.demo.jetpack.databinding.Person" />
<import type="java.util.List" />
<variable
name="persons"
type="List<Person>" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".jetpack.databinding.RcyclerViewDataBindingActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:persons="@{persons}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
这里稍不同于以往,这里所需要的数据对象是一个集合List,首先我们import导一下两个类,即Person和List,在variiable属性中设置对象名为persons,type这里要注意,由于编译器的原因,范型指定时的一对尖括号需要转义,接下来在RecyclerView的控件上将persons设置给对应属性即可。
在Activity中:
class RcyclerViewDataBindingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var binding = DataBindingUtil.setContentView<ActivityRcyclerViewDataBindingBinding>(
this,
R.layout.activity_rcycler_view_data_binding
)
var persons = ArrayList<Person>()
for (i in 0..10) {
persons.add(Person(i, "name $i", "beijing $i"))
}
binding.persons = persons
}
}
其核心就是通过DataBindingUtil类渲染布局,将准备好的数据List设置给Binding类,即完成了RecyclerView的列表数据展示。是不是很简单。