目錄
MVVM介紹
不習慣簡書界面,可以試試螞蟻筆記生成的博客地址 dataBinding,我的博客地址
本篇這是基礎入門篇,要想看源碼剖析請移步到 DataBinding實現(xiàn)原理探析
MVVM框架類似于早期的MVC和最熱的MVP,但是比起這兩個更為強勢。MV-VM相比于MVP,其實就是將Presenter層替換成了ViewModel層,我們都知道,MVP的好處就是將邏輯代碼從View層抽離出來,做到與UI層的低耦合,但是無形中會創(chuàng)造出許多的接口,有些接口很是冗余,不僅如此,當后期修改數(shù)據(jù)或者添加新的功能還需要修改或是添加接口,很是麻煩。
這個時候MV-VM的優(yōu)勢就體現(xiàn)出來了,ViewModel層所需要做的完全就是跟邏輯相關的代碼,完全不會涉及到UI。當數(shù)據(jù)變化,直接驅動UI的改變,中間省去了冗余的接口。同時,在ViewModel層編寫代碼中,要求開發(fā)者需要將每個方法盡可能的做的功能單一,不與外部有任何的引用或者是聯(lián)系,無形中提高了代碼的健壯性,方便了后期的單元測試。
DataBinding其實就是谷歌出臺的工具,是實現(xiàn)UI和數(shù)據(jù)綁定的框架,View和ViewModel通過DataBinding實現(xiàn)單向綁定或雙向綁定,做到UI和數(shù)據(jù)的相互監(jiān)聽,同時開發(fā)者的任務分配也就很明確了,負責ViewModel的小伙伴完全不用考慮UI如何實現(xiàn),很大程度上提高了代碼的開發(fā)效率和后期出問題跟蹤的準確性,針對這些好處,采用MVVM進行代碼開發(fā)還是非常有必要的。
初步使用
1. module的build.gradle文件加上一行配置代碼
android {
...
dataBinding {
enabled = true
}
}
2. 創(chuàng)建布局文件
只需要在之前布局的基礎上,外層嵌套 <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進行區(qū)分:
<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的反射,采用的是數(shù)組記錄每個view
final Object[] bindings = mapBindings(bindingComponent, root, 8, sIncludes, sViewsWithIds);
在XML創(chuàng)建一個TextView
<TextView
android:id="@+id/tv_content"
android:text="@{student.name}"
android:layout_width="match_parent"
android:layout_height="50dp"/>
在代碼中通過binding直接可以獲取到這個TextView
mBinding.tvContent
那么如何實現(xiàn)單向綁定呢?
Student student = new Student("xiaweizi", 12);
mBinding.setStudent(student);
這樣就可以直接改變TextView的值。
ViewModel就是簡單的數(shù)據(jù)
public class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
b. 雙向綁定
之前說的單向綁定,即當數(shù)據(jù)變化,通過mBinding.setStudent(student)方式驅動UI的改變
而雙向綁定,無論View還是ViewModel誰改變,都會驅動另一方的改變,實現(xiàn)雙向綁定有兩種方式:繼承BaseObservable和使用ObservableField創(chuàng)建成員變量。
代碼實現(xiàn):
第一種繼承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()方法,不僅數(shù)據(jù)改變,UI中的TextView內容也會隨之改變。
我們可以發(fā)現(xiàn)有兩個方法:notifyPropertyChanged()和notifyChange,一個是更新指定的變量,第二個是更新所有該ViewModel中的對象。
而notifyPropertyChanged(int fieldId)里面?zhèn)鞯膮?shù),即上面通過@Bindable注解創(chuàng)建對應的變量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創(chuàng)建的對象作用相當于第一種的方案,支持ObservableInt、ObservableBoolean或者是ObservableField<T>指定的類型、ObservableArrayMap<String, Object>、ObservableArrayList<Object>等。
ObservableField內部已經封裝了get和set方法,如果成員變量是public屬性,直接通過
mStudent.name.set("shabi");
String name = mStudent.name.get();
設置和獲取對應的成員變量的值。
其他使用
學會了上面基本的用戶還是遠遠不夠的,像按鈕的點擊事件或是EditText內容的監(jiān)聽,這些也是非常重要的,不過學會了一種,其他的舉一反三就會容易的多了。
1. 事件處理
dataBinding需要你通過一些表達式來處理view的分發(fā)事件,除了少數(shù)例子外,事件元素的名稱是由監(jiān)聽器中的方法所控制。比如View.OnLongClickListener內部有onLongClick()方法,所以XML定義的事件就為android:onLongClick.
可以直接在Activity內部定義一個類,用于處理事件的監(jiān)聽
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,但是參數(shù)必須是View,參數(shù)要對應于setOnClickListener(onClickListener listener)對應的onClickListener要實現(xiàn)的接口,即public void onClick(View)。
同理,監(jiān)聽EditText文本的變化,一般只要注意onTextChanged(CharSequence s, int start, int before, int count)方法即可,那么我們可以創(chuàng)建與之對應的方法,在XML文件中引用:android:onTextChanged="@{presenter::onTextChanged}"。
最后再來看從UI中獲取數(shù)據(jù),也就是數(shù)據(jù)的回調,即DataBinding的精髓支出,View和ViewModel雙向綁定。android:onClick="@{() -> presenter.onClickListenerBinding(student)}這里用到了lamda表達式,這樣就可以不遵循默認的方法簽名,將student對象直接傳回點擊方法中。來看一下實現(xiàn)效果:

一目了然,我就不贅述了,我們可以發(fā)現(xiàn)一點,一開始我們并沒有給Student對象設置值,所以顯示的是null,并沒有報空指針異常,這也是DataBinding的有點之一。
其實dataBinding自帶對數(shù)據(jù)監(jiān)聽的方法:
<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());
}
});
這個對數(shù)據(jù)的監(jiān)聽建立在,使用@Bindable作為雙向綁定為條件,當數(shù)據(jù)變化,便會出發(fā)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自帶的監(jiān)聽或是屬性,比如text、onClick,但是如果項目中需要動態(tài)改變ImageView的內容,那我們應該怎么辦呢?dataBinding給我們提供了BindingAdapter這個注解,方便我們定義自定義的屬性。
假如我們有個需求,點擊按鈕更換圖片,這個時候我們需要定義靜態(tài)的方法:
@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}"/>
這里有必要解釋一下,靜態(tài)方法loadImageView里第一個參數(shù)為作用的View,這里是ImageView;后面的參數(shù)即分別對應于@BindingAdapter里面的參數(shù)。那這里是怎么跟View聯(lián)系在一塊呢?我們發(fā)現(xiàn)XML中有這樣一行代碼bind:name="@{student.name}這里的name對應的的@BindingAdapter注解里的參數(shù)name,并映射于ViewModel中的student.name。當student.name值改變,就會觸發(fā)loadImageView方法,從而執(zhí)行里面的方法。
bind名稱是任意的定義的,不過要定義對應的命名空間xmlns:bind="http://schemas.android.com/apk/res-auto"。
實現(xiàn)的效果就很簡單了:

更強大的在于可以覆蓋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還支持對數(shù)據(jù)的轉換,或者是類型的轉換
@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類型.
5. DataBindingComponent
通過BindingAdapter是可以增加一些自定義的屬性或者是修改Android原生的屬性,但是它有一個弊端,就是全局修改所有的相關屬性,不過配合上DataBindingComponent就可以解決這個問題。
DataBindingUtil.setContentView(this, R.layout.activity_component, new FirstComponent());
開始的是以一個參數(shù)的形式傳入這個View中,那么只作用于當前的view。
DataBindingUtil.setDefaultComponent(new FirstComponent());
或者是這種設置全局的方式,也可以改變。
DataBindingComponent其實是一個空方法的接口,你需要先創(chuàng)建一個擁有@BindingAdapter的類,這里就不能定義為public,因為這樣DataBindingComponent就找不到對應的類,我們?yōu)榱朔奖愫笃诘拈_發(fā),可以定義一個抽象類:
public abstract class AbstractAdapter {
@BindingAdapter ("text")
public abstract void setText(TextView textView, String text);
}
然后定義一個實現(xiàn)類:
public class FirstAdapter extends AbstractAdapter{
@Override
public void setText(TextView textView, String text) {
Log.i("xwz--->", "FirstAdapter: " + "setText: ");
textView.setText(text+"first");
}
}
這個時候當你創(chuàng)建一個實現(xiàn)DataBindingComponent接口的類時,會發(fā)現(xiàn)讓你實現(xiàn)一個方法:
public class FirstComponent implements android.databinding.DataBindingComponent {
@Override
public AbstractAdapter getAbstractAdapter() {
return new FirstAdapter();
}
}
這里返回的就是創(chuàng)建的adapter,可以根據(jù)需求創(chuàng)建對應的component.
RecyclerView中的應用
除了最基本的使用,還有一個頻繁出現(xiàn)的就是列表了,那么我們這里就拿RecyclerView作為代表進行演示。
RecyclerView的好處就不多說了,已經完全代替了之前的ListView和GridView,用法也就不贅述了,這里主要介紹一下適配器的編寫。雖然網(wǎng)上有很多大神已經幫我們創(chuàng)建了各種通用的adapter,不過作為入門,我們還是要學習一下使用dataBinding創(chuàng)建adapter.
先來個簡單的XML文件:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.github.markzhai.sample.Person"/>
<variable
name="person"
type="Person"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:padding="5dp"
android:textColor="#f0f"
android:text='@{"姓名:" + person.name, default="aaa"}'/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:padding="2dp"
android:textColor="#090"
android:gravity="right"
android:layout_marginRight="80dp"
android:text='@{"年齡:" + person.age, default=12}'/>
</LinearLayout>
</layout>
至于ViewModel就是一個簡單的Person類,擁有name和age兩個屬性,接下來就是adapte的編寫。
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private List<Person> mList;
public MyAdapter() {
mList = new ArrayList<>();
}
public void setData(List<Person> persons) {
this.mList = persons;
}
@Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ItemRecyclerBinding itemBinding =
DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
R.layout.item_recycler,
parent,
false);
return new ViewHolder(itemBinding);
}
@Override
public void onBindViewHolder(MyAdapter.ViewHolder holder, int position) {
holder.bind(mList.get(position));
}
@Override
public int getItemCount() {
return mList.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
final ItemRecyclerBinding itemBinding;
public ViewHolder(ItemRecyclerBinding binding) {
super(binding.getRoot());
this.itemBinding = binding;
}
void bind(Person person) {
itemBinding.setPerson(person);
}
}
}
通過DataBindingUtil.inflate()創(chuàng)建item布局,在ViewHolder中進行數(shù)據(jù)的綁定,這個時候,當數(shù)據(jù)源變化的時候,RecyclerView中的數(shù)據(jù)也跟著變化。
至于item的點擊事件可以上面的onClick寫法:
創(chuàng)建Presenter處理點擊事件:
public static class Presenter{
ItemRecyclerBinding mBinding;
public Presenter(ItemRecyclerBinding binding){
this.mBinding = binding;
}
public void onItemClick(Person person){
Log.i("xwz--->", "name: " + person.getName() + "\tage: " + person.getAge());
Toast.makeText(mBinding.getRoot().getContext(), "name: " + person.getName() + "\tage: " + person.getAge(), Toast.LENGTH_SHORT).show();
}
}
在之前的bind方法中進行綁定:
void bind(Person person) {
itemBinding.setPerson(person);
itemBinding.setPresenter(new Presenter(itemBinding));
}
在XML中設置點擊事件即可:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onItemClick(person)}"
android:orientation="vertical">
...
</LinearLayout>
這里就可以直接將數(shù)據(jù)直接回傳。
看一下運行效果:

一些細節(jié)
databinding支持一些java的表達式
+ - * / %- 字符串的連接
"a"+"b" - 邏輯和位運算
&& || & | - 一元運算
+ - ! ~ - 移位
>> >>> << - 比較
== > < >= <= instance of- 支持數(shù)據(jù)類型:
character,String,numeric,null - 強轉
cast - 方法的調用
- 成員變量的訪問
- 數(shù)組訪問
- 三元表達式
? :
簡單例子:
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}"
支持數(shù)組,集合,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從之前只用于顯示視圖,到現(xiàn)在可以做一些操作。在性能上更是有很大的提高,內部采用0反射,使用位標記檢測需要更新的view,每次數(shù)據(jù)的改變是在下一幀開始改變等等。
當然也有一些不足之處,Android Studio的IDE支持還不是那么完善,在XML中一些方法不能智能生成和跳轉,還有就是報錯的錯誤信息,有的時候并不能定位到準確的位置。不過總體上來說dataBinding帶來的好處遠遠的超過這些不足,所以還沒有嘗試的小伙伴,不妨試一試,相信你會愛上他的。