MVVM dataBinding 學習心得

下位子發表於2017-12-23

目錄

MVVM DataBinding 介紹

MVVM框架類似於早期的MVC和最熱的MVP,但是比起這兩個更為強勢。MV-VM相比於MVP,其實就是將Presenter層替換成了ViewModel層,我們都知道,MVP的好處就是將邏輯程式碼從View層抽離出來,做到與UI層的低耦合,但是無形中會創造出許多的介面,有些介面很是冗餘,不僅如此,當後期修改資料或者新增新的功能還需要修改或是新增介面,很是麻煩。

這個時候MV-VM的優勢就體現出來了,ViewModel層所需要做的完全就是跟邏輯相關的程式碼,完全不會涉及到UI。當資料變化,直接驅動UI的改變,中間省去了冗餘的介面。同時,在ViewModel層編寫程式碼中,要求開發者需要將每個方法儘可能的做的功能單一,不與外部有任何的引用或者是聯絡,無形中提高了程式碼的健壯性,方便了後期的單元測試。

DataBinding其實就是谷歌出臺的工具,是實現UI和資料繫結的框架,ViewViewModel通過DataBinding實現單向繫結或雙向繫結,做到UI和資料的相互監聽,同時開發者的任務分配也就很明確了,負責ViewModel的小夥伴完全不用考慮UI如何實現,很大程度上提高了程式碼的開發效率和後期出問題跟蹤的準確性,針對這些好處,採用MVVM進行程式碼開發還是非常有必要的。

初步使用

1. modulebuild.gradle檔案加上一行配置程式碼

android {
    ...
    dataBinding {
        enabled = true
    }
}
複製程式碼

2. 建立佈局檔案

只需要在之前佈局的基礎上,外層巢狀 <layout></layout>即可。

<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="student"
            type="com.xiaweizi.bean.Student"/>
        <!-- 這裡 type 必須傳完整路徑,或者用 import 方式也是可以的 -->
        <!--
            <import type="com.xiaweizi.bean.Student"/>
            <variable
                name="student"
                type="Student"/>
        -->
    </data>

    <!-- 對應之前的XML檔案 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical">

    </LinearLayout>
</layout>
複製程式碼

因為XML是不支援自定義導包的,所以通過import先導包,如果類名相同的話可以通過alias進行區分:

<import type="android.view.View"/>
<import type="com.xiaweizi.View"
        alias="MyView"/>

<variable
    name="view1"
    type="View"/>

<variable
    name="view2"
    type="MyView"/>
複製程式碼

這個時候會在app\build\generated\source\debug\包名路徑下生成對應的binding類,命名方式,舉個例子最為直接:

原XML名:activity_main  ----> 生成對應的binding名: ActivityMainBinding
複製程式碼

3. Activity中替換原來的setContentView()程式碼

ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
複製程式碼

4. 接下來就是關鍵的ViewModel

a. 單向繫結

我們們先從簡單的開始,DataBinding有個很大的好處就是摒棄原生findViewById頻繁的遍歷檢視層和ButterKnife的反射,採用的是陣列記錄每個view

final Object[] bindings = mapBindings(bindingComponent, root, 8, sIncludes, sViewsWithIds);
複製程式碼

XML建立一個TextView

<TextView
    android:id="@+id/tv_content"
    android:text="@{student.name}"
    android:layout_width="match_parent"
    android:layout_height="50dp"/>
複製程式碼

在程式碼中通過binding直接可以獲取到這個TextView

mBinding.tvContent
複製程式碼

那麼如何實現單向繫結呢?

Student student = new Student("xiaweizi", 12);
mBinding.setStudent(student);
複製程式碼

這樣就可以直接改變TextView的值。

ViewModel就是簡單的資料

public class Student {
    public String name;
    public int age;
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
複製程式碼

b. 雙向繫結

之前說的單向繫結,即當資料變化,通過mBinding.setStudent(student)方式驅動UI的改變 而雙向繫結,無論View還是ViewModel誰改變,都會驅動另一方的改變,實現雙向繫結有兩種方式:繼承BaseObservable和使用ObservableField建立成員變數。

程式碼實現: 第一種繼承BaseObservable

public class Student extends BaseObservable{

    // 如果是 public 則在成員變數上方加上 @Bindable 註解
    @Bindable
    public String sex;

    public void setSex(String sex) {
        this.sex = sex;
        notifyPropertyChanged(BR.sex);
    }
    
    /*************************** 我是分割線 ***************************/
    // 如果是 private 則在成員變數的 get 方法中新增 @Bindable 註解
    private String name;
    @Bindable
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }
    
    public void setSexName(String name, String sex){
        this.name = name;
        this.sex = sex;
        notifyChange();
    }
}
複製程式碼

這個時候當呼叫setName()方法,不僅資料改變,UI中的TextView內容也會隨之改變。

我們可以發現有兩個方法:notifyPropertyChanged()notifyChange,一個是更新指定的變數,第二個是更新所有該ViewModel中的物件。

notifyPropertyChanged(int fieldId)裡面傳的引數,即上面通過@Bindable註解建立對應的變數id

第二種:使用ObservableField

public class Student extends BaseObservable{

    public ObservableField<String> name = new ObservableField<>();
    
    private ObservableInt age = new ObservableInt();
    public void setAge(int age) {
        this.age.set(age);
    }
    public int getAge() {
        return age.get();
    }
}
複製程式碼

通過使用ObservableField建立的物件作用相當於第一種的方案,支援ObservableIntObservableBoolean或者是ObservableField<T>指定的型別、ObservableArrayMap<String, Object>ObservableArrayList<Object>等。

ObservableField內部已經封裝了getset方法,如果成員變數是public屬性,直接通過

mStudent.name.set("shabi");
String name = mStudent.name.get();
複製程式碼

設定和獲取對應的成員變數的值。

如果是private,可以自己封裝getset方法,效果一樣。

其他使用

學會了上面基本的使用者還是遠遠不夠的,像按鈕的點選事件或是EditText內容的監聽,這些也是非常重要的,不過學會了一種,其他的舉一反三就會容易的多了。

1. 事件處理

dataBinding需要你通過一些表示式來處理view的分發事件,除了少數例子外,事件元素的名稱是由監聽器中的方法所控制。比如View.OnLongClickListener內部有onLongClick()方法,所以XML定義的事件就為android:onLongClick.

可以直接在Activity內部定義一個類,用於處理事件的監聽

public class Presenter {
    public void onClickExample(View view) {
        Toast.makeText(SimpleActivity.this, "點到了", Toast.LENGTH_SHORT).show();
    }
    
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        mStudent.name.set(s.toString());
    }

    public void onClickListenerBinding(Student student) {
        Toast.makeText(SimpleActivity.this, student.name.get(),Toast.LENGTH_SHORT).show();
    }
}
複製程式碼

XML中:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="輸入name"
    android:onTextChanged="@{presenter::onTextChanged}"/>


<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="@{presenter.onClickExample}"
    android:text='@{"年齡:" + student.age}'/>

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="5dp"
    android:onClick="@{() -> presenter.onClickListenerBinding(student)}"
    android:text='@{"姓名:" + student.name}'/>
複製程式碼

首先從點選事件開始分析,android:onClick="@{presenter.onClickExample}" 裡面對應的方法自然是要與Presenter定義的方法名一致,名字可以不為onClickExample,但是引數必須是View,引數要對應於setOnClickListener(onClickListener listener)對應的onClickListener要實現的介面,即public void onClick(View)

同理,監聽EditText文字的變化,一般只要注意onTextChanged(CharSequence s, int start, int before, int count)方法即可,那麼我們可以建立與之對應的方法,在XML檔案中引用:android:onTextChanged="@{presenter::onTextChanged}"

最後再來看從UI中獲取資料,也就是資料的回撥,即DataBinding的精髓支出,ViewViewModel雙向繫結。android:onClick="@{() -> presenter.onClickListenerBinding(student)}這裡用到了lamda表示式,這樣就可以不遵循預設的方法簽名,將student物件直接傳回點選方法中。來看一下實現效果:

簡單測試.gif

一目瞭然,我就不贅述了,我們可以發現一點,一開始我們並沒有給Student物件設定值,所以顯示的是null,並沒有報空指標異常,這也是DataBinding的有點之一。

其實dataBinding自帶對資料監聽的方法:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={student.name}"/>
複製程式碼

程式碼中:

student.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
        @Override
        public void onPropertyChanged(Observable observable, int i) {
            // i 為 BR 檔案中對應的 int 值
            Log.i("xwz--->", student.getName());
            Log.i("xwz--->", student.getAge());
        }
});
複製程式碼

這個對資料的監聽建立在,使用@Bindable作為雙向繫結為條件,當資料變化,便會出發onPropertyChanged方法。需要注意的是android:text="@={student.name}",@後面多了一個=

2. ViewStubinclude

dataBinding同樣是支援ViewStub的,使用起來也很簡單,直接貼程式碼了。

<ViewStub
    android:id="@+id/view_stub"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout="@layout/viewstub"/>
複製程式碼

程式碼中:

View inflate = binding.viewStub.getViewStub().inflate();
複製程式碼

inflate即為替代ViewStubView.

至於include更簡單,用法跟以前是差不多,唯一不同的是可以將ViewModel傳到下一個XML中:

<include layout="@layout/layout_include" bind:student="@{student}"/>
複製程式碼

layout_include中同樣可以共享student這個物件。

3. BindingAdapter的使用

我們之前用的都是Android自帶的監聽或是屬性,比如textonClick,但是如果專案中需要動態改變ImageView的內容,那我們應該怎麼辦呢?dataBinding給我們提供了BindingAdapter這個註解,方便我們定義自定義的屬性。 假如我們有個需求,點選按鈕更換圖片,這個時候我們需要定義靜態的方法:

@BindingAdapter({"url", "name"})
public static void loadImageView(ImageView view, String url, String name) {
    Log.i("xwz--->", url + "\t" + name);
    Glide.with(view.getContext())
         .load(url)
         .into(view);
}
複製程式碼

XML中使用

<ImageView
    android:layout_width="160dp"
    android:layout_height="160dp"
    bind:name="@{student.name}"
    bind:url="@{student.imgUrl}"/>
複製程式碼

這裡有必要解釋一下,靜態方法loadImageView裡第一個引數為作用的View,這裡是ImageView;後面的引數即分別對應於@BindingAdapter裡面的引數。那這裡是怎麼跟View聯絡在一塊呢?我們發現XML中有這樣一行程式碼bind:name="@{student.name}這裡的name對應的的@BindingAdapter註解裡的引數name,並對映於ViewModel中的student.name。當student.name值改變,就會觸發loadImageView方法,從而執行裡面的方法。

bind名稱是任意的定義的,不過要定義對應的名稱空間xmlns:bind="http://schemas.android.com/apk/res-auto"

實現的效果就很簡單了:

bindAdapter.gif

更強大的在於可以覆蓋Android原生的元素設定屬性,比如android:text最常見不過了

@BindingAdapter ("android:text")
public static void setText(TextView view, String text) {
    view.setText(text + "xiaweizi");
    Log.i("xwz--->", text);
}
複製程式碼

XML:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{"測試"}'/>
複製程式碼

這個時候所有設定text的地方字尾全部加上了xiaweizi.

4. @BindingConversion

dataBinding還支援對資料的轉換,或者是型別的轉換

@BindingConversion
public static String addString(String text){
    Log.i("xwz--->", "DemoBindingAdapter:  " + "addString: " + text);
    return text + "xiaweizi";
}
複製程式碼

這個時候會將專案中所有以@{String}方式用到的String字尾全部加上xiaweizi.

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color){
   return new ColorDrawable(color);
}
複製程式碼

XML:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
複製程式碼

這段程式碼的作用在於將int型別的color值,轉換成了ColorDrawable型別.

一些細節

databinding支援一些java的表示式

  • + - * / %
  • 字串的連線"a"+"b"
  • 邏輯和位運算&& || & |
  • 一元運算+ - ! ~
  • 移位 >> >>> <<
  • 比較 == > < >= <=
  • instance of
  • 支援資料型別:character,String,numeric,null
  • 強轉cast
  • 方法的呼叫
  • 成員變數的訪問
  • 陣列訪問
  • 三元表示式? :

簡單例子:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
複製程式碼

dataBinding不支援的Java特性

  • this
  • super
  • new
  • 泛型

dataBinding判空處理

使用??來進行判空操作

android:text="@{user.displayName ?? user.lastName}"
複製程式碼

如果不為空則選擇左側值,否則選擇右側值,類似於:

android:text="@{user.displayName != null ? user.displayName : user.lastName}"
複製程式碼

支援陣列,集合,map

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String&gt;"/>
    <variable name="sparse" type="SparseArray&lt;String&gt;"/>
    <variable name="map" type="Map&lt;String, String&gt;"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
複製程式碼

資源的訪問

dataBinding支援一般語法對資源的訪問:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
複製程式碼

小結

dataBinding主要的作用就在於減少ActivityFragment層的程式碼,不再使用findViewById,讓XML從之前只用於顯示檢視,到現在可以做一些操作。在效能上更是有很大的提高,內部採用0反射,使用位標記檢測需要更新的view,每次資料的改變是在下一幀開始改變等等。

當然也有一些不足之處,Android StudioIDE支援還不是那麼完善,在XML中一些方法不能智慧生成和跳轉,還有就是報錯的錯誤資訊,有的時候並不能定位到準確的位置。不過總體上來說dataBinding帶來的好處遠遠的超過這些不足,所以還沒有嘗試的小夥伴,不妨試一試,相信你會愛上他的。

感謝dataBinding視訊 markzhai

相關文章