Databinding 双向绑定详解

图片来自必应

Databinding官方文档

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()方法。

发布了154 篇原创文章 · 获赞 40 · 访问量 18万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览