目錄
MVVM介紹
不習慣簡書界面,可以試試螞蟻筆記生成的博客地址 dataBinding,我的博客地址
本篇這是基礎入門篇,要想看源碼剖析請移步到 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();
設置和獲取對應的成員變量的值。
其他使用
學會了上面基本的用戶還是遠遠不夠的,像按鈕的點擊事件或是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
類型.
5. DataBindingComponent
通過BindingAdapter
是可以增加一些自定義的屬性或者是修改Android
原生的屬性,但是它有一個弊端,就是全局修改所有的相關屬性,不過配合上DataBindingComponent
就可以解決這個問題。
DataBindingUtil.setContentView(this, R.layout.activity_component, new FirstComponent());
開始的是以一個參數的形式傳入這個View
中,那么只作用于當前的view。
DataBindingUtil.setDefaultComponent(new FirstComponent());
或者是這種設置全局的方式,也可以改變。
DataBindingComponent
其實是一個空方法的接口,你需要先創建一個擁有@BindingAdapter
的類,這里就不能定義為public
,因為這樣DataBindingComponent
就找不到對應的類,我們為了方便后期的開發,可以定義一個抽象類:
public abstract class AbstractAdapter {
@BindingAdapter ("text")
public abstract void setText(TextView textView, String text);
}
然后定義一個實現類:
public class FirstAdapter extends AbstractAdapter{
@Override
public void setText(TextView textView, String text) {
Log.i("xwz--->", "FirstAdapter: " + "setText: ");
textView.setText(text+"first");
}
}
這個時候當你創建一個實現DataBindingComponent
接口的類時,會發現讓你實現一個方法:
public class FirstComponent implements android.databinding.DataBindingComponent {
@Override
public AbstractAdapter getAbstractAdapter() {
return new FirstAdapter();
}
}
這里返回的就是創建的adapter
,可以根據需求創建對應的component
.
RecyclerView中的應用
除了最基本的使用,還有一個頻繁出現的就是列表了,那么我們這里就拿RecyclerView
作為代表進行演示。
RecyclerView
的好處就不多說了,已經完全代替了之前的ListView
和GridView
,用法也就不贅述了,這里主要介紹一下適配器的編寫。雖然網上有很多大神已經幫我們創建了各種通用的adapter
,不過作為入門,我們還是要學習一下使用dataBinding
創建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()
創建item
布局,在ViewHolder
中進行數據的綁定,這個時候,當數據源變化的時候,RecyclerView
中的數據也跟著變化。
至于item
的點擊事件可以上面的onClick
寫法:
創建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>
這里就可以直接將數據直接回傳。
看一下運行效果:
一些細節
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
帶來的好處遠遠的超過這些不足,所以還沒有嘗試的小伙伴,不妨試一試,相信你會愛上他的。