Databinding 雙向繫結詳解

fb0122發表於2018-04-11

圖片來自必應

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

相關文章