目錄
MVVM DataBinding 介紹
MVVM
框架類似於早期的MVC
和最熱的MVP
,但是比起這兩個更為強勢。MV-VM
相比於MVP
,其實就是將Presenter
層替換成了ViewModel
層,我們都知道,MVP
的好處就是將邏輯程式碼從View
層抽離出來,做到與UI層的低耦合,但是無形中會創造出許多的介面,有些介面很是冗餘,不僅如此,當後期修改資料或者新增新的功能還需要修改或是新增介面,很是麻煩。
這個時候MV-VM
的優勢就體現出來了,ViewModel
層所需要做的完全就是跟邏輯相關的程式碼,完全不會涉及到UI。當資料變化,直接驅動UI的改變,中間省去了冗餘的介面。同時,在ViewModel
層編寫程式碼中,要求開發者需要將每個方法儘可能的做的功能單一,不與外部有任何的引用或者是聯絡,無形中提高了程式碼的健壯性,方便了後期的單元測試。
DataBinding
其實就是谷歌出臺的工具,是實現UI和資料繫結的框架,View
和ViewModel
通過DataBinding
實現單向繫結或雙向繫結,做到UI和資料的相互監聽,同時開發者的任務分配也就很明確了,負責ViewModel
的小夥伴完全不用考慮UI如何實現,很大程度上提高了程式碼的開發效率和後期出問題跟蹤的準確性,針對這些好處,採用MVVM
進行程式碼開發還是非常有必要的。
初步使用
1. module
的build.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
建立的物件作用相當於第一種的方案,支援ObservableInt
、ObservableBoolean
或者是ObservableField<T>
指定的型別、ObservableArrayMap<String, Object>
、ObservableArrayList<Object>
等。
ObservableField
內部已經封裝了get
和set
方法,如果成員變數是public
屬性,直接通過
mStudent.name.set("shabi");
String name = mStudent.name.get();
複製程式碼
設定和獲取對應的成員變數的值。
如果是private
,可以自己封裝get
和set
方法,效果一樣。
其他使用
學會了上面基本的使用者還是遠遠不夠的,像按鈕的點選事件或是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
的精髓支出,View
和ViewModel
雙向繫結。android:onClick="@{() -> presenter.onClickListenerBinding(student)}
這裡用到了lamda
表示式,這樣就可以不遵循預設的方法簽名,將student
物件直接傳回點選方法中。來看一下實現效果:
一目瞭然,我就不贅述了,我們可以發現一點,一開始我們並沒有給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. ViewStub
和include
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
即為替代ViewStub
的View
.
至於include
更簡單,用法跟以前是差不多,唯一不同的是可以將ViewModel
傳到下一個XML
中:
<include layout="@layout/layout_include" bind:student="@{student}"/>
複製程式碼
layout_include
中同樣可以共享student
這個物件。
3. BindingAdapter
的使用
我們之前用的都是Android
自帶的監聽或是屬性,比如text
、onClick
,但是如果專案中需要動態改變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"
。
實現的效果就很簡單了:
更強大的在於可以覆蓋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<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<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
主要的作用就在於減少Activity
和Fragment
層的程式碼,不再使用findViewById
,讓XML
從之前只用於顯示檢視,到現在可以做一些操作。在效能上更是有很大的提高,內部採用0反射,使用位標記檢測需要更新的view
,每次資料的改變是在下一幀開始改變等等。
當然也有一些不足之處,Android Studio
的IDE
支援還不是那麼完善,在XML
中一些方法不能智慧生成和跳轉,還有就是報錯的錯誤資訊,有的時候並不能定位到準確的位置。不過總體上來說dataBinding
帶來的好處遠遠的超過這些不足,所以還沒有嘗試的小夥伴,不妨試一試,相信你會愛上他的。