開篇廢話
公司走了一個人,那個人寫的程序使用到了DataBinding,既然這樣,我就必須學習DataBinding,盡快接手這個項目。
DataBinding解決了Android UI編程中的一個痛點,官方原生支持MVVM模型可以讓我們在不改變既有代碼框架的前提下,非常容易地使用這些新特性。
MVVM的介紹
MVVM是Model-View-ViewModel的簡寫,這個模式提供對View和View Model的雙向數據綁定,使得View Model的狀態改變可以自動傳遞給View。
- Model:數據層,負責處理數據的加載或者存儲。
- View:視圖層,負責界面數據的展示,與用戶進行交互。
- ViewModel:負責完成View于Model間的交互,負責業務邏輯。
MVVM的模型關系圖:
準備工作
DataBinding是一個support library,所以它可以支持所有的android sdk,最低可以到android2.1(API7)。
如果是Android studio的版本在2.1以上,Android studio內置就支持了 DataBiding。如果是2.1之前的版本最好是升級一下,然后只需要在對應的Module的build.gradle中添加這么一句話即可。
dataBinding {
enabled=true
}
或
dataBinding.enabled=true
如果Android studio的版本不在2.1以上,那么就需要使用以下方面了,如果高于2.1,直接跳到基礎操作看。
使用DataBinding需要Android Gradle插件的支持,版本至少在1.5以上,需要的Android studio的版本在1.3以上。用如下方法導入。
AS 2.1以下修改build.gradle
再次提示一下,如果Android studio的版本在2.1以下,那么就需要使用以下方面了,如果高于2.1,直接跳到基礎操作看。
修改 Project 的build.gradle,為 build script 添加一條依賴,Gradle 版本為 1.2.3。
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.android.databinding:dataBinder:1.0-rc0'
為用到 DataBinding 的模塊添加插件,修改對應的build.gradle。
apply plugin: 'com.android.databinding'
注意
如果 Module 用到的 buildToolsVersion 高于 22.0.1,比如 23 rc1,那 com.android.databinding:dataBinder 的版本要改為 1.3.0-beta1,否則會出現如下錯誤:
基礎操作
工程創建完成后,我們通過一個最簡單的例子來說明 DataBinding 的基本用法。
布局文件
使用 DataBinding 之后,xml的布局文件就不再單純地展示 UI 元素,還需要定義 UI 元素用到的變量。所以,它的根節點不再是一個ViewGroup,而是變成了layout,并且新增了一個節點data。
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<!--原先的根節點(Root Element)-->
<LinearLayout>
....
</LinearLayout>
</layout>
要實現 MVVM 的ViewModel 就需要把數據與UI進行綁定,data節點就為此提供了一個橋梁,我們先在data 中聲明一個variable,這個變量會為UI 元素提供數據(例如 TextView 的 android:text),然后在Java代碼中把”后臺”數據與這個variable進行綁定。
如果要用一個表格來展示用戶的基本信息,用 DataBinding 應該怎么實現呢?
數據對象
添加一個 POJO類 - User,非常簡單,四個屬性以及他們的getter和setter。
public class User {
private final String firstName;
private final String lastName;
private String displayName;
private int age;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public User(String firstName, String lastName, int age) {
this(firstName, lastName);
this.age = age;
}
public int getAge() {
return age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getDisplayName() {
return firstName + " " + lastName;
}
public boolean isAdult() {
return age >= 18;
}
}
稍后,我們會新建一個User類型的變量,然后把它跟布局文件中聲明的變量進行綁定。
定義Variable
再回到布局文件,在data節點中聲明一個變量user。
<data>
<variable name="user" type="com.cc.databinding.User" />
</data>
其中type屬性就是我們在Java文件中定義的User類。
當然,data節點也支持import,所以上面的代碼可以換一種形式來寫。
<data>
<import type="com.cc.databinding.User" />
<variable name="user" type="User" />
</data>
然后我們剛才在 build.gradle 中添加的那個插件 - com.android.databinding會根據xml文件的名稱Generate一個繼承自ViewDataBinding的類。
例如,這里xml的文件名叫activity_basic.xml,那么生成的類就是ActivityBasicBinding。
注意
java.lang.*包中的類會被自動導入,可以直接使用,例如要定義一個String類型的變量:
<variable name="firstName" type="String" />
綁定Variable
Activity
修改BasicActivity的onCreate方法,用DatabindingUtil.setContentView()來替換掉setContentView(),然后創建一個user對象,通過binding.setUser(user)與variable進行綁定。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityBasicBinding binding = DataBindingUtil.setContentView(
this, R.layout.activity_basic);
User user = new User("guo", "cc");
binding.setUser(user);
}
Fragment
所幸DataBinding庫還提供了另外一個初始化布局的方法:DataBindingUtil.inflate()。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ViewDataBinding binding = DataBindingUtil.inflate(inflater,R.layout.fragment_blank,container,false);
return binding.getRoot();
}
注意
ActivityBasicBinding類是自動生成的,所有的set方法也是根據variable名稱生成的。例如,我們定義了兩個變量。
<data>
<variable name="firstName" type="String" />
<variable name="lastName" type="String" />
</data>
那么就會生成對應的兩個 set 方法。
setFirstName(String firstName);
setLastName(String lastName);
使用 Variable
數據與 Variable 綁定之后,xml 的 UI 元素就可以直接使用了。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}" />
至此,一個簡單的數據綁定就完成了。
高級用法
綁定非Activity的onClick寫法
android:onClick="@{(view)->user.show(view)}"
使用類方法
首先為類添加一個靜態方法。
public class MyStringUtils {
public static String capitalize(final String word) {
if (word.length() > 1) {
return String.valueOf(word.charAt(0)).toUpperCase() + word.substring(1);
}
return word;
}
}
然后在xml的data節點中導入:
<import type="com.cc.databinding.MyStringUtils" />
使用方法與 Java 語法一樣:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{MyStringUtils.capitalize(user.firstName)}" />
類型別名
如果我們在data節點了導入了兩個同名的類怎么辦?
<import type="com.example.home.data.User" />
<import type="com.examle.detail.data.User" />
<variable name="user" type="User" />
這樣一來出現了兩個User類,那user變量要用哪一個呢?不用擔心,import還有一個alias屬性。
<import type="com.example.home.data.User" />
<import type="com.examle.detail.data.User" alias="DetailUser" />
<variable name="user" type="DetailUser" />
Null Coalescing 運算符
android:text="@{user.displayName ?? user.lastName}"
就等價于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
屬性值
通過@{}可以直接把Java中定義的屬性值賦值給xml屬性。
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
別忘了導包,否則View.VISIBLE和View.GONE不可以使用。
<import type="android.view.View"/>
使用資源數據
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
完整版的布局文件如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 記得在前面加個“.” -->
<data class=".ResourceBinding">
<variable name="large" type="boolean" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
android:background="@android:color/black"
android:textColor="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
</LinearLayout>
</layout>
largePadding和smallPadding都是定義在dimens.xml文件中的資源數據。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="largePadding">20dp</dimen>
<dimen name="smallPadding">5dp</dimen>
</resources>
在Java代碼中與綁定large變量,并賦值為ture。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ResourceBinding不需要導包,導包就錯了
ResourceBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_resource);
binding.setLarge(true);
}
這樣做是沒有什么問題的,但是有老版本的可能會出問題。
如果在Run工程的時候,出現錯誤,報錯信息如下:
cannot find the setter for attribute 'android:padding' on android.widget.TextView with parameter type float.
看來像是DataBinder把@dimen/largePadding解析成了float類型,可以試一下類型轉換:
android:padding="@{large? (int)@dimen/largePadding : (int)@dimen/smallPadding}"
雙向綁定
在舊版本只支持單向綁定,建議升級為最新版。
在正向綁定中,我們在Layout里面的綁定表達式是這樣的:
<layout ...>
<data>
<variable type="com.example.myapp.User" name="user"/>
</data>
<RelativeLayout ...>
<TextView android:text="@{user.name}" .../>
</RelativeLayout>
</layout>
當user.name的數據改動時,我們的TextView都會同步改變文字。
現在假設一種情況,當你更換成EditText時,如果你的用戶名User.name已經綁定到EditText中,當用戶輸入文字的時候,你原來的user.name數據并沒有同步改動,因此我們需要修改成:
<layout ...>
<data>
<variable type="com.example.myapp.User" name="user"/>
</data>
<RelativeLayout ...>
<EditText android:text="@={user.name}" .../>
</RelativeLayout>
</layout>
看出微小的差別了嗎?對,就是"@{}"改成了"@={}",是不是很簡單?
開啟雙向綁定,需要在項目的build.gradle中設置:
classpath 'com.android.tools.build:gradle:2.1.0-alpha3'
我們剛才的例子里面只顯示了系統自帶的應用,那么如果是自定義控件,或者是我們更細顆粒度的Observable呢?等下就揭曉如何自定義自己的雙向綁定,我們來看看目前Android支持的控件:
- AbsListView android:selectedItemPosition
- CalendarView android:date
- CompoundButton android:checked
- DatePicker android:year, android:month, android:day
- NumberPicker android:value
- RadioGroup android:checkedButton
- RatingBar android:rating
- SeekBar android:progress
- TabHost android:currentTab (估計沒人用)
- TextView android:text
- TimePicker android:hour, android:minute
自定義雙向綁定
設想一下我們使用了下拉刷新SwipeRefreshLayout控件,這個時候我們希望在加載數據的時候能控制refreshing的狀態,所以我們加入了ObservableBoolean的變量swipeRefreshViewRefreshing來正向綁定數據,并且能夠在用戶手動下拉刷新的時候同步更新swipeRefreshViewRefreshing數據:
// SwipeRefreshLayout.java
public class SwipeRefreshLayout extends View {
private boolean isRefreshing;
public void setRefreshing() {/* ... */}
public boolean isRefreshing() {/* ... */}
public void setOnRefreshListener(OnRefreshListener listener) {
/* ... */
}
public interface OnRefreshListener {
void onRefresh();
}
}
接下來我們需要告訴框架,我們需要將SwipeRefreshLayout的isRefreshing的值反向綁定到swipeRefreshViewRefreshing:
@InverseBindingMethods({
@InverseBindingMethod(
type = android.support.v4.widget.SwipeRefreshLayout.class,
attribute = "refreshing",
event = "refreshingAttrChanged",
method = "isRefreshing")})
這是一種簡單的定義,其中event和method都不是必須的,因為系統會自動生成,寫出來是為了更好地了解如何綁定的,可以參考官方文檔InverseBindingMethod。
當然你也可以使用另外一種寫法,并且如果你的值并不是直接對應Observable
的值的時候,就可以在這里進行轉換:
@InverseBindingAdapter(attribute = "refreshing", event = "refreshingAttrChanged")
public static boolean isRefreshing(SwipeRefreshLayout view) {
return view.isRefreshing();
}
上面的event同樣也不是必須的。以上的定義都是為了讓我們能夠在布局文件中使用"@={}"這個雙向綁定的特性。接下來你需要告訴框架如何處理refreshingAttrChanged事件,就像處理一般的監聽事件一樣:
@BindingAdapter("refreshingAttrChanged")
public static void setOnRefreshListener(final SwipeRefreshLayout view,
final InverseBindingListener refreshingAttrChanged) {
if (refreshingAttrChanged == null) {
view.setOnRefreshListener(null);
} else {
view.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
colorChange.onChange();
}
});
}
}
一般情況下,我們都需要設置正常的OnRefreshListener,所以我們可以合并寫成:
@BindingAdapter(value = {"onRefreshListener", "refreshingAttrChanged"}, requireAll = false)
public static void setOnRefreshListener(final SwipeRefreshLayout view,
final OnRefreshListener listener,
final InverseBindingListener refreshingAttrChanged) {
OnRefreshListener newValue = new OnRefreshListener() {
@Override
public void onRefresh() {
if (listener != null) {
listener.onRefresh();
}
if (refreshingAttrChanged != null) {
refreshingAttrChanged.onChange();
}
}
};
OnRefreshListener oldValue = ListenerUtil.trackListener(view, newValue, R.id.onRefreshListener);
if (oldValue != null) {
view.setOnRefreshListener(null);
}
view.setOnRefreshListener(newValue);
}
現在我們終于可以使用雙向綁定的技術啦。但是要注意,需要設置requireAll = false,否則系統將識別不了refreshingAttrChanged屬性,前文提到的文章例子里并沒有設置這個。
在ViewModel中,我們的數據是這樣的:
// MyViewModel.java
public final ObservableBoolean swipeRefreshViewRefreshing = new ObservableBoolean(false);
public void load() {
swipeRefreshViewRefreshing.set(true);
// 網絡請求
....
swipeRefreshViewRefreshing.set(false);
}
public SwipeRefreshLayout.OnRefreshListener onRefreshListener() {
return new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
// Do something you need
}
};
}
在布局文件中是這樣設置的:
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:onRefreshListener="@{viewModel.onRefreshListener}"
app:refreshing="@={viewModel.swipeRefreshViewRefreshing}">
...
</android.support.v4.widget.SwipeRefreshLayout>
最后我們還有一個小問題,就是雙向綁定有可能會出現死循環,因為當你通過Listener反向設置數據時,數據也會再次發送事件給View。所以我們需要在設置一下避免死循環:
@BindingAdapter("refreshing")
public static void setRefreshing(SwipeRefreshLayout view, boolean refreshing) {
if (refreshing != view.isRefreshing()) {
view.setRefreshing(refreshing);
}
}
這樣就沒問題啦。
帶ID的View
DataBinding有效降低了代碼的冗余性,甚至完全沒有必要再去獲取一個View實例,但是情況不是絕對的,萬一我們真的就需要了呢?不用擔心,只要給View定義一個 ID,DataBinding就會為我們生成一個對應的final變量。
<TextView
android:id="@+id/firstName"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
上面代碼中定義了一個ID為firstName*的TextView,那么它對應的變量就是。
public final TextView firstName;
使用的時候用。
binding.firstName.setText("cc");
ViewStubs
xml中的ViewStub經過 binding 之后會轉換成 ViewStubProxy。
簡單用代碼說明一下,xml文件與之前的代碼一樣,根節點改為layout,在LinearLayout中添加一個ViewStub,添加ID。
<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實例,為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);
}
});
Dynamic Variables
以RecyclerView為例,Adapter的DataBinding需要動態生成,因此我們可以在onCreateViewHolder的時候創建這個DataBinding,然后在onBindViewHolder中獲取這個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();
}
注意此處DataBindingUtil的用法:
ViewDataBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(viewGroup.getContext()),
R.layout.list_item,
viewGroup,
false);
Attribute setters
當一個被綁定的數據的值發生改變時,Binding類會自動尋找該view上的綁定表達式上的方法去改變view,通過google數據綁定框架我們可以去自定義這些方法。
對于一個xml的attribute,DataBinding會去尋找setAttribute方法,xml屬性的命名空間是沒有關系的。比如TextView上的一個屬性android:text,會去尋找setText(String)。如果表達式返回的是int則會去尋找setText(int),所以必須確保xml中表達式返回正確的數據類型,必要時需要數據轉換。
我們可以比較容易地為任何屬性創造出setter去使用dataBinding。比如support包下的DrawerLayout沒有任何屬性,但是確有很多setter,下面利用這些已有的setter中的一個:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
自定義setters
一些xml屬性需要自己去定義并實現邏輯,比如android:paddingLeft。但是setPadding(left,top,right,bottom)是存在的,那么我們可以同BindingAdapter注解去自定義個自己的setter:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Note:開發者自定義的BindingAdapter和android自帶的發生沖突時,data bingding會優先采用開發者自定義的。
多參數的BindingAdapter
@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);
}
BindingAdpater方法可以對屬性的舊值和新值進行處理
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
事件處理的列子
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
轉換器 (Converters)
有時候我們想這樣寫xml屬性。
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
但是xml屬性的setter是一個drawable,我們可以定義一個標記了@BindingConversion的靜態方法即可。
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
由于這個注解至是發生在setter層面上,所以并不支持下面的混合寫法。
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
關于DataBinding的一些個人看法
DataBinding使用心得
- 使用xml進行view布局
- 采用符合Java Bean規范的數據原型
- 規范的自定義View
- 禁止在BindingAdapter中的setter方法中改變數據或者做數據處理
- 不建議用BindingConversion處理數據轉換
- 不建議在xml布局中處理view事件
- 不建議在xml中使用復雜的表達式
DataBinding使用的一些思考
DataBinding的不足之處:
- DataBinding在xml提供了豐富的操作符,但是由于Android studio天生的xml語法檢查的貧弱,xml布局中的表達式邏輯錯誤,不能準確定位,導致debug難度增加,事實上一些BindingAdapter的錯誤在build的時候也會被提示xml錯誤。
- 對自定義view的要求比較高,需要自定義綁定方法,如BindingAdapter等。
- 可能由于java8移除apt,采用了新的API的緣故,所以即使Android Studio2.2已經開始支持java8特性,但是需要開啟jack編譯鏈,DataBinding與之沖突,導致在代碼中不能使用lambda表達式等java8特性。值得欣慰的是,這一問題將在Android Studio2.4中得到解決。
PS:數據綁定的應用軟件開發的一種趨勢,使用DataBinding的優點顯而易見,但是使用的時候我們也需要小心。