*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布
什么是MVVM
說到DataBinding,就有必要先提起MVVM設計模式。
Model–View–ViewModel(MVVM) 是一個軟件架構設計模式,相比MVVM,大家對MVC或MVP可能會更加熟悉。
- MVC:(VIew-Model-Controller)
早期將VIew、Model、Controller代碼塊進行劃分,使得程序大部分分離,降低耦合。 - MVP:(VIew-Model-Presenter)由于MVC中View和Model之間的依賴太強,導致Activity中的代碼過于臃腫。為了他們可以絕對獨立的存在,慢慢演化出了MVP。在MVP中View并不直接使用Model,它們之間的通信是通過 Presenter (MVC中的Controller)來進行的。
- MVVM:(Model–View–ViewModel)
MVVM可以算是MVP的升級版,將 Presenter 改名為 ViewModel。關鍵在于View和Model的雙向綁定,當View有用戶輸入后,ViewModel通知Model更新數據,同理Model數據更新后,ViewModel通知View更新。
Data Binding
在Google I/O 2015上,伴隨著Android M預覽版發布的Data Binding兼容函數庫。
不知道要扯什么了,還是直接上代碼,來看看Data Binding的魅力吧。
-
環境要求
Data Binding對使用的環境還是有一定要求的(這貨有點挑)
Android Studio版本在1.3以上
gradle的版本要在1.5.0-alpha1以上
需要在Android SDK manager中下載Android Support repository
然后在對應的Module的build.gradle中添加
android {
....
dataBinding {
enabled =true
}
}
Gradle需要升級版本的可以參考升級Gradle版本
-
創建對象
創建一個User類
public class User {
private String firstName;
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
-
布局
在activity_main.xml中布局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.example.gavin.databindingtest.User"/>
<variable
name="user"
type="User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:textSize="20sp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:textSize="25sp" />
</LinearLayout>
</layout>
這里跟平時的布局有點不同,最外層是layout,里面分別是是data以及我們的布局。
data:聲明了需要用到的user對象,type用于是定路徑。
可以在TextView中的看到android:text="@{user.firstName}", 這是什么鬼,沒見過這么寫的!!!
(不急,繼續往下看)
-
綁定數據
看看下面的MainActivity
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Micheal", "Jack");
binding.setUser(user);
}
}
問我ActivityMainBinding哪來的?我怎么知道...
ActivityMainBinding是根據布局文件的名字生成的,在后面加了Binding。
運行下看看效果吧
有點懵逼了,就綁定了下而已,這些數據是怎么顯示到界面上的。
他是怎么工作的?
原來Data Binding 在程序代碼正在編譯的時候,找到所有它需要的信息。然后通過語法來解析這些表達式,最后生成一個類。
通過反編譯我們可以看到,Data Binding為我們生成了databinding包,以及ActivityMainBinding類(反編譯可以參考這里)
看看我們在onCreate中最后調用的binding.setUser(user),在ActivityMainBinding中可以看到這個方法。
setUser方法
我想就是這個 super.requestRebind()對數據進行了綁定,至于里面怎么實現的,有待進一步研究。
更多用法
上面只是用一個簡單的例子,展示了Data Binding的用法,如果想在實際項目中使用,可不是上面這例子可以搞定的。下面就來說說Data Bindig的更多用法。
-
首先消除下大家對空指針的顧慮
自動生成的 DataBinding 代碼會檢查null,避免出現NullPointerException。
例如在表達式中@{user.phone}如果user == null 那么會為user.phone設置默認值null而不會導致程序崩潰(基本類型將賦予默認值如int為0,引用類型都會賦值null)
-
自定義DataBinding名
如果不喜歡自動生成的Data Binding名,我們可以自己來定義
<data class="MainBinding">
....
</data>
class對應的就是生成的Data Binding名
-
導包
跟Java中的用法相似,布局文件中支持import的使用,原來的代碼是這樣
<data>
<variable name="user" type="com.example.gavin.databindingtest.User" />
</data>
使用import后可以寫成這樣:
<data>
<import type="com.example.gavin.databindingtest.User"/>
<variable
name="user"
type="User" />
</data>
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
遇到相同的類名的時候:
<data>
<import type="com.example.gavin.databindingtest.User" alias="User"/>
<import type="com.example.gavin.mc.User" alias="mcUser"/>
<variable name="user" type="User"/>
<variable name="mcUser" type="mcUser"/>
</data>
使用alias設置別名,這樣user對應的就是com.example.gavin.databindingtest.User,mcUser就對應com.example.gavin.mc.User,然后
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
當需要用到一些包時,在Java中可以自動導包,不過在布局文件中就沒有這么方便了。需要使用import導入這些包,才能使用。如,需要用到View的時候
<data>
<import type="android.view.View"/>
</data>
...
<TextView
...
android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
/>
注意:只要是在Java中需要導入包的類,這邊都需要導入,如:Map、ArrayList等,不過java.lang包里的類是可以不用導包的
-
表達式
在布局中,不僅可以使用
android:text="@{user.lastName}"
還可以使用表達式如:
三元運算
在User中添加boolean類型的isStudent屬性,用來判斷是否為學生。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.isStudent? "Student": "Other"}'
android:textSize="30sp"/>
注意:需要用到雙引號的時候,外層的雙引號改成單引號。
還可以這樣用
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="學生"
android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
android:textSize="30sp"/>
這里用到的View需要在data中聲明
<data>
<import type="android.view.View"/>
</data>
注意:android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}",可能會被標記成紅色,不用管它編譯會通過的
??
除了常用的操作法,另外還提供了一個 null 的合并運算符號 ??,這是一個三目運算符的簡便寫法。
contact.lastName ?? contact.name
相當于
contact.lastName != null ? contact.lastName : contact.name
所支持的操作符如下:
數學運算符 + - / * %
字符串拼接 +
邏輯運算 && ||
二進制運算 & | ^
一元運算符 + - ! ~
位運算符 >> >>> <<
比較運算符 == > < >= <=
instanceof
Grouping ()
文字 - character, String, numeric, null
類型轉換 cast
方法調用 methods call
字段使用 field access
數組使用 [] Arrary access
三元運算符 ? :
-
顯示圖片
除了文字的設置,網絡圖片的顯示也是我們常用的。來看看Data Binding是怎么實現圖片的加載的。
首先要提到BindingAdapter注解,這里創建了一個類,里面有顯示圖片的方法。
public class ImageUtil {
/**
* 使用ImageLoader顯示圖片
* @param imageView
* @param url
*/
@BindingAdapter({"bind:image"})
public static void imageLoader(ImageView imageView, String url) {
ImageLoader.getInstance().displayImage(url, imageView);
}
}
(這方法必須是public static的,否則會報錯)
這里只用了bind聲明了一個image自定義屬性,等下在布局中會用到。
這個類中只有一個靜態方法imageLoader,里面有兩參數,一個是需要設置圖片的view,另一個是對應的Url,這里使用了ImageLoader庫加載圖片。
看看吧它的布局是什么樣的吧
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data >
<variable
name="imageUrl"
type="String"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image = "@{imageUrl}"/>
</LinearLayout>
</layout>
最后在MainActivity中綁定下數據就可以了
binding.setImageUrl(
"http://115.159.198.162:3000/posts/57355a92d9ca741017a28375/1467250338739.jpg");
哇靠!!!就這樣?我都沒看出來它是怎么設置這些圖片的。
不管了,先看看效果。(其中的原理以后慢慢嘮,這里就負責說明怎么使用,這篇已經夠長了,不想再寫了)
使用BindingAdapter的時候,我這還出現了這樣的提示,不過不影響運行。不知道你們會不會...
【已解決】
感謝顏路同學指出@BindingAdapter({"bind:image"}) 改成@BindingAdapter({"image"}) 就不會有警告了
-
點擊事件
在MainActivity中聲明方法:
//參數View必須有,必須是public,參數View不能改成對應的控件,只能是View,否則編譯不通過
public void onClick(View view) {
Toast.makeText(this,"點擊事件", Toast.LENGTH_LONG).show();
}
布局中:
<data>
...
<variable
name="mainActivity"
type="com.example.gavin.databindingtest.MainActivity"/>
</data>
....
<Button
...
android:onClick="@{mainActivity.onClick}"
/>
最后記得在MainActivity中調用
binding.setMainActivity(this);
(發現:布局文件中,variable中的name,在binding中都會生成一個對應的set方法,如:setMainActivity。有set方法,那就應該有get方法,試試getMainActivity,還真有)
運行下看看效果
當然如果你不想吧點擊事件寫在MainActivity中,你把它單獨寫在一個類里面:
public class MyHandler {
public void onClick(View view) {
Toast.makeText(view.getContext(), "點擊事件", Toast.LENGTH_LONG).show();
}
}
<data>
...
<variable
name="handle"
type="com.example.gavin.databindingtest.MyHandler"/>
</data>
....
<Button
...
android:onClick="@{handle.onClick}"
/>
</data>
在MainActivity調用
binding.setHandle(new MyHandler());
-
調用Activity中的變量
上面看到它調用MainActivity中的onClick方法,那么可以調用MainActivity中的屬性嗎?
在MainActivity中定義mName,
public static String mName = "MM";
布局中
<data>
...
<variable
name="mainActivity"
type="com.example.gavin.databindingtest.MainActivity"/>
</data>
<Button
...
android:text="@{mainActivity.mName}"
/>
注意:這個變量必須是public static
-
數據改變時更新UI
當數據發生變化時,我們可以這樣更新UI
private ActivityMainBinding binding;
private User user;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
user = new User("Micheal", "Jack");
binding.setUser(user);
binding.setHandle(new MyHandler());
delay();
}
/**
* 兩秒后改變firstName
*/
private void delay() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
user.setFirstName("Com");
binding.setUser(user);
}
}, 2000);
}
看看調用的這個setUser是什么:
從反編譯的代碼中可以看出,setUser方法中重新綁定了數據。
看下效果
-
BaseObservable
使用上面的代碼實現了UI的更新你就滿足了?其實官方為我們提供了更加簡便的方式,使User繼承BaseObservable,代碼如下
public class User extends BaseObservable {
private String firstName;
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
}
只要user發生變化,就能達到改變UI的效果。在MainActivity中只要調用以下代碼
user.setFirstName("Com");
有了BaseObservable就夠了?不不不,我比較懶,不想寫那么多@Bindable和notifyPropertyChanged。萬一里面有幾十個屬性,那不寫哭起來?而且還有可能寫丟了。
Data Binding的開發者貼心得為我們準備了一系列的ObservableField,包括: ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat,ObservableDouble, 以及 ObservableParcelable看看它們的用法
ObservableField的使用
1、創建User2
public class User2 {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
public final ObservableBoolean isStudent = new ObservableBoolean();
}
這類里面沒有Get/Set。
2、布局文件
<TextView
...
android:text="@{user2.firstName}" />
<TextView
...
android:text="@{user2.lastName}" />
<TextView
...
android:text="@{String.valueOf(user2.age)}"
/>
3、MainActivity中
mUser2 = new User2();
binding.setUser2(mUser2);
mUser2.firstName.set("Mr");
mUser2.lastName.set("Bean");
mUser2.age.set(20);
mUser2.isStudent.set(false);
這里new了一個User2對象后,直接就綁定了。之后只要mUser2中的數據發生變化,UI也會隨之更新。
除了這幾個Map跟List也是必不可少的,Data Binding為我們提供了 ObservableArrayMap和ObservableArrayList。
ObservableArrayMap的使用
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text='@{user["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user["age"])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
ObservableArrayList的使用
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在布局中使用中文時,編譯無法通過。
android:text='@{user2.isStudent?"學生":"非學生"}'
感謝呂檀溪同學的解決方案:
這是java環境的問題,在系統環境變量中增加一個變量,變量名為: JAVA_TOOL_OPTIONS, 變量值為:-Dfile.encoding=UTF-8,保存。要重啟一次電腦,中文就解決了,但是在某些地方,編譯的時候控制臺會出現部分亂
-
在RecyclerView或ListView中使用
前面說了那么多基礎的用法,可還是不能達到我們的需求。幾乎在每個app中都有列表的存在,RecyclerView或ListView,從上面所說的似乎還看不出Data Binding在RecyclerView或ListView中是否也能起作用。(用屁股想也知道,Google的開發團對怎么可能會犯這么低級的錯誤)。下面以RecyclerView為例子:
1、直接看Item的布局(user_item.xml):
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user2"
type="com.example.gavin.databindingtest.User2" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user2.firstName}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="·"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user2.lastName}"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user2.age+""}'/>
</LinearLayout>
</layout>
2、RecyclerView的數據綁定是在Adapter中完成的,下面看看Adapter(這里使用了一個Adapter,如果你在使用的時候發現RecyclerView的動畫沒了,去這里尋找答案)
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyHolder> {
private List<User2> mData = new ArrayList<>();
public MyAdapter(List<User2> data) {
this.mData = data;
}
@Override
public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return MyHolder.create(LayoutInflater.from(parent.getContext()), parent);
}
@Override
public void onBindViewHolder(MyHolder holder, int position) {
holder.bindTo(mData.get(position));
}
@Override
public int getItemCount() {
if (mData == null)
return 0;
return mData.size();
}
static class MyHolder extends RecyclerView.ViewHolder {
private UserItemBinding mBinding;
static MyHolder create(LayoutInflater inflater, ViewGroup parent) {
UserItemBinding binding = UserItemBinding.inflate(inflater, parent, false);
return new MyHolder(binding);
}
private MyHolder(UserItemBinding binding) {
super(binding.getRoot());
this.mBinding = binding;
}
public void bindTo(User2 user) {
mBinding.setUser2(user);
mBinding.executePendingBindings();
}
}
}
3、最后在布局和MainActivity中的使用跟平時的用法一樣
布局中加入RecyclerView:
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
MainActivity中:
List<User2> data = new ArrayList<>();
for (int i = 0; i < 20; i++) {
User2 user2 = new User2();
user2.age.set(30);
user2.firstName.set("Micheal " + i);
user2.lastName.set("Jack " + i);
data.add(user2);
}
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(
this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(new MyAdapter(data));
這樣就可以了。
不過,在自動生成的ActivityMainBinding中,我們可以看到根據RecyclerView的id,會自動生成一個recyclerView。
所以在MainActivity中,我們可以不用findViewById,直接使用binding.recyclerView。
LinearLayoutManager layoutManager = new LinearLayoutManager(
this, LinearLayoutManager.VERTICAL, false);
binding.recyclerView.setLayoutManager(layoutManager);
binding.recyclerView.setAdapter(new MyAdapter(data));
來看看效果吧:
Tips:
-
1:若需要顯示int類型,需要加上"":如
user.age為int類型,需要這樣用
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{""+user.age}'/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(user.age)}"/>
-
2:不建議新手使用,出現錯誤的時候根據提示,不容易找到出錯位置。(是根本找不到...)
參考
Google官方(權威,不過全英文。點擊事件寫的好像不對,后來去其他地方查的):
Realm(十分全面):
CSDN-亓斌(有點像google文檔的翻譯版,整體結果相似):
陽春面的博客(好奇怪的名字)
源碼地址https://github.com/Gavin-ZYX/DataBindingTest
以上有錯誤之處感謝指出