我的更多 android 博文,關(guān)注作者~每周更新一篇 Android干貨博文
http://xuyushi.github.io/archives/
在我的上一篇博文中介紹了Android的MVP模式(http://www.reibang.com/p/f6252719b3af) MVVM 是從 MVP 的進(jìn)一步發(fā)展與規(guī)范,MVP 隔離了 M 與 V 的直接聯(lián)系后拜英,靠 Presenter 來中轉(zhuǎn)棍郎,所以使用 MVP 時(shí) P 是直接調(diào)用 View 的接口來實(shí)現(xiàn)對視圖的操作的命锄,M 與 V是隔離了霸褒,方便測試了膛檀,但代碼還不夠優(yōu)雅簡潔啊馅扣,所以 MVVM 就彌補(bǔ)了這些缺陷斟赚。
概述
MVVM模式包含了三個(gè)部分:
- Model :基本業(yè)務(wù)邏輯
- View :視圖內(nèi)容
ViewModel: 將前面兩者聯(lián)系在一起的對象
當(dāng)View有用戶輸入后,ViewModel通知Model更新數(shù)據(jù)差油,同理Model數(shù)據(jù)更新后拗军,ViewModel通知View更新。
MVP MVVM區(qū)別
可以看到 ViewModel 承擔(dān)了 Presenter 中與 view和 Model 交互的職責(zé)蓄喇,與 MVP模式不同的是发侵,VM與 V 之間是通過 Datebingding 實(shí)現(xiàn)的,而 P是持有 View 的對象妆偏,直接調(diào)用 View 中的一些接口方法來實(shí)現(xiàn)刃鳄。ViewModel可以理解成是View的數(shù)據(jù)模型和Presenter的合體。**它通過雙向綁定(松耦合)解決了MVP中Presenter與View聯(lián)系比較緊密的問題钱骂。 **
環(huán)境搭建
Android 的 Gradle 插件版本不低于 1.5.0-alpha1:
classpath 'com.android.tools.build:gradle:1.5.0'
然后修改對應(yīng)模塊(Module)的 build.gradle:
android {
....
dataBinding {
enabled = true
}
}
注:Android stuido 的版本要大于1.3
Android Studio目前對binding對象沒有自動代碼提示叔锐,只會在編譯時(shí)進(jìn)行檢查。
基礎(chǔ)入門
布局問文件
相比傳統(tǒng)的 xml罐柳,根節(jié)點(diǎn)編程了layout
掌腰,里面包括了data
節(jié)點(diǎn) 和傳統(tǒng)的視圖。data
節(jié)點(diǎn)就像是連接 View 和 Modle 的橋梁张吉。在data
節(jié)點(diǎn)中聲明一個(gè)variable
變量齿梁,使其可以在這個(gè)layout中使用
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user"type="com.example.User"/>
</data>
<!--原先的根節(jié)點(diǎn)(Root Element)-->
<LinearLayout>
....
</LinearLayout>
</layout>
例如在 TextView 中使用
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="@{user.firstName}"/>
數(shù)據(jù)對象
定義一個(gè) User java bean 類
public class User {
private final String firstName;
private final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
在layout
中定義User
中的對象,然后把它跟布局文件中聲明的變量進(jìn)行綁定
定義 Variable
<data>
<variable
name="user"
type="io.github.xuyushi.androidmvvmdemo.User" />
</data>
- 變量名為
user
- 變量類型為
"io.github.xuyushi.androidmvvmdemo.User"
data
也支持 import
<data>
<import type="io.github.xuyushi.androidmvvmdemo.User"/>
<variable
name="user"
type="User" />
</data>
注意坑
import 并不能和 java
一樣可以 import xx.xxx.*
肮蛹,必須具體寫明每個(gè)要導(dǎo)入的類名勺择,如
<import type="io.github.xuyushi.androidmvvmdemo.User"/>
<import type="io.github.xuyushi.androidmvvmdemo.MyHandler"/>
// this is WRONG
<import type="io.github.xuyushi.androidmvvmdemo.*"/>
編譯之后,插件會根據(jù) xml 的命名(activity_main)伦忠,在 output
會生成ActivityMainBinding
類
java.lang.*
包中的類會被自動導(dǎo)入省核,可以直接使用,例如要定義一個(gè) String 類型的變量:
<variable name="firstName" type="String" />
綁定 Variable
修改MainActivity
中的onCreate
昆码,用 DatabindingUtil.setContentView()
來替換掉 setContentView()
气忠,然后創(chuàng)建一個(gè) user
對象邻储,通過 binding.setUser(user)
與 variable 進(jìn)行綁定。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("testFirst", "testLast");
binding.setUser(user);
}
}
如果使用的 ListView
或者RecyclerView
可以使用這個(gè)
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
有時(shí)候不能預(yù)先知道 Bingding 類的種類旧噪,這時(shí)候可以使用DataBindingUtil
類:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
支持的語法
Mathematical + - / * %
String concatenation +
Logical && ||
Binary & | ^
Unary + - ! ~
Shift >> >>> <<
Comparison == > < >= <=
instanceof
Grouping ()
Literals - character, String, numeric, null
Cast
Method calls
Field access
Array access []
Ternary operator ?:
不支持的語法
this
super
new
ActivityMainBinding
類是自動生成的吨娜,所有的 set 方法也是根據(jù) variable 名稱生成的。例如淘钟,我們定義了兩個(gè)變量宦赠。
<data>
<variable name="firstName" type="String" />
<variable name="lastName" type="String" />
</data>
那么會生成兩個(gè) set
方法
setFirstName(String firstName);
setLastName(String lastName);
使用Variable
數(shù)據(jù)與 Variable 綁定之后,xml 的 UI 元素就可以直接使用了
<TextView
android:text="@{user.firstName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
綁定事件
可以直接在 xml 導(dǎo)入android.view.View.OnClickListener
米母,并制定其點(diǎn)擊事件
<variable
name="clickListener"
type="android.view.View.OnClickListener" />
...
android:onClick="@{clickListener}"
...
holder.binding.setClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//do something
});
進(jìn)階用法
使用類的方法
//Error:(27, 29) cannot find method addSomeThing in class io.github.xuyushi.androidmvvmdemo.MyUtill
類的別名
如果導(dǎo)入不同的包中有相同的類名勾扭,使用import
中的 alias
屬性。
<import type="com.example.home.data.User" />
<import type="com.examle.detail.data.User" alias="DetailUser" />
<variable name="user" type="DetailUser" />
數(shù)據(jù)綁定
直接修改數(shù)據(jù)對象并不能直接更新 UI铁瞒,Android的Data Binding模塊給提供了通知機(jī)制妙色,有3種類型,分別對應(yīng)于類(Observable)精拟,字段(ObservableField)燎斩,集合類型(Observable Collections)。
Android的Data Binding模塊給提供了通知機(jī)制蜂绎,有3種類型,分別對應(yīng)于類(Observable)笋鄙,字段(ObservableField)师枣,集合類型(Observable Collections)。
Observable Objects
目前 DataBinding 暫時(shí)只支持單向綁定萧落。
要實(shí)現(xiàn) Observable Binding践美,首先得有一個(gè) implement
了接口android.databinding.Observable
的類,為了方便找岖,Android 原生提供了已經(jīng)封裝好的一個(gè)類 - BaseObservable
陨倡,并且實(shí)現(xiàn)了監(jiān)聽器的注冊機(jī)制
public class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(io.github.xuyushi.androidmvvmdemo.BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(io.github.xuyushi.androidmvvmdemo.BR.lastName);
}
}
The Bindable annotation should be applied to any getter accessor method of an {@link Observable} class. Bindable will generate a field in the BR class to identify the field that has changed.
Bindable
注解是為了在編程的時(shí)候生成 BR 類,Bindable
會在 BR 類中生成一個(gè)域變量 许布,來表明這個(gè)域有木有被改變兴革。通過代碼可以看出,當(dāng)數(shù)據(jù)發(fā)生變化時(shí)還是需要手動發(fā)出通知蜜唾。 通過調(diào)用 notifyPropertyChanged(BR.firstName)
可以通知系統(tǒng) BR.firstName 這個(gè) entry 的數(shù)據(jù)已經(jīng)發(fā)生變化杂曲,需要更新 UI。
ObservableFields
具體到成員變量,這種方式無需繼承 BaseObservable
如果變量比較少袁余,都是簡單的數(shù)據(jù)類型是時(shí)擎勘,可以用ObservableFields
, ObservableFields
自包含具有單個(gè)字段的observable對象颖榜。它有所有基本類型和一個(gè)是引用類型棚饵。要使用它需要在data對象中創(chuàng)建public final
字段:
private static class User extends BaseObservable {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
注意
- 可以在 java bean 中定義煤裙,也可以在 activity 中 或者bind 出定義
- 使用
ObservableFields
在 Model 中的 @Bindable get set 方法都可以去掉 - 當(dāng)
firstName
、lastName
變化時(shí)噪漾,UI 會得到通知积暖,使用的賦值語句為user.firstName.set("Google");
Observable 集合
一些app使用更多的動態(tài)結(jié)構(gòu)來保存數(shù)據(jù)。Observable集合允許鍵控訪問這些data對象怪与。ObservableArrayMap用于鍵是引用類型,如String夺刑。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在layout文件中,通過String鍵可以訪問map
<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"/>
當(dāng) key 是 inter 是分别, 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"/>
binding 生成
binding 類連接了 layout中的variables與Views遍愿。,所生成的Binding類都擴(kuò)展了android.databinding.ViewDataBinding
創(chuàng)建
Binding應(yīng)在inflation之后就立馬創(chuàng)建耘斩,以確保View層次結(jié)構(gòu)沒被改變沼填。
首先 inflate
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(LayoutInflater, viewGroup, false);
帶 ID 的 View
同步 bind 我們可以不需要 view 實(shí)例,但是玩意需要也可以有
<TextView
android:id="@+id/firstName"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
上面代碼中定義了一個(gè) ID 為 firstName
的 TextView括授,那么它對應(yīng)的變量就是
public final TextView firstName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_view_with_ids);
}
public void showMyName(View view) {
binding.firstName.setText("liang");
binding.lastName.setText("fei");
}
這樣就免去了些 findViewById
了
ViewStubs
ViewStubs跟正常的Views略有不同坞笙。他們開始時(shí)是不可見的,當(dāng)他們要么設(shè)置為可見或被明確告知要載入時(shí)荚虚,它們通過載入另外一個(gè)layout取代了自己薛夜。
當(dāng)載入另一個(gè)layout,為新的布局必需創(chuàng)建一個(gè)Binding版述。因此梯澜,ViewStubProxy必需監(jiān)聽ViewStub的OnInflateListener監(jiān)聽器并在那個(gè)時(shí)候建立Binding。因?yàn)橹挥幸粋€(gè)可以存在渴析,ViewStubProxy允許開發(fā)者在其上設(shè)置一個(gè)OnInflateListener它會在建立Binding后調(diào)用晚伙。
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
...>
<ViewStub
android:id="@+id/view_stub"
android:layout="@layout/view_stub"
... />
</LinearLayout>
</layout>
在 Java 代碼中獲取 binding 實(shí)例,ViewStubProy 注冊ViewStub.OnInflateListener 事件:
binding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
ViewStubBinding binding = DataBindingUtil.bind(inflated);
User user = new User("fee", "lang");
binding.setUser(user);
}
});
動態(tài) Variables
有時(shí)候不止具體綁定的對象俭茧,以 RecyclerView
為例咆疗,Adapter 的 DataBinding 需要動態(tài)生成,因此我們可以在 onCreateViewHolder
的時(shí)候創(chuàng)建這個(gè) DataBinding母债,然后在 onBindViewHolder
中獲取這個(gè) DataBinding午磁。
public static class BindingHolder extends RecyclerView.ViewHolder {
private ViewDataBinding binding;
public BindingHolder(View itemView) {
super(itemView);
}
public ViewDataBinding getBinding() {
return binding;
}
public void setBinding(ViewDataBinding binding) {
this.binding = binding;
}
}
@Override
public BindingHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
ViewDataBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(viewGroup.getContext()),
R.layout.list_item,
viewGroup,
false);
BindingHolder holder = new BindingHolder(binding.getRoot());
holder.setBinding(binding);
return holder;
}
@Override
public void onBindViewHolder(BindingHolder holder, int position) {
User user = users.get(position);
holder.getBinding().setVariable(BR.user, user);
holder.getBinding().executePendingBindings();
}
屬性 Setter
每當(dāng)綁定值的變化,生成的Binding類必須調(diào)用setter方法??场斑。Data Binding框架有可以自定義賦值的方法漓踢。
自動Setters
對于一個(gè)屬性,Data Binding試圖找到setAttribute方法漏隐。與該屬性的namespace并不什么關(guān)系喧半,僅僅與屬性本身名稱有關(guān)。
例如青责,有關(guān)TextView的android:text
屬性的表達(dá)式會尋找一個(gè)setText(String)的方法挺据。如果表達(dá)式中的參量是一個(gè)int取具,Data Binding會搜索的setText(int)方法。注意:要表達(dá)式返回正確的類型扁耐,如果需要的話使用轉(zhuǎn)型暇检。Data Binding仍會運(yùn)行即使沒有給定名稱的屬性存在。然后婉称,您可以通過Data Binding輕松地為任何setter“創(chuàng)造”屬性块仆。例如,DrawerLayout沒有任何屬性王暗,但可以有很多的setters悔据。您可以使用其中的一個(gè)setters。
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
這里我們增加了一個(gè)命名空間app俗壹,并且注意DrawerLayout
的app:scrimColor
屬性科汗,這里和我們自定義view時(shí)自定義的屬性一樣,但是這里并不需要我們?nèi)ブ貙?code>DrawerLayout,此時(shí)绷雏,我們可以自己定義setTcrimColor
头滔、setDrawerListener
的方法
重命名的Setters
一些有setters的屬性按名稱并不匹配。對于這些方法涎显,屬性可以通過BindingMethods注解相關(guān)聯(lián)坤检。這必須與一個(gè)包含BindingMethod注解的類相關(guān)聯(lián),每一個(gè)用于一個(gè)重命名的方法棺禾。例如缀蹄,android:tint屬性與setImageTintList相關(guān)聯(lián),而不與setTint相關(guān)膘婶。
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
自定義Setters
有些屬性需要自定義綁定邏輯。例如蛀醉,對于android:paddingLeft
屬性并沒有相關(guān)setter悬襟。相反,setPadding(left, top, right, bottom)
是存在在拯刁。一個(gè)帶有BindingAdapter注解的靜態(tài)綁定適配器方法允許開發(fā)者自定義setter如何對于一個(gè)屬性的調(diào)用脊岳。
Android的屬性已經(jīng)創(chuàng)造了BindingAdapters。舉例來說垛玻,對于paddingLeft:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding適配器對其他定制類型非常有用割捅。例如,自定義loader可以用來異步載入圖像帚桩。
當(dāng)有沖突時(shí)亿驾,開發(fā)人員創(chuàng)建的Binding適配器將覆蓋Data Binding默認(rèn)適配器。
您也可以創(chuàng)建可以接收多個(gè)參數(shù)的適配器账嚎。
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
loadImage 可以放在任意類中莫瞬,該類中只有一個(gè)靜態(tài)的方法imageLoader儡蔓,該方法有3個(gè)參數(shù),一個(gè)是需要設(shè)置數(shù)據(jù)的view疼邀, 一個(gè)是我們需要的url喂江、有個(gè)個(gè)是錯誤加載的圖像,值得注意的是那個(gè)BindingAdapter注解旁振,看看他的參數(shù)获询,是一個(gè)數(shù)組,內(nèi)容只有一個(gè)bind:imageUrl拐袜,僅僅幾行代碼吉嚣,我們不需要 手工調(diào)用 (類 xxxxxxx)中的loadImage,也不需要知道loadImage方法定義到哪了阻肿,一個(gè)網(wǎng)絡(luò)圖片加載就搞定了這里面起關(guān)鍵作用的就是BindingAdapter 注解瓦戚,這里要遵循一定的規(guī)則,、
以bind:開頭丛塌,接著書寫你在控件中使用的自定義屬性名稱较解。
轉(zhuǎn)換器
在 xml 中為屬性賦值時(shí),如果變量的類型與屬性不一致赴邻,通過 DataBinding 可以進(jìn)行轉(zhuǎn)換
例如印衔,下面代碼中如果要為屬性 android:background 賦值一個(gè) int 型的 color 變量:
<View
android:background="@{isError.get() ? @color/red : @color/white}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_height="@{height}" />
只需要定義一個(gè)標(biāo)記了 @BindingConversion 的靜態(tài)方法即可(方法的定義位置可以隨意):
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
再舉個(gè)栗子 ,假如你的控件需要一個(gè)格式化好的時(shí)間姥敛,但是你只有一個(gè)Date類型的變量奸焙。可以轉(zhuǎn)化完成后在設(shè)置彤敛,此時(shí)更適合使用 conver
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data class=".Custom">
<variable
name="time"
type="java.util.Date" />
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{time}"/>
</layout>
binding.setTime(new Date());
看看 conver
public class ConvertUtil {
@BindingConversion
public static String convertDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
}
注:convert 可以放在任意包中与帆,只要寫明注解,已經(jīng)被轉(zhuǎn)換和轉(zhuǎn)換成的類型墨榄,所以注意不要重復(fù)定義類型相同的 convert玄糟,使用 Converter 一定要保證它不會影響到其他的屬性。舉個(gè)栗子袄秩,int -> int 的 convert 就影響到了android:visibility
Android stuido 的預(yù)覽支持
類似于 tools:text
,代碼如下
<TextView
style="@style/TextAppearance.AppCompat.Large"
android:text="@{user.firstName,default=PLACEHOLDER}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
舉個(gè)例子
工程分為兩個(gè)部分
- 第一個(gè)簡單的例子阵翎,點(diǎn)擊事件產(chǎn)生后,直接改變了之剧,user 的數(shù)據(jù)郭卫,并沒有對 view 操作的邏輯,但是 databinding 已經(jīng)幫我們完成了一切
- 第二個(gè)例子是一個(gè) recycleVIew 的例子背稼,點(diǎn)擊每個(gè) cardview 增加一點(diǎn)數(shù)據(jù)
代碼不貼了贰军,放 github了
https://github.com/xuyushi/AndroidMVVMDemo
我的更多 android 博文
http://xuyushi.github.io/archives/
關(guān)注作者~每周更新一篇 Android干貨博文
參考
https://www.zhihu.com/question/30976423
https://developer.android.com/intl/zh-cn/tools/data-binding/guide.html#generated_binding
https://segmentfault.com/a/1190000002876984
http://tech.vg.no/2015/07/17/android-databinding-goodbye-presenter-hello-viewmodel/
http://www.reibang.com/p/4e3220a580f6
https://github.com/LyndonChin/MasteringAndroidDataBinding
http://www.cnblogs.com/dxy1982/p/3793895.html
https://realm.io/cn/news/data-binding-android-boyar-mount/?utm_source=tuicool&utm_medium=referral
https://www.aswifter.com/2015/07/04/android-data-binding-1/
http://blog.csdn.net/qibin0506/article/details/47720125