图片来自必应
Android-Architecture-Components官方文档
Databinding是Google推出的一个支持View与ViewModel绑定的Library,可以说Databinding建立了一个UI与数据模型之间的桥梁,即UI的变化可以通知到ViewModel, ViewModel的变化同样能够通知到UI从而使UI发生改变,大大减少了之前View与Model之间的胶水代码,如findViewById, 改变及获取TextView的内容还需要调用setText()、 getText(),获取EditText编辑之后的内容需要调用getText(),而有了Databinding的双向绑定,这些重复的工作都将被省去。下面我们就来看一下如何使用Databinding来双向绑定
首先我们先定一个ViewModel,将这个ViewModel的变量content与布局文件中的TextView绑定:
class MyViewModel: ViewModel(){
var content: ObservableFiled<String> = ObservableFiled()
}
复制代码
- 官方支持的双向绑定 这里的官方支持指的是Databinding库中已经定义了一些View的双向绑定,我们如果要使用的话只需要将xml文件中的"@{}"改成"@={}", 如下代码
(test_layout.xml):
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.fb.onedayimprove.viewmodel.MyModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.content}"
/>
<!--android:text="@={model.content}"-->
</LinearLayout>
</layout>
复制代码
上述代码中注释部分就是将单向绑定改为双向绑定的代码
官方支持的双向绑定:
- 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
那么双向绑定是怎么实现的呢?首先来看一下Databinding的源码:
首先是我们在编译之后会生成几个相关文件如test_layout.xml, test_layout-layout.xml, TestLayoutBinding.java, BR文件等。我们主要来看一下TestLayoutBinding.java这个文件。这个文件的主要作用是声明xml内的View控件以及声明的ViewModel,如本例中声明了一个TextView:
@NonNull
private final android.widget.LinearLayout mboundView0;
@NonNull
private final android.widget.TextView mboundView1;
// variables
@Nullable
private com.fb.onedayimprove.viewmodel.MyModel mModel;
复制代码
上述代码中mboundView0为根布局、mboundView1为声明了双向绑定的TextView,mModel为与View绑定的ViewModel。(注意:并不是所有的变量都会被声明,有三种情况:在xml布局中声明id的,将会被定义为静态变量,可以直接通过Databinding对象访问;引用了model数据的;根布局)
接下来当我们定义了双向绑定的时候,TestLayoutBinding.java会生成这样一段代码:
private android.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of model.content.get()
// is model.content.set((java.lang.String) callbackArg_0)
//调用定义的TextViewBindingAdapter中的getTextString(TextView view)方法得到mboundView1(即布局中定义的TextView)的值
java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
// localize variables for thread safety
// model
com.fb.onedayimprove.viewmodel.MyModel model = mModel;
// model.content.get()
//当前View绑定的ViewModel中的content的值
java.lang.String modelContentGet = null;
// model != null
boolean modelJavaLangObjectNull = false;
// model.content
android.databinding.ObservableField<java.lang.String> modelContent = null;
// model.content != null
boolean modelContentJavaLangObjectNull = false;
modelJavaLangObjectNull = (model) != (null);
if (modelJavaLangObjectNull) {
modelContent = model.getContent();
modelContentJavaLangObjectNull = (modelContent) != (null);
if (modelContentJavaLangObjectNull) {
//将View中的值取出并复制给相应的model中绑定的数据
modelContent.set(((java.lang.String) (callbackArg_0)));
}
}
}
};
复制代码
通过上述代码可以看到实现逆向绑定的关键部分,可以看到调用InverseBindingListener接口的onChange()方法就可以将view的值传回ViewModel。那么它在哪里被用到呢?看下面的代码:
@Override
protected void executeBindings() {
...
if ((dirtyFlags & 0x4L) != 0) {
android.databinding.adapters.TextViewBindingAdapter.setTextWatcher(..., mboundView1androidTextAttrChanged);
}
}
复制代码
可以看到在executeBindings这个方法中,将InverseBindingListener 的值传给setTextWatcher,这个方法怎么来的后面会提到。
上面的代码中提到一个TextViewBindingAdapter,通过它的名字就可以看出是绑定View与ViewModel的适配器,那么来看一下TextViewBindingAdapter做了什么 (在包android.databinding.adapters下):
@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);
}
复制代码
上述代码相信大家并不陌生,当Databinding在给某一个控件的XXX属性赋值的时候,需要去找到相应的setXXX()方法,然后将model中的值给这个View。而@BindingAdapter ("xxx")正是将这个setXXX()方法与xxx属性关联起来,所以上面部分的代码作用总结起来就是做了view.setText(text)。(注:可以看到代码中有对新旧内容的比较,只有当内容不同的时候才会执行下一步,这是为了防止双向绑定循环调用)
再来看下一个方法:
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
复制代码
这个方法在TestLayoutBinding.java中已经说明了,是双向绑定取值时调用的方法,@InverseBindingAdapter注解和@BindingAdapter注解作用类似。
再来看下一个关键方法:
@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
"android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
final OnTextChanged on, final AfterTextChanged after,
final InverseBindingListener textAttrChanged) {
final TextWatcher newValue;
//listener为空则不作处理
if (before == null && after == null && on == null && textAttrChanged == null) {
newValue = null;
} else {
//创建一个TextWatcher监听text内容的变化
newValue = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (before != null) {
before.beforeTextChanged(s, start, count, after);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (on != null) {
on.onTextChanged(s, start, before, count);
}
//如果设定了双向绑定,则InverseBindingListener不为空,可参见之前的TestLayoutBinding.java对其的赋值。
if (textAttrChanged != null) {
//调用onChange()方法(实现在TestLayoutBinding.java中),即将view的值传回给ViewModel
textAttrChanged.onChange();
}
}
@Override
public void afterTextChanged(Editable s) {
if (after != null) {
after.afterTextChanged(s);
}
}
};
}
final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
if (oldValue != null) {
view.removeTextChangedListener(oldValue);
}
if (newValue != null) {
view.addTextChangedListener(newValue);
}
}
复制代码
从上面的代码我们可以看到@BindingAdapter绑定了几个属性,其中有一个叫做"android:textAttrChanged",那么这个属性就代表了当TextView的text属性变化,xxxAttrChanged。可以想象得到那这个方法就是在android:text属性发生变化的时候会被调用。我们在这个方法中设置了对android:text这个属性内容变化的监听(注意是内容变化,不是属性变化),以后当每次改内容变化的时候,就会调用textAttrChanged.onChange()这个方法将TextView的值传回给ViewModel。
上述的是通过官方支持的源码来看双向绑定,那么我们要自定义的View使用双向绑定应该怎么做呢?
-
自定义View双向绑定
通过对官方源码分析,我们发现要实现双向绑定关键是需要实现Adapter,实现setter,getter,以及Listene方法。下面就让我们来实现一个自定义View 的双向绑定吧。 对MyModel稍作修改:
class MyModel: ViewModel(){
var content: ObservableField<String> = ObservableField()
fun onChangeClick(view: View){
if(view is TestEditView){
Log.v("---TAG---", content.get())
view.setValue("改变后")
Log.v("---TAG---", "click: ${content.get()}")
}
}
}
复制代码
对test_layout.xml修改(主要是使用自定义的View),加了一个点击事件,点击之后修改View的内容:
<com.fb.onedayimprove.widget.TestEditView
android:layout_width="match_parent"
android:layout_height="50dp"
app:content="@={model.content}"
android:onClick="@{(view) -> model.onChangeClick(view)}"
/>
复制代码
这里我们引入一个自定义View:这是一个类似表单的View,左边是个小标题,右边可以填一些信息等:
class TestEditView: LinearLayout{
private lateinit var contentView: TextView
private var onContentedListener: OnContentChangedListener? = null
{
...
}
private fun initView(context: Context){
val view = View.inflate(context, R.layout.test_edit_layout, this)
contentView = view.findViewById(R.id.text_content)
}
fun setOnContentListener(
listener: OnContentChangedListener?){
this.onContentedListener = listener
}
public fun setValue(content: String){
if (content.isEmpty() || (!content.isEmpty() && content == contentView.text)){
return
}
contentView.text = content
onContentedListener?.onContentChanged()
}
fun getContent(): String{
return contentView.text.toString()
}
//内容改变Listener
interface OnContentChangedListener{
fun onContentChanged()
}
}
复制代码
省略了部分代码,主要就是定义了一个内容改变的Listener。 下面是TestEditViewAdapter的代码:
//使用InverbaseBindingMethod注解,与使用@BindingAdapter效果一样其中,event、method可声明也可以不声明,不声明的话会默认设置xxxAttrChanged 与 getXXX() 方法
//@InverseBindingMethods(InverseBindingMethod(type = TestEditView::class, attribute = "content", event = "contentAttrChanged", method = "getContent"))
object TestEditViewAdapter{
@JvmStatic
@BindingAdapter("app:content")
fun setContent(view: TestEditView, content: String){
if (content.isNotEmpty() && view.getContent() == content){
return
}
view.setValue(content)
}
@JvmStatic
@InverseBindingAdapter(attribute = "app:content", event = "contentAttrChanged")
fun getContent(view: TestEditView): String{
return view.getContent()
}
@JvmStatic
@BindingAdapter(value = "app:contentAttrChanged", requireAll = false)
fun setContentAttrChangedListener(view: TestEditView, bindingListener: InverseBindingListener){
if(bindingListener == null){
view.setOnContentListener(null)
}else{
//如果设置了双向绑定则为其添加监听
view.setOnContentListener(object : OnContentChangedListener{
override fun onContentChanged() {
bindingListener.onChange()
}
})
}
}
}
复制代码
接下来我们只需要在Fragment中为content赋值,然后运行程序,点击TestEditView,就可以看到Log输出了content的初始值与改变后的值(不贴图展示了)。
val binding = TestLayoutBinding.inflate(inflater!!, container, false)
val model = MyModel()
model.content.set("初始值")
binding.model = model
return binding.root
复制代码
-
总结 以上就是Databinding双向绑定的使用方法,总的来说应该注意两点:
1、修改"@{}" 为 "@={}"
2、写setXXX(), xxxAttrChanged(), getXXX()方法。