C# 很早就有了MVVM的開發(fā)模式,Android手機中的MVVM一直到去年Google的I\O大會上才推出,姍姍來遲。MVVM這中開發(fā)模式的優(yōu)點自不必多說,可以實現(xiàn)視圖和邏輯代碼的解耦,而且,按照Google的說法,使用了MVVM的開發(fā)模式,還可以提高布局文件的解析速度,個人覺得這一點非常重要。我們在安卓開發(fā)中經(jīng)常需要寫很多個findViewById,讓人心煩,很多人不想寫這個于是用了一些注解框架,可是注解框架無論性能多好,效率總是要低于findViewById的,因此,Android中的MVVM也即databinding可以幫助我們徹底解決這個問題。OK,廢話不多說,我們來看看具體要怎么在Android開發(fā)中使用MVVM。
在低版本的AndroidStudio中使用DataBinding稍微有點麻煩,這里不做介紹。我這里以AndroidStuido2.1為例來介紹DataBinding。本文主要包含以下幾方面內容:
1.基本使用
2.綁定ImageView
3.綁定ListView
4.點擊事件處理
5.數(shù)據(jù)更新處理
好了,那就開始吧!
創(chuàng)建好一個Android Project之后,在gradle文件中添加如下幾行代碼,表示開啟databinding:
[java]view plaincopy
android?{
...
...
...
dataBinding{
enabled true
? ? ? ?}
}
就是這么簡單,一個簡單的databinding配置之后,就可以開始使用數(shù)據(jù)綁定了。
要使用數(shù)據(jù)綁定,我們得首先創(chuàng)建一個實體類,比如User實體類,如下:
[java]view plaincopy
public class UserEntity?{
private String?username;
private String?nickname;
private int age;
public User Entity()?{
}
public int getAge()?{
return age;
}
public void setAge(intage)?{
this.age?=?age;
}
public String?getNickname()?{
return nickname;
}
public void setNickname(String?nickname)?{
this.nickname?=?nickname;
}
public String?getUsername()?{
return username;
}
public void setUsername(String?username)?{
this.username?=?username;
}
public UserEntity(intage,?String?nickname,?String?username)?{
this.age?=?age;
this.nickname?=?nickname;
this.username?=?username;
}
}
然后我們來看看布局文件該怎么寫,首先布局文件不再是以傳統(tǒng)的某一個容器作為根節(jié)點,而是使用作為根節(jié)點,在節(jié)點中我們可以通過節(jié)點來引入我們要使用的數(shù)據(jù)源,如下:
[java]view plaincopy
xmlns:android="http://schemas.android.com/apk/res/android"
>
?name="user"
?type="org.lenve.databinding1.UserEntity"/>
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="org.lenve.databinding1.MainActivity">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.username}"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.nickname}"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(user.age)}"/>
在data中定義的variable節(jié)點,name屬性表示變量的名稱,type表示這個變量的類型,實例就是我們實體類的位置,當然,這里你也可以換一種寫法,如下:
[java]view plaincopy
name="user"
type="UserEntity"/>
先使用import節(jié)點將UserEntity導入,然后直接使用即可。但是如果這樣的話又會有另外一個問題,假如我有兩個類都是UserEntity,這兩個UserEntity分屬于不同的包中,又該如何?看下面:
[java]view plaincopy
name="user"
type="Lenve"/>
在import節(jié)點中還有一個屬性叫做alias,這個屬性表示我可以給該類取一個別名,我給UserEntity這個實體類取一個別名叫做Lenve,這樣我就可以在variable節(jié)點中直接寫Lenve了。
看完data節(jié)點我們再來看看布局文件,TextView的text屬性被我直接設置為了@{user.username},這樣,該TextView一會直接將UserEntity實體類的username屬性的值顯示出來,對于顯示age的TextView,我用了String.valueOf來顯示,因為大家知道TextView并不能直接顯示int型數(shù)據(jù),所以需要一個簡單的轉換,事實上,我們還可以在{}里邊進行一些簡單的運算,這些我一會再說。
最后,我們來看看Activity中該怎么寫,setContentView方法不能夠再像以前那樣來寫了,換成下面的方式:
[java]view plaincopy
DataBindingUtil.setContentView(this,?R.layout.activity_main)
該方法有一個返回值,這個返回值就是系統(tǒng)根據(jù)我們的activity_main.xml布局生成的一個ViewModel類,所以完整寫法如下:
[java]view plaincopy
ActivityMainBinding?activityMainBinding?=?DataBindingUtil.setContentView(this,?R.layout.activity_main);
有了ViewModel,再把數(shù)據(jù)綁定上去就可以了,如下:
[java]view plaincopy
@Override
protectedvoidonCreate(Bundle?savedInstanceState)?{
super.onCreate(savedInstanceState);
ActivityMainBinding?activityMainBinding?=?DataBindingUtil.setContentView(this,?R.layout.activity_main);
UserEntity?user?=newUserEntity();
user.setAge(34);
user.setUsername("zhangsan");
user.setNickname("張三");
activityMainBinding.setUser(user);
}
運行,顯示效果如下:
OK,那我們剛才還說到可以在@{}進行簡單的計算,都有哪些計算呢?我們來看看:
[java]view plaincopy
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.username??user.nickname}"/>
兩個??表示如果username屬性為null則顯示nickname屬性,否則顯示username屬性。
[java]view plaincopy
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`username?is?:`+user.username}"/>
大家注意,這里的字符拼接不是用單引號哦,用的是ESC按鍵下面那個按鍵按出來的。目前DataBinding中的字符拼接還不支持中文。
[java]view plaincopy
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@{user.age?<?30???0xFF0000FF:0xFFFF0000}"
android:text="@{String.valueOf(user.age)}"/>
我在這里給TextView設置背景的時候,做了一個簡單的判斷,如果用戶的年齡小于30,背景就顯示為藍色,否則背景就顯示為紅色,DataBinding里支持小于號但是不支持大于號,索性,大于小于號我都用轉義字符來表示。
另外,DataBinding對于基本的四則運算、邏輯與、邏輯或、取反位移等都是支持的,我這里不再舉例。
OK,上文只是一個簡單的綁定文本,下面我們來看看怎么樣綁定圖片,這里我們還得介紹DataBinding的另一項新功能,就是關于DataBinding自定義屬性的問題,事實上,在我們使用DataBinding的時候,可以給一個控件自定義一個屬性,比如我們下面即將說的這個綁定ImageView的案例。假設我現(xiàn)在想要通過Picasso顯示一張網(wǎng)絡圖片,正常情況下這個顯示很簡單,可是如果我要通過DataBinding來實現(xiàn),該怎么做呢?我們可以使用
[java]view plaincopy
@BindingAdapter
注解來創(chuàng)建一個自定義屬性,同時還要有一個配套的注解的方法。當我們在布局文件中使用這個自定義屬性的時候,會觸發(fā)這個被我們注解的方法,這樣說大家可能還有一點模糊,我們來看看新的實體類:
[java]view plaincopy
/**
*?Created?by?王松?on?2016/7/31.
*/
publicclassUser?{
privateString?username;
privateString?userface;
publicUser()?{
}
publicUser(String?userface,?String?username)?{
this.userface?=?userface;
this.username?=?username;
}
@BindingAdapter("bind:userface")
publicstaticvoidgetInternetImage(ImageView?iv,?String?userface)?{
Picasso.with(iv.getContext()).load(userface).into(iv);
}
publicString?getUserface()?{
returnuserface;
}
publicvoidsetUserface(String?userface)?{
this.userface?=?userface;
}
publicString?getUsername()?{
returnusername;
}
publicvoidsetUsername(String?username)?{
this.username?=?username;
}
}
新類里邊只有兩個屬性,分別是用戶名和用戶圖像,用戶圖像中存儲的實際上是一個網(wǎng)絡圖片地址,這里除了基本的get/set方法之外還多了一個叫做getInternetImage的網(wǎng)絡方法,這個方法有一個注解@BindAdapter("bind:userface"),該注解表示當用戶在ImageView中使用自定義屬性userface的時候,會觸發(fā)這個方法,我在這個方法中來為這個ImageView加載一張圖片,這里有一點需要注意,就是該方法必須為靜態(tài)方法。OK,我們再來看看這次的布局文件:
[java]view plaincopy
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
name="user"
type="org.lenve.databinding2.User"/>
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="org.lenve.databinding2.MainActivity">
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:userface="@{user.userface}">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.username}"/>
大家注意我在ImageView控件中使用userface屬性的時候,使用的前綴不是android而是app哦。再來看看Activity中的代碼:
[java]view plaincopy
@Override
protectedvoidonCreate(Bundle?savedInstanceState)?{
super.onCreate(savedInstanceState);
ActivityMainBinding?dataBinding?=?DataBindingUtil.setContentView(this,?R.layout.activity_main);
dataBinding.setUser(newUser("http://img2.cache.netease.com/auto/2016/7/28/201607282215432cd8a.jpg","張三"));
}
就是這么簡單,加上網(wǎng)絡權限就可以運行了,運行效果如下:
好了,看完了簡單使用之后,不知道你有沒有喜歡上DataBinding,如果還沒有,那就再來看看使用DataBinding來給ListView綁定數(shù)據(jù)吧,這個你一定會喜歡上的。因為使用這中方式來綁定太簡單了。
先來看看我們要做的效果吧:
就是一個ListView,左邊顯示圖片,右邊顯示文本,這樣一個效果。OK,那就一步一步來吧,先是主布局:
[java]view plaincopy
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.lenve.databinding3.MainActivity">
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="match_parent">
主布局很簡單,就是一個ListView,再來看看ListView的item布局:
[java]view plaincopy
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
name="food"
type="org.lenve.databinding3.Food"/>
android:layout_width="match_parent"
android:layout_height="96dp"
android:orientation="vertical">
android:id="@+id/iv"
android:layout_width="96dp"
android:layout_height="96dp"
android:padding="6dp"
app:img="@{food.img}"/>
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_toRightOf="@id/iv"
android:ellipsize="end"
android:maxLines="3"
android:text="@{food.description}"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_toRightOf="@id/iv"
android:layout_alignParentBottom="true"
android:layout_marginBottom="2dp"
android:text="@{food.keywords}"
android:textStyle="bold"/>
圖片加載、文本加載前兩節(jié)都已經(jīng)說過了,這里的東西就沒有什么難度了,我們再來看看實體類Food:
[java]view plaincopy
/**
*?Created?by?王松?on?2016/7/31.
*/
publicclassFood?{
privateString?description;
privateString?img;
privateString?keywords;
privateString?summary;
publicFood()?{
}
publicFood(String?description,?String?img,?String?keywords,?String?summary)?{
this.description?=?description;
this.img?=?img;
this.keywords?=?keywords;
this.summary?=?summary;
}
@BindingAdapter("bind:img")
publicstaticvoidloadInternetImage(ImageView?iv,?String?img)?{
Picasso.with(iv.getContext()).load(img).into(iv);
}
publicString?getDescription()?{
returndescription;
}
publicvoidsetDescription(String?description)?{
this.description?=?description;
}
publicString?getImg()?{
returnimg;
}
publicvoidsetImg(String?img)?{
this.img?=?img;
}
publicString?getKeywords()?{
returnkeywords;
}
publicvoidsetKeywords(String?keywords)?{
this.keywords?=?keywords;
}
publicString?getSummary()?{
returnsummary;
}
publicvoidsetSummary(String?summary)?{
this.summary?=?summary;
}
}
這個實體類中有一個加載圖片的方法,加載方式我們上文都已經(jīng)介紹過了,不多說。好了,再來看看我們的終極Adapter類:
[java]view plaincopy
/**
*?Created?by?王松?on?2016/7/31.
*/
publicclassMyBaseAdapterextendsBaseAdapter?{
privateContext?context;
privateLayoutInflater?inflater;
privateintlayoutId;
privateintvariableId;
privateList?list;
publicMyBaseAdapter(Context?context,intlayoutId,?List?list,intresId)?{
this.context?=?context;
this.layoutId?=?layoutId;
this.list?=?list;
this.variableId?=?resId;
inflater?=?LayoutInflater.from(context);
}
@Override
publicintgetCount()?{
returnlist.size();
}
@Override
publicObject?getItem(intposition)?{
returnlist.get(position);
}
@Override
publiclonggetItemId(intposition)?{
returnposition;
}
@Override
publicView?getView(intposition,?View?convertView,?ViewGroup?parent)?{
ViewDataBinding?dataBinding;
if(convertView?==null)?{
dataBinding?=?DataBindingUtil.inflate(inflater,?layoutId,?parent,false);
}else{
dataBinding?=?DataBindingUtil.getBinding(convertView);
}
dataBinding.setVariable(variableId,?list.get(position));
returndataBinding.getRoot();
}
}
這個大概算是Adapter的終極寫法了,如果你按這種方式來寫Adapter,那么如果沒有非常奇葩的需求,你這個App中可能就只有這一個給ListView使用的Adapter了,為什么這么說呢?因為這個Adapter中沒有一個變量和我們的ListView沾邊,解釋一下幾個變量吧:layoutId這個表示item布局的資源id,variableId是系統(tǒng)自動生成的,根據(jù)我們的實體類,直接從外部傳入即可。另外注意布局加載方式為DataBindingUtil類中的inflate方法。OK,最后再來看看Activity:
[java]view plaincopy
publicclassMainActivityextendsAppCompatActivity?{
privateHandler?mHandler?=newHandler(){
@Override
publicvoidhandleMessage(Message?msg)?{
MyBaseAdapter?adapter?=newMyBaseAdapter<>(MainActivity.this,?R.layout.listview_item,?foods,?org.lenve.databinding3.BR.food);
lv.setAdapter(adapter);
}
};
privateList?foods;
privateListView?lv;
@Override
protectedvoidonCreate(Bundle?savedInstanceState)?{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
lv?=?((ListView)?findViewById(R.id.lv));
initData();
}
privatevoidinitData()?{
OkHttpClient?client?=newOkHttpClient.Builder().build();
Request?request?=newRequest.Builder().url("http://www.tngou.net/api/food/list?id=1").build();
client.newCall(request).enqueue(newCallback()?{
@Override
publicvoidonFailure(Call?call,?IOException?e)?{
}
@Override
publicvoidonResponse(Call?call,?Response?response)throwsIOException?{
if(response.isSuccessful())?{
parseJson(response.body().string());
}
}
});
}
privatevoidparseJson(String?jsonStr)?{
foods?=newArrayList<>();
try{
JSONObject?jo?=newJSONObject(jsonStr);
JSONArray?tngou?=?jo.getJSONArray("tngou");
for(inti?=0;?i?<?tngou.length();?i++)?{
JSONObject?item?=?tngou.getJSONObject(i);
String?description?=?item.getString("description");
String?img?="http://tnfs.tngou.net/image"+item.getString("img");
String?keywords?="【關鍵詞】?"+item.getString("keywords");
String?summary?=?item.getString("summary");
foods.add(newFood(description,?img,?keywords,?summary));
}
mHandler.sendEmptyMessage(0);
}catch(JSONException?e)?{
e.printStackTrace();
}
}
}
OkHttp下載數(shù)據(jù)和Json解析自不用多說,在構造MyAdapter的時候傳入的最后一個參數(shù),是BR中的,這個BR和我們項目中的R文件類似,都是系統(tǒng)自動生成的。
至此,我們使用DataBinding的方式來給ListView加載數(shù)據(jù)就算完成了。so easy~~~
如果你使用DataBinding,我們的點擊事件也會有新的處理方式,首先以ListView為例來說說如何綁定點擊事件,在listview_item布局文件中每一個item的根節(jié)點添加如下代碼:
[java]view plaincopy
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
....
....
android:layout_width="match_parent"
android:layout_height="96dp"
android:onClick="@{food.onItemClick}"
android:orientation="vertical">
android:id="@+id/iv"
android:layout_width="96dp"
android:layout_height="96dp"
android:padding="6dp"
app:img="@{food.img}"/>
....
....
....
OK,我給RelativeLayout容器添了onClick屬性,屬性的值為food.onItemClick,那么這個onItemClick到底是什么呢?其實就是在實體類Food中定義的一個方法,如下:
[java]view plaincopy
publicvoidonItemClick(View?view)?{
Toast.makeText(view.getContext(),?getDescription(),?Toast.LENGTH_SHORT).show();
}
點擊item獲取當前position的數(shù)據(jù),獲取方式也是非常簡單,直接get方法獲取即可,比傳統(tǒng)的ListView的點擊事件通過position來獲取數(shù)據(jù)方便多了。如果我想為關鍵字這個TextView添加點擊事件也很簡單,和上面一樣,這里我就不再貼代碼了,文末可以下載源碼。
單純的更新Food對象并不能改變ListView的UI顯示效果,那該怎么做呢?Google給我們提供了三種解決方案,分別如下:
讓實體類繼承自BaseObservable,然后給需要改變的字段的get方法添加上@Bindable注解,然后給需要改變的字段的set方法加上notifyPropertyChanged(org.lenve.databinding3.BR.description);一句即可,比如我想點擊item的時候把description字段的數(shù)據(jù)全部改為111,我可以修改Food類變?yōu)橄旅娴臉幼樱?/p>
[java]view plaincopy
publicclassFoodextendsBaseObservable?{
privateString?description;
privateString?img;
privateString?keywords;
privateString?summary;
publicFood()?{
}
publicFood(String?description,?String?img,?String?keywords,?String?summary)?{
this.description?=?description;
this.img?=?img;
this.keywords?=?keywords;
this.summary?=?summary;
}
@BindingAdapter("bind:img")
publicstaticvoidloadInternetImage(ImageView?iv,?String?img)?{
Picasso.with(iv.getContext()).load(img).into(iv);
}
publicvoidonItemClick(View?view)?{
//????????Toast.makeText(view.getContext(),?getDescription(),?Toast.LENGTH_SHORT).show();
setDescription("111");
}
publicvoidclickKeywords(View?view)?{
Toast.makeText(view.getContext(),?getKeywords(),?Toast.LENGTH_SHORT).show();
}
@Bindable
publicString?getDescription()?{
returndescription;
}
publicvoidsetDescription(String?description)?{
this.description?=?description;
notifyPropertyChanged(org.lenve.databinding3.BR.description);
}
publicString?getImg()?{
returnimg;
}
publicvoidsetImg(String?img)?{
this.img?=?img;
}
publicString?getKeywords()?{
returnkeywords;
}
publicvoidsetKeywords(String?keywords)?{
this.keywords?=?keywords;
}
publicString?getSummary()?{
returnsummary;
}
publicvoidsetSummary(String?summary)?{
this.summary?=?summary;
}
}
OK,這是第一種解決方案,也是比較簡單常用的一種。
2.使用DataBinding提供的ObservableFields來創(chuàng)建實體類
這種方式使用起來略微麻煩,除了繼承BaseObservable之外,創(chuàng)建屬性的方式也變成下面這種:
[java]view plaincopy
privatefinalObservableField?description?=newObservableField<>();
屬性的讀寫方式也變了,讀取方式如下:
[java]view plaincopy
description.get()
寫入方式如下:
[java]view plaincopy
this.description.set(description);
OK,依據(jù)上面幾個規(guī)則,我新定義的實體類如下:
[java]view plaincopy
/**
*?Created?by?王松?on?2016/7/31.
*/
publicclassFoodextendsBaseObservable?{
privatefinalObservableField?description?=newObservableField<>();
privatefinalObservableField?img?=newObservableField<>();
privatefinalObservableField?keywords?=newObservableField<>();
privatefinalObservableField?summary?=newObservableField<>();
publicFood()?{
}
publicFood(String?description,?String?img,?String?keywords,?String?summary)?{
this.description.set(description);
this.keywords.set(keywords);
this.img.set(img);
this.summary.set(summary);
}
@BindingAdapter("bind:img")
publicstaticvoidloadInternetImage(ImageView?iv,?String?img)?{
Picasso.with(iv.getContext()).load(img).into(iv);
}
publicvoidonItemClick(View?view)?{
//????????Toast.makeText(view.getContext(),?getDescription(),?Toast.LENGTH_SHORT).show();
setDescription("111");
}
publicvoidclickKeywords(View?view)?{
Toast.makeText(view.getContext(),?getKeywords(),?Toast.LENGTH_SHORT).show();
}
@Bindable
publicString?getDescription()?{
returndescription.get();
}
publicvoidsetDescription(String?description)?{
this.description.set(description);
notifyPropertyChanged(org.lenve.databinding3.BR.description);
}
publicString?getImg()?{
returnimg.get();
}
publicvoidsetImg(String?img)?{
this.img.set(img);
}
publicString?getKeywords()?{
returnkeywords.get();
}
publicvoidsetKeywords(String?keywords)?{
this.keywords.set(keywords);
}
publicString?getSummary()?{
returnsummary.get();
}
publicvoidsetSummary(String?summary)?{
this.summary.set(summary);
}
}
這種方式實現(xiàn)的功能和第一個實體類實現(xiàn)的功能一模一樣。
3.使用DataBinding中提供的集合來存儲數(shù)據(jù)即可
DataBinding中給我們提供了一些現(xiàn)成的集合,用來存儲數(shù)據(jù),比如ObservableArrayList,ObservableArrayMap,因為這些用的少,我這里就不做介紹了。