轉自:https://github.com/LyndonChin/MasteringAndroidDataBinding
官方雖然已經給出了教程 - Data Binding Guide (中文版 - Data Binding(數據綁定)用戶指南) ,但是實踐之后發現槽點實在太多,於是就有了這個教程,針對每個知識點給出更詳實的例子同時也總結了遇到的一些坑,希望對你有所幫助:)
Data Binding 解決了 Android UI 編程的一個痛點,官方原生支持 MVVM 模型可以讓我們在不改變既有代碼框架的前提下,非常容易地使用這些新特性。
Data Binding 框架如果能夠推廣開來,也許 RoboGuice、ButterKnife 這樣的依賴注入框架會慢慢失去市場,因為在 Java 代碼中直接使用 View
變量的情況會越來越少。
准備
新建一個 Project,確保 Android 的 Gradle 插件版本不低於 1.5.0-alpha1:
classpath 'com.android.tools.build:gradle:1.5.0'
然后修改對應模塊(Module)的 build.gradle:
dataBinding {
enabled true
}
基礎
工程創建完成后,我們通過一個最簡單的例子來說明 Data Binding 的基本用法。
布局文件
使用 Data Binding 之后,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
就需要把數據(Model)與 UI(View) 進行綁定,data
節點的作用就像一個橋梁,搭建了 View 和 Model 之間的通路。
我們先在 xml 布局文件的 data
節點中聲明一個 variable
,這個變量會為 UI 元素提供數據(例如 TextView
的 android:text
),然后在 Java 代碼中把『后台』數據與這個 variable
進行綁定。
下面我們使用 Data Binding 創建一個展示用戶信息的表格。
數據對象
添加一個 POJO 類 - User
,非常簡單,兩個屬性以及他們的 getter 和 setter。
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; } }
稍后,我們會新建一個 User
類型的變量,然后把它跟布局文件中聲明的變量進行綁定。
定義 Variable
回到布局文件,在 data
節點中聲明一個 User
類型的變量 user
。
<data>
<variable name="user" type="com.liangfeizc.databindingsamples.basic.User" /> </data>
其中 type
屬性就是我們在 Java 文件中定義的 User
類。
當然,data
節點也支持 import
,所以上面的代碼可以換一種形式來寫。
<data>
<import type="com.liangfeizc.databindingsamples.basic.User" /> <variable name="user" type="User" /> </data>
然后我們剛才在 build.gradle 中添加的那個插件 - com.android.databinding
會根據 xml 文件的名稱 Generate 一個繼承自 ViewDataBinding
的類。 當然,IDE 中看不到這個文件,需要手動去 build 目錄下找。
例如,這里 xml 的文件名叫 activity_basic.xml
,那么生成的類就是 ActivityBasicBinding
。
注意
java.lang.*
包中的類會被自動導入,可以直接使用,例如要定義一個 String
類型的變量:
<variable name="firstName" type="String" />
綁定 Variable
修改 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("fei", "Liang"); binding.setUser(user); }
除了使用框架自動生成的 ActivityBasicBinding
,我們也可以通過如下方式自定義類名。
<data class="com.example.CustomBinding"> </data>
注意
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}" />
至此,一個簡單的數據綁定就完成了,可參考完整代碼
高級用法
使用類方法
首先定義一個靜態方法
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.liangfeizc.databindingsamples.utils.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}"/>
使用資源數據
這個例子,官方教程有錯誤,可以參考Android Data Binder 的一個bug,完整代碼在此
<TextView
android:padding="@{large? (int)@dimen/largePadding : (int)@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" />
Observable Binding
本來這一節的標題應該叫雙向綁定,但是很遺憾,現在的 Data Binding 暫時支持單向綁定,還沒有達到 Angular.js 的威力。
要實現 Observable Binding,首先得有一個 implement
了接口 android.databinding.Observable
的類,為了方便,Android 原生提供了已經封裝好的一個類 - BaseObservable
,並且實現了監聽器的注冊機制。
我們可以直接繼承 BaseObservable
。
public class ObservableUser extends BaseObservable { private String firstName; private String lastName; @Bindable public String getFirstName() { return firstName; } @Bindable public String getLastName() { return lastName; } public void setFirstName(String firstName) { this.firstName = firstName; notifyPropertyChanged(BR.firstName); } public void setLastName(String lastName) { this.lastName = lastName; notifyPropertyChanged(BR.lastName); } }
BR
是編譯階段生成的一個類,功能與 R.java
類似,用 @Bindable
標記過 getter
方法會在 BR
中生成一個 entry。
通過代碼可以看出,當數據發生變化時還是需要手動發出通知。 通過調用 notifyPropertyChanged(BR.firstName)
可以通知系統 BR.firstName
這個 entry
的數據已經發生變化,需要更新 UI。
除此之外,還有一種更細粒度的綁定方式,可以具體到成員變量,這種方式無需繼承 BaseObservable
,一個簡單的 POJO 就可以實現。
public class PlainUser { public final ObservableField<String> firstName = new ObservableField<>(); public final ObservableField<String> lastName = new ObservableField<>(); public final ObservableInt age = new ObservableInt(); }
系統為我們提供了所有的 primitive type 所對應的 Observable類,例如 ObservableInt
、ObservableFloat
、ObservableBoolean
等等,還有一個 ObservableField
對應着 reference type。
剩下的數據綁定與前面介紹的方式一樣,具體可參考ObservableActivity。
帶 ID 的 View
Data Binding 有效降低了代碼的冗余性,甚至完全沒有必要再去獲取一個 View 實例,但是情況不是絕對的,萬一我們真的就需要了呢?不用擔心,只要給 View 定義一個 ID,Data Binding 就會為我們生成一個對應的 final
變量。
<TextView
android:id="@+id/firstName" android:layout_width="wrap_content" android:layout_height="wrap_content" />
上面代碼中定義了一個 ID 為 firstName* 的 TextView
,那么它對應的變量就是
public final TextView firstName;
具體代碼可參考 ViewWithIDsActivity.java
ViewStubs
xml 中的 ViewStub
經過 binding 之后會轉換成 ViewStubProxy
, 具體代碼可參考 ViewStubActivity.java
簡單用代碼說明一下,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
完整代碼可以參考 dynamic
以 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);
還有另外一種比較簡潔的方式,直接在構造 Holder 時把 View
與自動生成的 XXXBinding
進行綁定。
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserHolder> { private static final int USER_COUNT = 10; @NonNull private List<User> mUsers; public UserAdapter() { mUsers = new ArrayList<>(10); for (int i = 0; i < USER_COUNT; i ++) { User user = new User(RandomNames.nextFirstName(), RandomNames.nextLastName()); mUsers.add(user); } } public static class UserHolder extends RecyclerView.ViewHolder { private UserItemBinding mBinding; public UserHolder(View itemView) { super(itemView); mBinding = DataBindingUtil.bind(itemView); } public void bind(@NonNull User user) { mBinding.setUser(user); } } @Override public UserHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View itemView = LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.user_item, viewGroup, false); return new UserHolder(itemView); } @Override public void onBindViewHolder(UserHolder holder, int position) { holder.bind(mUsers.get(position)); } @Override public int getItemCount() { return mUsers.size(); } }
Attribute setters
有了 Data Binding,即使屬性沒有在 declare-styleable
中定義,我們也可以通過 xml 進行賦值操作。 為了演示這個功能,我自定義了一個 View - NameCard,屬性資源 R.styleable.NameCard 中只定義了一個 age
屬性,其中 firstName
和 lastName
只有對應的兩個 setter
方法。
只要有 setter
方法就可以像下面代碼一樣賦值:
<com.liangfeizc.databindingsamples.attributesetters.UserView
android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="@dimen/largePadding" app:onClickListener="@{activity.clickListener}" app:firstName="@{@string/firstName}" app:lastName="@{@string/lastName}" app:age="27" />
onClickListener
也是同樣道理,只不過我們是在 Activity
中定義了一個 Listener
。
轉換器 (Converters)
非常重要
使用 Converter 一定要保證它不會影響到其他的屬性,例如這個
@BindingConversion
- convertColorToString 就會影響到android:visibility, 因為他們都是都符合從 int 到 int 的轉換。
在 xml 中為屬性賦值時,如果變量的類型與屬性不一致,通過 DataBinding 可以進行轉換。
例如,下面代碼中如果要為屬性 android:background
賦值一個 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}" />
只需要定義一個標記了 @BindingConversion
的靜態方法即可(方法的定義位置可以隨意):
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); }
具體代碼可參考 ConversionsActivity.java。