安卓 Data Binding 使用方法總結(姐姐篇)
0. 前言
在專案中使用到了 Data Binding,總結使用經驗後寫成本文。
本文涉及安卓自帶框架 DataBinding 的基礎使用方法,適合初次接觸 Data Binding 的同學閱讀。
1. Data Binding 利弊
優勢
DataBinding 出現以前,我們在實現 UI 介面時,不可避免的編寫大量的毫無營養的程式碼:比如 View.findViewById()
;比如各種更新 View 屬性的 setter:setText()
,setVisibility()
,setEnabled()
或者 setOnClickListener()
等等。
這些“垃圾程式碼”數量越多,越容易滋生 bug。
使用 DataBinding,我們可以避免書寫這些“垃圾程式碼”。
劣勢
使用 Data Binding 會增加編譯出的 apk 檔案的類數量和方法數量。
新建一個空的工程,統計開啟 build.gradle 中 Data Binding 開關前後的 apk 檔案中類數量和方法數量,類增加了 120+,方法數增加了 9k+(開啟混淆後該數量減少為 3k+)。
如果工程對方法數量很敏感的話,請慎重使用 Data Binding。
2. 怎麼使用 DataBinding
2.0 配置 build.gradle
Gradle 1.5 alpha 及以上自帶支援 DataBinding,僅需在使用 DataBinding 的 module 裡面的 build.gradle 裡面加上配置即可:
android {
...
dataBinding {
enabled = true
}
...
}
在詳細學習 DataBinding 之前,我們可以先來看下一個簡單的例子:LoginDemo4DataBinding。該例子使用非 DataBinding 技術和 DataBinding 技術 2 種方式,實現了一個簡單的登入頁面。通過該例子,我們可以直觀的感受下 DataBinding 的不同。
下面我們詳細討論 DataBinding 的使用方法,下面的例子實現的是在介面上顯示兩個文字:姓氏和名字。
2.1 公共程式碼
公用的 Activity 如下:
public class DataBindingActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TestDbBinding binding = DataBindingUtil.setContentView(this, R.layout.test_db_layout);
// viewmodel
UserViewModel viewModel = new UserViewModel();
binding.setUser(viewModel);
}
}
其中,TestDbBinding 是根據 R.layout.test_db_layout 自動生成的,binding.setUser() 方法也是根據 layout 中 variable name 自動生成的。
公用的 R.layout.test_db_layout 如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="userViewModel" type="com.example.UserViewModel"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{userViewModel.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{userViewModel.lastName}"/>
</LinearLayout>
</layout>
做過 J2EE 開發的同學,有沒有似曾相識感覺?感人感覺,不管是 Data Binding,還是 React Native,都是將 Web 開發的先進思想或技術引進到移動開發領域的一種嘗試。更具體的說,是將宣告式程式設計(Declarative programming,如 JavaScript,CSS 等)引入指令式程式設計(Imperative programming,如 Java 等)中。
<div id="welcome">Welcome <a href="#">{user.username}</a> <a href="{site}home/logout">logout</a></div>
<div class="clear"></div>
{if:isset(menus)}
{if:menus}
<div id="moduleList">
<ul>
{foreach:menus,$menu}
<li {if:$menu.is_active} class="current" {end}><div><a href="{site}{$menu.m_uri}/">{__($menu.m_label)}</a></div></li>
{end}
</ul>
</div>
2.2 僅作靜態展示
如果 UI 比較簡單,介面僅僅是靜態展示,不涉及 UI 的動態更新,以下程式碼就能滿足需求了。
public class UserViewModel {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
即使在複雜的 UI 介面中,多數介面元素僅是靜態展示,只有少數介面元素才需要根據使用者的操作動態更新。
以微信朋友圈介面為例,一條狀態的使用者頭像、暱稱、內容、時間等這些元素一旦載入成功,就不再改變了;而點贊列表和評論列表是動態改變的。
如何使用 DataBinding 動態更新介面資料呢?
2.3 動態更新資料
有3種方式實現動態更新介面資料:
- 實現 Observable 介面;
- 繼承 BaseObservable;
- 使用 ObservableField;
2.3.0 實現 Observable 介面
由於 Java 不允許多繼承,而允許同時實現多個介面,所以該方法更具有通用性。
2.3.1 繼承 BaseObservable
public class UserViewModel extends BaseObservable {
private String firstName;
private String lastName;
public UserViewModel(String firstname, String lastname) {
this.firstName = firstname;
this.lastName = 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.java 是類似 R.java 的資原始檔,是 Binding Resources 的縮寫,由框架自動生成。
注意,BR 中的 id 生成的依據是 @Bindable 修飾的方法名 getXXX(),而非方法體的內容。當在 getXXX() 方法前加 @Bindable 之後, BR.java 中就立即生成常量 BR.xXX。
還有另外一種寫法:
public @Bindable String firstName;
@Bindable 可以放在 public 之前或之後,但是不能放在 String 之後。
這種方式,框架會自動生成 getFirstName() 方法。注意,此時變數的訪問許可權必須是 public
。
如果 @Bindable 修飾的變數和 @Bindable 修飾的該變數的 getter 方法同時存在,則 getter 方法失效。
上述兩種方式的區別在於,@Bindable 修飾的 get 方法體,不一定是簡單的 return xxx
,也可以是複雜的處理過程。例如,介面上顯示的是 displayName,而 displayName 是由 firstName 和 lastName 按一定規則加工生成,改變 firstName 或 lastName 均會導致 displayName 對應的 UI 元素更新。這時,我們可以這麼寫:
// ...
private String displayName;
// ...
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(com.mmlovesyy.displaynamedemo.BR.displayName);
}
@Bindable
public String getDisplayName() {
return firstName + "." + lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(com.mmlovesyy.displaynamedemo.BR.displayName);
}
和
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.displayName}' />
而如果使用 public @Bindable String displayName;
,由於 get 方法是框架自動生成的,方法體是 return displayName;
, 我們將無法做到這種效果。
其實,從這個案例我們可以一窺其動態更新的原理:通過 setLastName() 改變 lastName,並在該方法中通知訂閱者,訂閱者再呼叫 getDisplayName() 方法來代替 layout 檔案中的 user.displayName,
2.2.2 使用 ObservableXXX / ObservableField<T>
public class UserViewModel {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
ObservableByte / ObservableChar / ObservableShort / ObservableInt /ObservableLong / ObservableFloat / ObservableDouble / ObservableBoolean / ObservableParcelable 為基本資料型別;ObservableField<T> 對應應用型別,如 String,Integer 等。
注意,firstName 的操作方法是 get()
和 set()
方法,例如要更新 firstName 的值:
firstName.set("linus chen");
age.set(age.get() + 1);
推而廣之,不管是 ObservableInt/ObservableBoolean/ObservableFloat 等基本資料型別,還是ObservableField<T>的變數,只有呼叫其 set() 方法,其繫結的 UI 元素才會更新。這是因為,更新 UI 元素操作(notifyChanged() 方法)是在 set() 方法中觸發的,具體見如下程式碼:
ObservableInt.java
public class ObservableInt extends BaseObservable implements Parcelable, Serializable {
/**
* Set the stored value.
*/
public void set(int value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
}
ObservableField.java
public class ObservableField<T> extends BaseObservable implements Serializable {
/**
* Set the stored value.
*/
public void set(T value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
}
3. 動態更新 ViewGroup
前幾節寫的都是比較簡單的使用方法,是在 View 已經確定的情況下更新其屬性(資料、可見性等)。那麼如何更新 ViewGroup 呢,即動態的向 ViewGroup 新增或移除子 View 呢?這是我們需要使用 @BindingAdapter({“bind:attr1”, “bind:attr2”}) 註解。
先簡要介紹一下該註解的使用方法,然後用一個例子來具體說明。
在 layout 中,使用 “app:attr1” 的格式來新增引數,這些引數會被傳遞到 @BindingAdapter 修飾的方法中,方法必須是 public static void
型別。注意其中的 static
,這就限制了方法體中使用的變數(基本型別,引用型別,各種 XXXListener等)和方法要麼通過 @BindingAdapter 傳進去的,要麼是 static的。
當你被 static 困擾時,請考慮一下通過 @BindingAdapter 裡面的引數傳進去。
舉個動態生成的 View 的 click 事件和 layout 中 DataBinding 的事件的互動的例子,就是通過引數將 OnClickListener 的例項以引數的形式傳給 static 方法,而例項 onClick() 實際呼叫 ViewModel 中的 handleOnClick() 方法。
舉個例子,效果圖如下,向一個 LinearLayout 中動態新增 TextView:
layout 程式碼如下:
<?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="viewModel"
type="com.mmlovesyy.bindingadapterdemo.NamesViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<Button
android:id="@+id/add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{viewModel.onClick}"
android:text="+" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:context="@{viewModel.context}"
app:names="@{viewModel.names}"></LinearLayout>
</LinearLayout>
</layout>
Activity 程式碼如下:
public class MainActivity extends AppCompatActivity {
private NamesViewModel viewModel = new NamesViewModel(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setViewModel(viewModel);
}
}
NamesViewModel 程式碼如下:
public class NamesViewModel {
public Context context;
public final ObservableArrayList<String> names = new ObservableArrayList<>();
public NamesViewModel(Context context) {
names.add("linus chen");
names.add("lin xueyan");
names.add("zhang xiaona");
names.add("chen lei");
names.add("liu yuhong");
this.context = context;
}
@BindingAdapter({"bind:names", "bind:context"})
public static void setNames(ViewGroup linearLayout, ArrayList<String> names, Context context) {
linearLayout.removeAllViews();
for (String s : names) {
TextView t = new TextView(context);
t.setText(s);
linearLayout.addView(t);
}
}
public void onClick(View v) {
int id = v.getId();
if (id == R.id.add_btn) {
names.add("yanyu cai");
}
}
}
4. 如何除錯
如果編譯出錯,log 日誌的報錯資訊,即出錯的程式碼行是指框架根據 xml 檔案生成的 xxxBinding.java,,由於目前 Android Studio 尚不支援自動定位出錯程式碼行,所以我們要手動去找該檔案。xxxBinding.java 檔案位置:將 AS 切換成 Project 檢視 - 對應的 module(如 app)- build - intermediates - classes - debug - 對應的 package。
然後根據出錯程式碼行,推測對應的 xml 檔案中出錯的位置。
希望以後 Android Studio 能改善這方面的體驗。其實,這也間接要求我們不要在表示式使用複雜的邏輯,越簡單越容易除錯。
5. 工作原理
6. 效能如何
7. 單元測試
這裡要講的單元測試主要是針對 @BindingAdapter 修飾的方法的。
我們可以這麼寫:
public class MyBindingAdapters {
@BindingAdapter("android:text")
public static void setText(TextView view, String value) {
if (isTesting) {
doTestingStuff(view, value);
} else {
TextViewBindingAdapter.setText(view, value);
}
}
}
這有點噁心,而且由於 setText() 方法是 static 的,所以它裡面使用的變數或方法都必須是 static 的,即變數 isTesting
和 doTestingStuff()
都是 static 的,更加不方便,這顯然是面向過程程式設計的方法,不符合 OCP 原則(對擴充套件開放,非修改封閉)。
我們有一種更好的方法來做單元測試。不過首先我們要先來了解 android.databinding.DataBindingComponent
這個介面的用法,弄懂了它的用法,就知道怎麼做單元測試。而且不僅僅可以做單元測試,還有其他用途。
UML 圖:
DataBindingComponent.java
/**
* This interface is generated during compilation to contain getters for all used instance
* BindingAdapters. When a BindingAdapter is an instance method, an instance of the class
* implementing the method must be instantiated. This interface will be generated with a getter
* for each class with the name get* where * is simple class name of the declaring BindingAdapter
* class/interface. Name collisions will be resolved by adding a numeric suffix to the getter.
* <p>
* An instance of this class may also be passed into static or instance BindingAdapters as the
* first parameter.
* <p>
* If using Dagger 2, the developer should extend this interface and annotate the extended interface
* as a Component.
*
* @see DataBindingUtil#setDefaultComponent(DataBindingComponent)
* @see DataBindingUtil#inflate(LayoutInflater, int, ViewGroup, boolean, DataBindingComponent)
* @see DataBindingUtil#bind(View, DataBindingComponent)
*/
public interface DataBindingComponent {
}
這是一個空的介面,沒有宣告任何方法。注意檔案開頭的註釋部分。定義一個抽象類,抽象 @BindingAdapter 修飾的方法,這裡我們使用 setText() 方法作為例子。
MyBindingAdapter.java
public abstract class MyBindingAdapters {
@BindingAdapter("android:text")
public abstract void setText(MyDataBindingComponent component, TextView view, String value);
}
再定義兩個 MyBindingAdapters 的子類,分別用於單元測試和實際生產環境:TestBindingAdapters.java 和 ProdBindingAdapters.java:
TestBindingAdapters.java
private static final String TAG = "TestBindingAdapters";
@Override
public void setText(MyDataBindingComponent component, TextView view, String value) {
// test code
Log.d("TestBindingAdapters", "SETTEXT INVOKED");
}
ProdBindingAdapters.java
public class ProdCBindingAdapters extends MyBindingAdapters {
@Override
public void setText(MyDataBindingComponent component, TextView view, String value) {
TextViewBindingAdapter.setText(view, value);
}
}
注意,這兩個類提供的 @BindingAdapter 修飾的方法都是非 static 的。
我們在
DataBindingUtil.setContentView(Activity activity, int layoutId,DataBindingComponent bindingComponent);
中要使用的是 DataBindingComponent,所以我們還要定義一個 MyDataBindingComponent,及其兩個子類:TestComponent 和 ProdComponent,分別與 TestBindingAdapters 和 ProdBindingAdapters 相對應。
MyDataBindingComponent.java
public interface MyDataBindingComponent extends android.databinding.DataBindingComponent {
MyBindingAdapters getMyBindingAdapters();
}
TestComponent.java
public class TestComponent implements MyDataBindingComponent {
private MyBindingAdapters mAdapter = new TestBindingAdapters();
@Override
public MyBindingAdapters getMyBindingAdapters() {
return mAdapter;
}
}
ProdComponent.java
public class ProdComponent implements MyDataBindingComponent {
private String color;
public ProdComponent(String _color) {
color = _color;
}
private MyBindingAdapters mAdapter = new ProdCBindingAdapters();
@Override
public MyBindingAdapters getMyBindingAdapters() {
return mAdapter;
}
public String getColor() {
return color;
}
}
然後在 MainActivity 中呼叫:
public class MainActivity extends AppCompatActivity {
private UserViewModel user;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main, new ProdComponent("blue"));
user = new UserViewModel("linus", "chen");
binding.setUser(user);
}
@BindingAdapter("android:url")
public static void setColor(ProdComponent component, TextView view, String url) {
view.setText(component.getColor());
}
}
注意,@BindingAdapter 修飾的方法包含在類 MyBindingAdapter 中,所以 DataBindingUtil.setContentView(this, R.layout.activity_main, new ProdComponent("blue"));
中的最後一個引數,DataBindingComponent 類中必須包含一個名為 getMyBindingAdapter() 的 getter 方法,遵循本節開頭的檔案註釋中的規定。
這樣,我們使用 DataBindingUtil.setContentView(this, R.layout.activity_main, new TestComponent());
進行單元測試。
我們還可以看到,DataBindingComponent 中可以書寫其他方法(例如網路下載方法等),供 @BindingAdapter 修飾的方法(不論是 static 或非 static)呼叫。
如果要使用 Dagger2, 則程式碼如下:
Dagger2
@Module
public class TestModule {
@Provides
public MyBindingAdapters getMyBindingAdapter() {
return TestBindingAdapter();
}
}
@Component(modlues = TestModule.class)
public interface TestComponent extends android.databinding.DataBindingComponent {
}
DataBindingUtil.setDefaultComponent(DaggerTestComponent.create());
還有一點需要注意的是,無自定義 DataBindingComponent 時框架生成的 ActivityMainBinding.java 相關程式碼:
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, firstNameUser);
自定義 DataBindingComponent 時生成的相關程式碼:
this.mBindingComponent.getMyBindingAdapters().setText(this.mboundView2, firstNameUser);
對比可以看出,當自定義 DataBindingAdapter 時,框架會自動呼叫自定義的 setText() 方法,而非預設的 TextViewBindingAdapter.setText()。
8. 最佳實踐
至於 DataBinding 怎麼寫才是最好的,可以參考 Google 的這個開源專案:Android Architecture Blueprints [beta],使用 DataBinding、MVP、DataBinding+MVP 等多種方式實現同一個便箋應用,包括其中的單元測試的寫法,都非常值得學習。
9. 一些建議
9.1 表示式儘量簡單
xml 檔案中不要出現業務邏輯,只出現簡單的 UI 相關的表示式;
在 xml 繫結變數時儘量使用 ” 代替 “”,即使如此,轉義依然不可避免:
- ‘&’ –> ‘&;(用英文分號替換)’;
- ‘<’ –> ‘<;(用英文分號替換)’;
- ‘>’ -> ‘>;(用英文分號替換)’;
9.2 Clean 大法好
有時會遇到莫名其妙的檢查錯誤,可嘗試 clean 工程,或無視之;
9.3 關於程式碼結構
關於程式碼結構,使用 ViewModel(如 UserViewModel)+ Model(如 UserModel) + xml 的結構,對點選事件的處理以及 View 的狀態資料(如評論列表是否展開,當前使用者登入資訊等)放在 ViewModel 中,而正常的資料放在 Model 中。
9.4 不要拒絕 findViewById
DataBinding 和 findViewById() 並不是互斥的,即使在使用 DataBinding 的工程中,我們仍然可以根據需要使用之,特別是在動態更新 ViewGroup 的情景中,有時不可避免的要是用該方法。
9.4 關於對 null 的處理
DataBinding 已經對 null 做了處理,我們無需再關心表示式 npe 的問題,例如 binding.setUser(viewModel);
中 viewModel 為 null 時,執行時不會出現 npe。
關於 @BindingMethod
這個註解是用來關聯 SDK 中提供的控制元件的屬性和方法的,這些屬性的名稱和其 setter 不匹配,需要 @BindingMethod 來“牽繩拉線”,以便自動更新屬性的時候呼叫其對應的 setter。具體的使用方法可以參考 android.databinding.adapters.ImageViewBindingAdapter.java。
開發者一般用不到該註解。
10. 更多資料
也許本文不值得一看,但是下面這些資料則不然。