[TOC]
# 生成綁定類
引入數據綁定庫后,系統會為每個布局文件生成一個綁定類,綁定類會將布局變量與布局中的視圖關聯起來。創建綁定對象時,會為布局變量自動生成setter和getter方法。
```xml
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
// 布局變量
<data
class="CustomBinding">
<import type="java.util.List" />
<variable
name="stringList"
type="List<String>" />
</data>
// 布局視圖
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
//...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```
生成綁定類共有以下幾種方式,以Activity為示例:
**方式1:**
```java
CustomBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
```
**方式2:**
```java
CustomBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.activity_main, null, false);
setContentView(binding.getRoot());
```
**方式3:**
```java
View view = getLayoutInflater().inflate(R.layout.activity_main, null, false);
CustomBinding binding = DataBindingUtil.bind(view);
setContentView(binding.getRoot());
```
**方式4:**
此方法最終調用的是DataBindingUtil的bind方法。
```java
View view = getLayoutInflater().inflate(R.layout.activity_main, null, false);
CustomBinding binding = CustomBinding.bind(view);
setContentView(binding.getRoot());
```
**方式5:**
此方法最終調用的是DataBindingUtil的inflate方法。且此方法已經Deprecated。
```java
CustomBinding binding = CustomBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
```
以上5種方法中,方法1-3調用DataBindingUtil的方法創建綁定對象;方法4-5調用綁定類的方法創建綁定對象,方法4-5中綁定類的方法最終調用的也是DataBindingUtil的方法。所以推薦直接調用DataBindingUtil的方法。
## 源碼分析
下面重點關注方法1和方法2,看看它們的源碼。
**1、DataBindingUtil的setContentView方法**
方法1DataBindingUtil的setContentView方法,僅適用于Activity的綁定,源碼如下:
```java
// DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity, int layoutId) {
return setContentView(activity, layoutId, sDefaultComponent);
}
// DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId, @Nullable DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
```
DataBindingUtil的setContentView方法會自動為Activity設置ContentView。
重點關注下bindToAddedViews方法,源碼如下:
```java
// DataBindingUtil.java
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
ViewGroup parent, int startChildren, int layoutId) {
final int endChildren = parent.getChildCount();
final int childrenAdded = endChildren - startChildren;
if (childrenAdded == 1) {
final View childView = parent.getChildAt(endChildren - 1);
return bind(component, childView, layoutId);
} else {
final View[] children = new View[childrenAdded];
for (int i = 0; i < childrenAdded; i++) {
children[i] = parent.getChildAt(i + startChildren);
}
return bind(component, children, layoutId);
}
}
```
bindToAddedViews的第二個參數是parent,第三個參數是startChildren,需要注意,bindToAddedViews方法綁定的是parent父布局內部的子View或子View集合。對于Activity來說,parent就是contentView,子View也就是我們Activity布局文件的根布局。
**2、DataBindingUtil的inflate方法**
方法2DataBindingUtil的inflate方法適用于Activity、Fragment、Adapter等。
```java
// DataBindingUtil.java
public static <T extends ViewDataBinding> T inflate(@NonNull LayoutInflater inflater,
int layoutId, @Nullable ViewGroup parent, boolean attachToParent) {
return inflate(inflater, layoutId, parent, attachToParent, sDefaultComponent);
}
// DataBindingUtil.java
public static <T extends ViewDataBinding> T inflate(
@NonNull LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent,
boolean attachToParent, @Nullable DataBindingComponent bindingComponent) {
final boolean useChildren = parent != null && attachToParent;
// 1、startChildren是目標加載View在parent中的index
final int startChildren = useChildren ? parent.getChildCount() : 0;
// root不空且附加到root時,返回的是root(當然View已經附加到里面了);否則返回的是目標加載View
final View view = inflater.inflate(layoutId, parent, attachToParent);
if (useChildren) {
// 1.1 綁定的是parent父布局內部的子View或子View集合
return bindToAddedViews(bindingComponent, parent, startChildren, layoutId);
} else {
// 2、直接綁定當前布局文件加載出來的View視圖
return bind(bindingComponent, view, layoutId);
}
}
```
1、當parent不為null,且附加到parent時,調用bindToAddedViews方法,傳遞過去的參數是parent和目標View的index。最終綁定parent父布局的子View,也就是我們的目標加載View
2、當parent為null,或不添加到parent時,直接調用bind方法,綁定當前布局文件加載出來的View視圖。
兩種情況的區別在于,綁定出的View是否具有LayoutParams。
# 綁定表達式
## 表達式語言
### 引用
**1、對象屬性引用**
表達式可以使用以下格式在類中引用對象的屬性,對于字段、getter 和[`ObservableField`](https://developer.android.com/reference/androidx/databinding/ObservableField)對象都一樣。
```xml
<data>
<variable
name="user"
type="com.markxu.notitest.User" />
</data>
android:text="@{user.lastName}"
```
**2、集合引用**
```xml
<data>
<import type="java.util.List" />
<variable
name="stringList"
type="List<String>" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{stringList.get(0)}'
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
```
注意:`<`必須轉義書寫為`<`
**3、資源引用**
```xml
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
```
### 避免 Null 指針異常
生成的數據綁定代碼會自動檢查有沒有`null`值并避免出現 Null 指針異常。例如,在表達式`@{user.name}`中,如果`user`為 Null,則為`user.name`分配默認值`null`。
### Null合并運算符
如果左邊運算數不是`null`,則 Null 合并運算符 (`??`) 選擇左邊運算數,如果左邊運算數為`null` ,則選擇右邊運算數。
```xml
android:text="@{user.displayName ?? user.lastName}"
等價于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
```
## 事件處理
通過數據綁定,可以編寫從視圖分派的表達式處理事件。有方法引用和監聽器綁定兩種機制。
使用方法引用方式進行事件處理時,在編譯時就會實現監聽器。如果方法不存在或簽名不正確,會收到編譯錯誤;使用監聽器綁定方式進行事件處理時,事件發生時才會實現監聽器。
首先熟悉[Lambda表達式的使用](http://www.androidwiki.site/1569131#Lambda_1),理解事件處理會更容易些。
### 方法引用
當表達式求值結果為方法引用時,數據綁定會將方法引用和所有者對象封裝到監聽器中,并在目標視圖上設置該監聽器。
```java
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
```
```xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.MyHandlers"/>
<variable name="user" type="com.example.User"/>
</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="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>
```
**備注**
使用Lambda表達式時,當已經有現成的方法可以完成你想要傳遞到其他代碼的某個動作時,可以直接使用方法引用。
表達式`handlers::onClickFriend`就是一個方法引用,等價于lambda表達式`(v) -> handlers.onClickFriend(v)`,具體可參考[http://www.androidwiki.site/1569131#\_48](http://www.androidwiki.site/1569131#_48)
### 監聽器綁定
事件發生時對lambda表達式進行求值。
```java
public class Presenter {
public void onSaveClick(Task task){}
}
```
```xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>
```
監聽器綁定中,可以忽略方法的所有參數`android:onClick="@{() -> presenter.onSaveClick(task)}"`,也可以命名所有參數`android:onClick="@{(view) -> presenter.onSaveClick(task)}"`
**備注**
使用Lambda表達式時,不能忽略方法的參數
# 使用可觀察數據對象
任何 plain-old 對象都可用于數據綁定,但修改對象不會自動使界面更新。通過數據綁定,數據對象可在其數據發生更改時通知其他對象,自動更新界面。
## 可觀察對象
將我們的對象繼承BaseObservable即可實現可觀察功能。
```java
private static 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(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
```
向getter分配`Bindable`,在setter中調用`notifyPropertyChanged()`方法即可。
## 可觀察字段
除了使用繼承BaseObservable的方式實現可觀察對象,還可以直接使用可觀察字段。
```java
private static class User {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
```
Android系統提供了如下可觀察字段:
* [`ObservableBoolean`](https://developer.android.com/reference/androidx/databinding/ObservableBoolean)
* [`ObservableByte`](https://developer.android.com/reference/androidx/databinding/ObservableByte)
* [`ObservableChar`](https://developer.android.com/reference/androidx/databinding/ObservableChar)
* [`ObservableShort`](https://developer.android.com/reference/androidx/databinding/ObservableShort)
* [`ObservableInt`](https://developer.android.com/reference/androidx/databinding/ObservableInt)
* [`ObservableLong`](https://developer.android.com/reference/androidx/databinding/ObservableLong)
* [`ObservableFloat`](https://developer.android.com/reference/androidx/databinding/ObservableFloat)
* [`ObservableDouble`](https://developer.android.com/reference/androidx/databinding/ObservableDouble)
* [`ObservableParcelable`](https://developer.android.com/reference/androidx/databinding/ObservableParcelable)
* [`ObservableArrayMap`](https://developer.android.com/reference/androidx/databinding/ObservableArrayMap)
* [`ObservableArrayList`](https://developer.android.com/reference/androidx/databinding/ObservableArrayList)
# 綁定適配器
## 設置綁定值
綁定適配器的作用是,View的屬性綁定的值發生變化時,告訴數據綁定庫應該調用哪個方法來更新View。
**注意:**
View的屬性在初次綁定時,以及綁定值發生變化時,數據綁定庫都會調用View的方法更新UI。
```java
TextView tvName = findViewById(R.id.tv_name);
tvName.setText("Mark");
```
不使用數據綁定庫時,更新UI控件需要首先通過findViewById方法找到控件,再調用控件的setter方法改變屬性更新UI。如上面例子中,先通過findViewById找到TextView控件,再調用TextView的setText方法,來給TextView的text屬性賦值,以更新TextView所展示的文案。
```xml
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
android:textSize="24sp" />
```
使用數據綁定庫后,只要綁定值`user.name`發生更改,綁定類就會在視圖上調用setter方法(相當于自動調用TextView的setText方法),來更新UI,前面的賦值更新UI操作全部由數據綁定庫幫我們完成了。
綁定值發生變化時,數據綁定庫調用哪個方法來更新控件呢,有如下三種情況:
**1、讓數據綁定庫自己決定調用哪個方法**
還以`android:text="@{user.name}"`為例,綁定值發生變更時,數據綁定庫會自動在TextView類中查找`setText`方法,方法參數需要與`user.name`的值一致。在TextView類中找到`setText(CharSequence text)`方法后,自動調用。
**2、指定自定義方法名稱**
View的某些特性擁有名稱不符的 setter時,可以指定想讓數據綁定庫調用的方法。
```java
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
```
上面示例中,`android:tint`屬性綁定的值變化時,數據綁定庫會去控件類中尋找`setImageTintList(ImageView imageView)`方法并調用,而不是去尋找`setTint()`方法。
**3、提供自定義邏輯**
某些情況下,沒有現成的方法供數據綁定庫去調用,我們可以提供自定義邏輯。
例如,TextView的`android:paddingLeft`特性沒有關聯的 setter,也就是TextView沒有setPaddingLeft方法,但是TextView提供了`setPadding(left, top, right, bottom)`方法。此時可以使用BindingAdapter來創建綁定適配器:
```java
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int paddingLeft) {
view.setPadding(paddingLeft,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
```
參數類型非常重要。第一個參數確定與特性關聯的視圖類型(TextView),第二個參數確定View屬性綁定表達式中接受的類型(`android:paddingLeft`對應的類型)。
即,TextView的`android:paddingLeft`綁定的值發生變更時,數據綁定庫根據BindingAdapter注解找到setPaddingLeft方法并調用,會把待更新的View和變更后的值傳遞過去。
**接收多個特性的適配器:**
```java
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.get().load(url).error(error).into(view);
}
```
```xml
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}" />
```
如果ImageView對象同時設置了`imageUrl`和`error`,并且`imageUrl`是字符串,`error`是Drawable,則會調用適配器。如果希望在設置了任意特性時調用適配器,則可以將適配器的`requireAll`設置為`false`,如下示例所示:
```java
@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
```
> 備注:
數據綁定庫在匹配時會忽略自定義命名空間。
## 對象轉換
**1、自動對象轉換**
```xml
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
```
View屬性綁定值返回的為Object時,數據綁定庫會自動進行對象轉換,將Object對象轉換為text屬性對應的setText方法所需要的參數類型,也就是String類型。
**2、自定義轉換**
```xml
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
```
某些情況下,View屬性綁定值返回的類型可以自定義轉換。上面例子中,background屬性需要的是Drawable類型的值,但color是整型值,此時可以使用`BindingConversion`注解完成轉換:
```java
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
```
綁定表達式中提供的值類型必須保持一致。您不能在同一個表達式中使用不同的類型,如以下示例所示:
```xml
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
```
自定義轉換的應用場景:假如某個控件需要一個格式化好的時間,但是目前只有一個`Date`類型額變量,除了轉化完成之后再進行設置時間,現在還可以使用自定義轉換的方式,先設置再轉換。
# 總結
在沒有使用數據綁定庫的時候,更新UI需要先找到控件,再調用控件的方法更改控件屬性來更新UI。引入數據綁定庫后,數據綁定庫直接將控件的屬性值和綁定表達式的值(數據源)進行綁定,當綁定表達式值變更時,數據綁定庫會自動去尋找控件對象的合適的方法,自動調用,來更新UI。
因此本文的主要內容也就是:怎么將視圖和數據源(綁定表達式)進行綁定,綁定表達式的規則,賦予數據源變化時數據綁定庫自動調用控件方法的能力,幫助數據綁定庫選擇應該調用哪個方法,數據源返回對象的轉換。
# 參考文檔
[官方文檔:綁定適配器](https://developer.android.com/topic/libraries/data-binding/binding-adapters#object-conversions)
[Android官方數據綁定框架DataBinding(二)](https://blog.csdn.net/qibin0506/article/details/47720125)
- 導讀
- Java知識
- Java基本程序設計結構
- 【基礎知識】Java基礎
- 【源碼分析】Okio
- 【源碼分析】深入理解i++和++i
- 【專題分析】JVM與GC
- 【面試清單】Java基本程序設計結構
- 對象與類
- 【基礎知識】對象與類
- 【專題分析】Java類加載過程
- 【面試清單】對象與類
- 泛型
- 【基礎知識】泛型
- 【面試清單】泛型
- 集合
- 【基礎知識】集合
- 【源碼分析】SparseArray
- 【面試清單】集合
- 多線程
- 【基礎知識】多線程
- 【源碼分析】ThreadPoolExecutor源碼分析
- 【專題分析】volatile關鍵字
- 【面試清單】多線程
- Java新特性
- 【專題分析】Lambda表達式
- 【專題分析】注解
- 【面試清單】Java新特性
- Effective Java筆記
- Android知識
- Activity
- 【基礎知識】Activity
- 【專題分析】運行時權限
- 【專題分析】使用Intent打開三方應用
- 【源碼分析】Activity的工作過程
- 【面試清單】Activity
- 架構組件
- 【專題分析】MVC、MVP與MVVM
- 【專題分析】數據綁定
- 【面試清單】架構組件
- 界面
- 【專題分析】自定義View
- 【專題分析】ImageView的ScaleType屬性
- 【專題分析】ConstraintLayout 使用
- 【專題分析】搞懂點九圖
- 【專題分析】Adapter
- 【源碼分析】LayoutInflater
- 【源碼分析】ViewStub
- 【源碼分析】View三大流程
- 【源碼分析】觸摸事件分發機制
- 【源碼分析】按鍵事件分發機制
- 【源碼分析】Android窗口機制
- 【面試清單】界面
- 動畫和過渡
- 【基礎知識】動畫和過渡
- 【面試清單】動畫和過渡
- 圖片和圖形
- 【專題分析】圖片加載
- 【面試清單】圖片和圖形
- 后臺任務
- 應用數據和文件
- 基于網絡的內容
- 多線程與多進程
- 【基礎知識】多線程與多進程
- 【源碼分析】Handler
- 【源碼分析】AsyncTask
- 【專題分析】Service
- 【源碼分析】Parcelable
- 【專題分析】Binder
- 【源碼分析】Messenger
- 【面試清單】多線程與多進程
- 應用優化
- 【專題分析】布局優化
- 【專題分析】繪制優化
- 【專題分析】內存優化
- 【專題分析】啟動優化
- 【專題分析】電池優化
- 【專題分析】包大小優化
- 【面試清單】應用優化
- Android新特性
- 【專題分析】狀態欄、ActionBar和導航欄
- 【專題分析】應用圖標、通知欄適配
- 【專題分析】Android新版本重要變更
- 【專題分析】唯一標識符的最佳做法
- 開源庫源碼分析
- 【源碼分析】BaseRecyclerViewAdapterHelper
- 【源碼分析】ButterKnife
- 【源碼分析】Dagger2
- 【源碼分析】EventBus3(一)
- 【源碼分析】EventBus3(二)
- 【源碼分析】Glide
- 【源碼分析】OkHttp
- 【源碼分析】Retrofit
- 其他知識
- Flutter
- 原生開發與跨平臺開發
- 整體歸納
- 狀態及狀態管理
- 零碎知識點
- 添加Flutter到現有應用
- Git知識
- Git命令
- .gitignore文件
- 設計模式
- 創建型模式
- 結構型模式
- 行為型模式
- RxJava
- 基礎
- Linux知識
- 環境變量
- Linux命令
- ADB命令
- 算法
- 常見數據結構及實現
- 數組
- 排序算法
- 鏈表
- 二叉樹
- 棧和隊列
- 算法時間復雜度
- 常見算法思想
- 其他技術
- 正則表達式
- 編碼格式
- HTTP與HTTPS
- 【面試清單】其他知識
- 開發歸納
- Android零碎問題
- 其他零碎問題
- 開發思路