在資料庫中建立 View (Room)

Room 2.1.0 及更高版本提供對 SQLite 資料庫的 View 功能,允許使用者將查詢封裝到類別中。Room 將這些查詢支持的類別稱為 View,作用和使用 DAO 的簡單資料物件相同。

注意:
與實體(Entitiy)一樣,可以針對 View 運行 SELECT 語句。但是無法對 View 進行 INSERT, UPDATE 或 DELETE 語句。

 

Create a view


要建立 View,請將 @DatabaseView 註釋加入類別。將註釋的值設置為該類別應表示的查詢。以下代碼段提供了一個 View 範例:
@DatabaseView("SELECT user.id, user.name, user.departmentId," +  
"department.name AS departmentName FROM user " +
"INNER JOIN department ON user.departmentId = department.id")
public class UserDetail {
public long id;
public String name;
public long departmentId;
public String departmentName;
}

 

 

Associate a view with your database


要將該 View 作為資料庫的一部分,請在 @Database 註釋中加入 views 屬性
@Database(entities = {User.class}, views = {UserDetail.class},  
version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

 

Orignal From: 在資料庫中建立 View (Room)

使用 Room entities 定義資料 (Room)

使用 Room 時,可以將相關的屬性定義為實體(entities)。

對於每個實體,將會創建一個資料表來保存項目在 Database 物件中,必須透過 Database 類別中的 entities 陣列來引用實體類別。

以下示範如何定義一個實體(entity)
@Entity  
public class User {
@PrimaryKey
public int id;

public String firstName;
public String lastName;
}

為了保存屬性,Room 必須去存取它。可以透過將該屬性設定為 public 或提供 getter 和 setter 存取器。若是使用存取器必須注意該存取器必須符合 JavaBeans 的命名規則。

注意:
實體(Entity)可以有一個空構造函數(如果相應的 DAO 類別可以訪問每個持久化字段),或者一個構造函數,其參數包含與實體中的屬性匹配的類型和名稱。Room 也可以使用完整或部分構造函數,例如只接收某些屬性的構造函數。

Use a primary key


每個實體必須至少定義一個屬性作為主鍵。即使只有一個屬性,仍需要使用 @PrimaryKey 註釋來註釋該屬性。

此外,如果希望 Room 為實體分配自動 ID,可以設定 @PrimaryKey 的 autoGenerate 屬性。

如果實體具有複合主鍵,則可以使用 @Entity 註釋的 primaryKeys 屬性,如以下代碼段所示:
@Entity(primaryKeys = {"firstName", "lastName"})  
public class User {
public String firstName;
public String lastName;
}

 

Room 預設使用類別名稱作為資料表名稱。如果希望資料表使用不同的名稱,可以設定  @Entity 註釋的 tableName 屬性,如以下代碼段所示:
@Entity(tableName = "users")  
public class User {
// ...
}

注意:
資料表名稱是區分大小寫的

 

類似於 tableName,Room 使用屬性名稱作為資料庫中的列名稱。如果希望列具有不同的名稱,請將 @ColumnInfo 註釋加到屬性中,如以下代碼段所示:
@Entity(tableName = "users")  
public class User {
@PrimaryKey
public int id;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;
}

 

Ignore fileds


預設情況下,Room 會為實體中定義的每個屬性都創建一列。如果實體具有不想保存的屬性,則可以使用 @Ignore 對其進行註釋,如以下代碼段所示:
@Entity  
public class User {
@PrimaryKey
public int id;

public String firstName;
public String lastName;

@Ignore
Bitmap picture;
}

 

如果實體從另一個實體(父實體)繼承屬性,則通常使用 @Entity 屬性的 ignoredColumns:
@Entity(ignoredColumns = "picture")  
public class RemoteUser extends User {
@PrimaryKey
public int id;

public boolean hasVpn;
}

 

Provide table search support


Room 支持多種類型的註釋,可以更輕鬆地搜索資料表中的詳細信息。除非應用程序的 minSdkVersion 小於 16,否則請使用全文搜索(full-text search)。

Support full-text search


如果應用程序需要通過全文搜索(FTS)快速訪問數據庫信息,就讓實體由使用 FTS3 或 FTS4 SQLite 擴展模塊的虛擬表支持。

要使用全文搜索,必須在 Room 2.1.0 及更高版本,請將 @ Fts3 或 @ Fts4 註釋添加到給定實體,如以下代碼段所示:
// Use `@Fts3` only if your app has strict disk space requirements or if you  
// require compatibility with an older SQLite version.
@Fts4
@Entity(tableName = "users")
public class User {
// Specifying a primary key for an FTS-table-backed entity is optional, but
// if you include one, it must use this type and column name.
@PrimaryKey
@ColumnInfo(name = "rowid")
public int id;

@ColumnInfo(name = "first_name")
public String firstName;
}

注意:啟用 FTS 的資料表必須使用 INTEGER 類型的主鍵和列名''rowid''。如果 FTS 資料表支持的實體定義主鍵,則它必須使用該類型和列名稱。

 

如果資料表支持多種語言的內容,請使用 languageId 選項指定存儲每行語言信息的列:
@Fts4(languageId = "lid")  
@Entity(tableName = "users")
public class User {
// ...

@ColumnInfo(name = "lid")
int languageId;
}

 

Room 提供了其他幾個用於定義FTS支持的實體選項,包括結果排序(result ordering),tokenizer 類型(tokenizer types)和作為外部內容管理的資料表。有關這些選項的更多詳細信息,請參閱 FtsOptions 參考。

 

Index specific columns


如果應用程序必須支持不允許使用 FTS3 或 FTS4 資料表支持實體的 SDK 版本,仍然可以索引數據庫中的某些列以加快查詢速度。

要向實體添加索引,請在 @Entity 註釋中包含 indices 屬性,列出要包含在索引或複合索引中的列的名稱。如下所示
@Entity(indices = {@Index("name"),  
@Index(value = {"last_name", "address"})})
public class User {
@PrimaryKey
public int id;

public String firstName;
public String address;

@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}

有時,資料庫中的某些屬性或屬性組必須是唯一的。

可以通過將 @Index 註釋的 unique 屬性設置為 true 來強制實施此唯一性屬性。

以下代碼示例可防止表具有兩行,這些行包含 firstName 和 lastName 列的相同值集:
@Entity(indices = {@Index(value = {"first_name", "last_name"},  
unique = true)})
public class User {
@PrimaryKey
public int id;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}

 

Include AutoValue-based objects


注意:
這個特性只用於 java-based entities,不支援 Kotlin。

在 Room 2.1.0 及更高版本中,可以使用基於 Java 的不可變值類(使用 @AutoValue 進行註釋)作為數據庫中的實體。

如果實體的兩個實例的列包含相同的值,則此支持特別有用。

使用帶 @AutoValue 註釋的類別作為實體時,可以使用 @PrimaryKey,@ColumnInfo,@ Embedded 和 @Relation 註釋類別的抽象方法。但是,在使用這些註釋時,每次都必須包含 @CopyAnnotations 註釋,以便 Room 可以正確解釋方法的自動生成實現。

以下代碼段顯示了一個使用 @AutoValue 註釋類別的示例,Room 將其識別為實體:
@AutoValue  
@Entity
public abstract class User {
// Supported annotations must include `@CopyAnnotations`.
@CopyAnnotations
@PrimaryKey
public abstract long getId();

public abstract String getFirstName();
public abstract String getLastName();

// Room uses this factory method to create User objects.
public static User create(long id, String firstName, String lastName) {
return new AutoValue_User(id, firstName, lastName);
}
}

 

Define relationships between objects


由於 SQLite 是關係數據庫,因此可以指定物件之間的關係。儘管大多數對象關係映射庫允許實體對象相互引用,但 Room 明確禁止這樣做。即使不能使用直接關係,Room 仍允許在實體之間定義外鍵約束。

例如,如果有另一個名為 Book 的實體,可以使用 @ForeignKey 註釋定義其與 User 實體的關係,如以下代碼段所示:
@Entity(foreignKeys = @ForeignKey(entity = User.class,  
parentColumns = "id",
childColumns = "user_id"))
public class Book {
@PrimaryKey
public int bookId;

public String title;

@ColumnInfo(name = "user_id")
public int userId;
}

外鍵非常強大,因為它們允許指定更新引用實體時發生的情況。

例如,如果通過在 @ForeignKey 註釋中包含 onDelete = CASCADE 來刪除相應的 User 實例,則可以告訴 SQLite 刪除用戶的所有書籍。

注意:
SQLite 將 @Insert(onConflict = REPLACE)作為一組 REMOVE 和 REPLACE 操作處理,而不是單個 UPDATE 操作。這種替換衝突值的方法可能會影響外鍵約束。

 

Create nested objects


有時候希望將實體或 POJO 表達為資料庫邏輯中的一個整體,即使該物件包含多個屬性。

在這些情況下,可以使用 @Embedded 註釋來表示要分解到資料表內子字屬性的物件。然後,可以像查找其他單個列一樣查詢嵌入屬性(Embedded filed)。

例如,User 類別可以包含 Address 類別的屬性,而 Address 也有其屬性為 street,city,state 和 postCode 屬性。

User.java
@Entity  
public class User {
@PrimaryKey
public int id;

public String firstName;

@Embedded
public Address address;
}

Address.java
public class Address {  
public String street;
public String state;
public String city;

@ColumnInfo(name = "post_code")
public int postCode;
}

然後,表示 User 物件的資料表包含具有以下名稱的列:id,firstName,street,state,city 和 post_code。

注意:
嵌入屬性(Embedded filed)可以再包含其它嵌入屬性(Embedded filed)

如果實體具有多個相同類型的嵌入屬性,則可以通過設置 prefix 屬性使每個列保持唯一。然後,Room 將提供的值添加到嵌入屬性中每個列名稱的開頭。

Orignal From: 使用 Room entities 定義資料 (Room)

在本地端資料庫儲存資料 (Room)

在 Room 中有 3 個主要的元件

1.Database:


包含資料庫持有者(database holder),並作為應用程序的持久化數據基礎的主要訪問點。

使用 @Database 註釋的類別需要滿足以下條件:

  • 是一個抽象類別並繼承 RoomDatabase 類別

  • 在註釋加入與資料庫關聯的實體列表

  • 包含一個具有0個參數的抽象方法,並返回使用 @Dao 註釋的類別


在程式運行時,可以通過呼叫 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder()來獲取 Database 實體。

2.Entity:


表示為資料庫中的資料表。

3.DAO:


包含用於訪問資料庫的方法。

應用程序使用 Room 資料庫來獲取與該資料庫關聯的數據訪問對像(DAO)。接著,應用程序使用每個 DAO 從資料庫中獲取實體,並將對這些實體的任何更改保存回資料庫。最後,應用程序使用實體來獲取和設置與資料庫中的表列對應的值。

如下圖所示


以下的範例為包含一個 entity 和一個 dao 的資料庫

User.java
@Entity  
public class User {
@PrimaryKey
public int uid;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;
}

UserDao.java
@Dao  
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();

@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);

@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
User findByName(String first, String last);

@Insert
void insertAll(User... users);

@Delete
void delete(User user);
}

AppDatabase.java
@Database(entities = {User.class}, version = 1)  
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

 

下面是建立並取得 database 的實體
AppDatabase db = Room.databaseBuilder(getApplicationContext(),  
AppDatabase.class, "database-name").build();

 

注意:

如果應用程序在單進程(single process)中執行,則在實例化 AppDatabase 物件時應該套用 Singleton Pattern。因為建立 RoomDatabase 實例相當昂貴,很少需要在單進程中訪問多個實例。

如果應用程序在多進程中運行,請在數據庫構建器調用中包含 enableMultiInstanceInvalidation(),當在每個進程中都有一個 AppDatabase 實例時,可在一個進程中使共享數據庫文件無效,並且讓此無效自動傳播到其他進程中的 AppDatabase 實例。

 

Orignal From: 在本地端資料庫儲存資料 (Room)

Room 基本紀錄

什麼是 Room?

Room 是 Android 官方所提供的ORM,可快速方便的存取 SQLite。

基本上 Room 為 SQLite 提供了一個抽象層,該層透過使用SQLite 實現更強大的資料庫存取功能。

處理大量結構化數據的應用程序可以從本地持久化保存數據中獲得好處。最常見的範例是當設備無法使用網絡時緩存相關的數據,讓用戶仍然可以在離線時瀏覽該內容。之後在設備重新連上網路時,任何更改的內容都會同步到服務器。

強烈建議使用 Room 而不是 SQLite。

使用 Room 在本地端資料庫儲存資料

Orignal From: Room 基本紀錄

Two-way data binding (雙向數據綁定)

前言


使用單向數據綁定可以在屬性上設置數值,並可設定對該屬性的變化作出反應的監聽器。如下
<CheckBox  
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>

雙向數據綁定提供了此過程的捷徑。如下
<CheckBox  
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>

注意在 @={} 表示法中的 = 符號表示了同時接受資料變化以及監聽更新。

為了對數據的變化做出反應,可以讓佈局變量成為 Observable 的實現,通常是 BaseObservable,並使用 @Bindable 註釋,如下
public class LoginViewModel extends BaseObservable {  
// private Model data = ...

@Bindable
public Boolean getRememberMe() {
return data.rememberMe;
}

public void setRememberMe(Boolean value) {
// Avoids infinite loops.
if (data.rememberMe != value) {
data.rememberMe = value;

// React to the change.
saveData();

// Notify observers of a new value.
notifyPropertyChanged(BR.remember_me);
}
}
}

由於 bindable 屬性的 getter 方法稱為 getRememberMe(),因此屬性的相應 setter 方法會自動使用名稱 setRememberMe()。

關於使用 BaseObservable 和 @Bindable 的更多訊息,參考 Work with observable data objects.

Two-way data binding using custom attributes


該平台為最常見的雙向屬性和監聽器提供雙向數據綁定的實現,使用者可以將當作應用程序的一部分。

若使用者想在自定義屬性上使用雙向資料綁定則必須了解如何使用 @InverseBindingAdapter 和 @InverseBindingMethod 註解。

例如,如果要在名稱為 MyView 的自定義視圖中對 "time" 屬性啟用雙向數據綁定,必須完成以下步驟

1.註釋設置初始值的方法,並在使用 @BindingAdapter 更改時進行更新
@BindingAdapter("time")  
public static void setTime(MyView view, Time newValue) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue;
}
}

2.使用 @InverseBindingAdapter 註釋從視圖中讀取數值的方法
@InverseBindingAdapter("time")  
public static Time getTime(MyView view) {
return view.getTime();
}

此時,data binding library 知道數據變化時要執行的操作(呼叫使用 @BindingAdapter 註釋的方法)以及視圖屬性變化時調用的內容(它呼叫 InverseBindingListener)。

為此,需要在視圖上設置一個監聽器。它可以是與自定義視圖關聯的自定義監聽器,也可以是通用事件,例如失去焦點或文本更改。

將 @BindingAdapter 註釋添加到為該屬性的變化設置監聽器的方法,如下
@BindingAdapter("app:timeAttrChanged")  
public static void setListeners(
MyView view, final InverseBindingListener attrChange) {
// Set a listener for click, focus, touch, etc.
}

這個 Listener 包含了 InverseBindingListener 作為參數,可以使用 InverseBindingListener 告知 data binding library 該屬性已更改。接著,系統可以開始使用 @InverseBindingAdapter 呼叫註釋方法,依此類推。

注意每個雙向綁定都會生成一個合成事件屬性( synthetic event attribute)。此屬性與基本屬性具有相同的名稱,但具有後綴 "AttrChanged"。

合成事件屬性允許 data binding library 建立使用 @BindingAdapter 註釋的方法,以將事件監聽器連結到 View 的相應實體。

在實作中,該監聽器包括一些特殊的邏輯,包括用於單向數據綁定的監聽器。相關範例,參考TextViewBindingAdapter

 

Converters


如果綁定到 View 對象的變數需要在顯示之前進行格式化,轉換或更改,則可以使用 Converter 對象。

例如,使用顯示日期的 EditText 對象
<EditText  
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>

viewmodel.birthDate 屬性包含 Long 類型的值,因此需要使用轉換器對其進行格式化。

因為使用雙向表達式,所以還需要一個反向轉換器(inverse converter)讓 data binding library 知道如何將字符串轉換回數據類型,在本例中為 Long。

此過程通過將 @InverseMethod 註釋添加到其中一個轉換器並使此註釋引用反向轉換器來完成。如下
public class Converter {  
@InverseMethod("stringToDate")
public static String dateToString(EditText view, long oldValue,
long value) {
// Converts long to String.
}

public static long stringToDate(EditText view, String oldValue,
String value) {
// Converts String to long.
}
}

 

Infinite loops using two-way data binding


使用雙向數據綁定時,小心不要引入無限迴圈。

當用戶更改屬性時,將呼叫使用 @InverseBindingAdapter 註釋的方法,並將數值分配給 屬性。反過來,這將呼叫使用 @BindingAdapter 註釋的方法,這將觸發對使用 @InverseBindingAdapter 註釋的方法的另一個調用,依此類推。

因此,通過比較使用 @BindingAdapter 註釋的方法中的新値和舊值來打破可能的無限循環非常重要。

 

Two-way attributes


下表為平台預設的雙向綁定屬性表。














Class
Attribute(s)
Binding adapter

AdapterView
android:selectedItemPosition
android:selection

AdapterViewBindingAdapter

CalendarView
android:date
CalendarViewBindingAdapter

CompoundButton
android:checked
CompoundButtonBindingAdapter

DatePicker
android:year
android:month
android:day

DatePickerBindingAdapter

NumberPicker
android:value
NumberPickerBindingAdapter

RadioButton
android:checkedButton
RadioGroupBindingAdapter

RatingBar
android:rating
RatingBarBindingAdapter

SeekBar
android:progress
SeekBarBindingAdapter

TabHost
android:currentTab
TabHostBindingAdapter

TextView
android:text
TextViewBindingAdapter

TimePicker
android:hour
android:minute

TimePickerBindingAdapter




Orignal From: Two-way data binding (雙向數據綁定)

Bind layout views to Architecture Components (綁定佈局到架構元件)

前言


AndroidX 函式庫包含架構元件(Architecture Components),可以使用它來設計健壯,可測試和可維護的應用程序。

Data Binding Library 可與架構元件無縫協作,進一步簡化 UI 的開發。

應用程序中的 layout 可以綁定到架構元件中的數據,這些數據可以幫助管理 UI 控制器生命週期並通知數據中的更改。

以下介紹如何將架構元件合併到應用程序,以進一步使用 Data Binding Library 的好處。

 

Use LiveData to notify the UI about data changes


可以使用 LiveData 物件作為數據綁定來源,以自動通知UI有關數據更改的訊息。更多的資訊可以參考 LiveData Overview

與實現 Observable 的物件(例如 observable fields)不同,LiveData 物件了解訂閱對象的生命週期。相關的好處可以參考 The advantages of using LiveData

在 Android Studio 3.1 或之後的版本可以使用 LiveData 取代 observable fields。

要將 LiveData 與綁定類別一起使用,需要指定生命週期所有者(lifecycle owner)以定義 LiveData 對象的範圍。如下,在綁定類別初始化之後指定了 Activity 本身為生命週期所有者。
class ViewModelActivity extends AppCompatActivity {  
@Override
protected void onCreate(Bundle savedInstanceState) {
// Inflate view and obtain an instance of the binding class.
UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this);
}
}

可以使用 ViewModel元件(如 Use ViewModel to manage UI-related data)將數據綁定到佈局。在 ViewModel 中,可以使用 LiveData 對象轉換數據或合併多個數據來源。

如下示範如何在 ViewModel 中轉換數據。
class ScheduleViewModel extends ViewModel {  
LiveData username;

public ScheduleViewModel() {
String result = Repository.userName;
userName = Transformations.map(result, result -> result.value);
}

 

Use VeiwModel to manage UI-related data


Data binding library 和 ViewModel 可以無縫協作,以顯示數據並對其更改做出反應。

將 ViewModel 元件與 data binding library 一起使用,可以將 UI 邏輯從佈局移動到元件中,讓元件更易於測試。

Data binding library 可確保在需要時綁定和取消綁定數據來源。

更多的訊息可以參考 ViewModel Overview

要將 ViewModel 與 data binding library 一起使用,必須實體化繼承自 ViewModel 的元件,取得綁定類別的實體,並將 ViewModel 元件分配給綁定類別中的屬性。如下
class ViewModelActivity extends AppCompatActivity {  
@Override
protected void onCreate(Bundle savedInstanceState) {
// Obtain the ViewModel component.
UserModel userModel = ViewModelProviders.of(getActivity())
.get(UserModel.class);

// Inflate view and obtain an instance of the binding class.
UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

// Assign the component to a property in the binding class.
binding.viewmodel = userModel;
}
}

在 layout 中,使用表達式將 ViewModel 元件的屬性和方法分配給相對應的視圖。
<CheckBox  
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />

 

Use an Observable ViewModel for more control over binding adapters


可以使用實現 Observable 的 ViewModel 來通知其他應用程序元件有關數據更改的信息,類似於使用 LiveData 對象的方式。

在某些情況下,可能更適合使用 ViewModel 元件來實現 Observable 介面而不是使用 LiveData 對象,即使沒有 LiveData 的生命週期管理功能。

使用實現 Observable 的 ViewModel 元件可以更好地控制應用程序中的綁定適配器。此模式可以在數據變化時更容易控制通知,允許指定自定義方法以在雙向數據綁定中設置屬性的值。

要實現可觀察的 ViewModel 元件,必須創建一個繼承 ViewModel 並實現 Observable 介面的類別。

當觀察者使用 addOnPropertyChangedCallback() 和 removeOnPropertyChangedCallback()方法訂閱或取消訂閱通知時,使用者可以提供自定義邏輯。

還可以提供在 notifyPropertyChanged()方法中屬性更改時執行的自定義邏輯。

以下示範如何實現可觀察的 ViewModel
/**  
* A ViewModel that is also an Observable,
* to be used with the Data Binding Library.
*/
class ObservableViewModel extends ViewModel implements Observable {
private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();

@Override
protected void addOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback) {
callbacks.add(callback);
}

@Override
protected void removeOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback) {
callbacks.remove(callback);
}

/**
* Notifies observers that all properties of this instance have changed.
*/
void notifyChange() {
callbacks.notifyCallbacks(this, 0, null);
}

/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
void notifyPropertyChanged(int fieldId) {
callbacks.notifyCallbacks(this, fieldId, null);
}
}

 

Orignal From: Bind layout views to Architecture Components (綁定佈局到架構元件)

Binding adapters (綁定轉接器)

前言


Binding Adapter 主要用來呼叫適當方式來設定數值。一個例子為透過 setText 方法來設定屬性值,另一個例子為透過呼叫 setOnClickListener 方法來設定事件監聽器。

Data Binding Library 讓使用者指定調用方法來設定數值,客製化綁定邏輯,並使用適配器指定返回對象的類型。

Setting attribute values


每當綁定值發生更改時,綁定類別必須使用綁定表達式在視圖上調用 setter 方法。使用者可以允許 data binding library 自動決定哪個方法,顯式的聲明方法或提供自定義邏輯來選擇方法。

 

Automatic method selection


對於一個命名為 example 的屬性,data binding library 會嘗試尋找名稱為 setExample(arg) 的方法,其方法接受兼容的參數。

只有屬性的名稱和型別會被用於搜尋,屬性的名稱空間則不會被考慮。

例如,對於 android:text="@{user.name}" 表達式,data binding library 會尋找一個 setText(arg) 方法,該方法的參數(arg)的型別為 user.getName 方法所回傳的。例如,若 user.getName 方法回傳的型態為 String,則 data binding library 會尋找 setText 方法,其方法接受的參數型態為 String。若 user.getName 方法回傳的型態為 int,則 data binding library 會尋找 setText 方法,其方法接受的參數型態為 int。

表達式必須回傳正確的型態,如有必要可以進行轉型的動作。即使沒有屬性符合給定的名稱,數據綁定仍然工作。之後,可以使用數據綁定為任何 setter 建立屬性。

例如,類別 DrawerLayout 沒有任何屬性,但有很多 setter。

下方的佈局自動使用 setScrimColor(int) 和 setDrawerListener(DrawerListener) 方法作為 app:scrimColor 和 app:drawerListener 屬性的 setter
<android.support.v4.widget.DrawerLayout  
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">

 

Specify a custom method name


有些屬性的 setter 並不符合名稱,在這種情況下一個屬性可以藉由 BindingMethods 註解去連結一個 setter。

註釋與類別一起使用,可以包含多個 BindingMethod 註釋,每個註釋方法一個註釋。綁定方法可以添加到應用程序中任何類別的註釋。如下方範例

android:tint 屬性和 setImageTintList(ColorStateList) 方法結合,而不是和 setTint 方法結合。
@BindingMethods({  
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})

大多數情況下不需要重命名 setter。已使用名稱約定實現的屬性可自動查找相對應的方法。

 

Provide custom logic


有些屬性需要客製化綁定邏輯。例如 android:paddingLeft 屬性並沒有其相對應的 setter,代替的是 setPadding(left, top, right, bottom) 方法。

一個 static binding adapter 方法加上 BindingAdapter 註釋允許使用者自定義如何調用屬性的 setter。

Android framework 類別的屬性已經有 BindingAdapter 註釋。如下為 paddingLeft 屬性的 binding adapter
@BindingAdapter("android:paddingLeft")  
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}

參數的型態相當重要,第一個參數決定與屬性相關連的視圖型別。第二個參數確定給定屬性的綁定表達式所接受的型別。

Binding adapter 相當有用於其他客製化的類型。如一個客製化的 loader 可以從 worker thread 被呼叫用來讀取圖像。

當發生衝突時,使用者自定義的 binding adapter 會覆蓋過原先的 adapter。

也可以有多個 adapter 用來接收多個屬性。如下
@BindingAdapter({"imageUrl", "error"})  
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.get().load(url).error(error).into(view);
}

如下,可以在 layout 使用 adapter。

注意 @drawable/venueError 參考到 App 中的資源。冒號包括住@{}的寫法為合法的綁定表達式。
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

注意 data binding library 會忽略自定義命名空間以進行匹配。

當 imageUrl 和 error 都被 ImageView 物件使用時,Adapter 將會被呼叫。

若想讓屬性其中之一被使用時就呼叫 adapter,可以設定 requireAll 為 false,如下
@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);
}
}

Binding Adapter method 可以選擇在其處理程序中使用舊值。使用舊值和新值的方法應首先聲明屬性的所有舊值。如下
@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());
}
}

 

Event handler 只能與帶有一個抽象方法的接口或抽象類別一起使用。如下
@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);
}
}
}

 

在 layout 中使用這個 event handler,如下
<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

 

當偵聽器具有多個方法時,必須將其拆分為多個偵聽器。

例如 View.OnAttachStateChangeListener 有 2 個方法為 onViewAttachedToWindow(View) 和 onViewDetachedFromWindow(View)。就有 2 個介面去區分它們的 attribute 和 handler。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)  
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}

 

因為更改一個偵聽器也會影響另一個偵聽器,所以需要一個適用於任一屬性或適用於兩者的適配器。

可以透過設定 requireAll 為 false 表示並不是每個屬性都必須指定給表達式。
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)  
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}

OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}

上面的示例比正常情況稍微複雜一些,因為 View 使用 addOnAttachStateChangeListener()和 removeOnAttachStateChangeListener()方法而不是 OnAttachStateChangeListener 的 setter 方法。

android.databinding.adapters.ListenerUtil 類別有助於跟踪以前的偵聽器,以便可以在綁定適配器中刪除它們。

藉由在 OnViewDetachedFromWindow 和 OnViewAttachedToWindow 方法加入 @TargetApi(VERSION_CODES.HONEYCOMB_MR1),data binding library 只會在裝置運行為 Android3.1 或以上的版本產生 listener。

 

Object conversions


Automatic object conversion


從綁定表達式返回 Object 時,data binding library 會選擇用於設置屬性值的方法。Object 會被強制轉換為所選方法的參數類型。

在使用 ObservableMap 存儲數據的應用程序,此行為很方便,如以下示例所示
<TextView  
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

也可以使用 object.key 的方式在 Map 中參考數值,如上例的 @{userMap["lastName"]} 可以使用 @{userMap.lastName} 來取代。

在表達式中的 userMap 會回傳一個數值,該數值將會被自動轉型為 setText(CharSequence) 方法的參數類型,該方法用來設定 android:text 屬型的値。

如果參數類型不明確,則必須在表達式中強制轉換返回類型。

Custom conversions


在某些情況下,特定類型之間需要自定義轉換。例如 android:background 需要 Drawable,但指定的顏色值是整數。

以下範例顯示需要 Drawable 的屬性,但是提供了一個整數
<View  
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

每當需要 Drawable 並返回一個整數時,int 應該轉換為 ColorDrawable。可以使用帶有BindingConversion 註釋的靜態方法完成轉換,如下所示
@BindingConversion  
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}

但是,綁定表達式中提供的值類型必須一致。不能在同一表達式中使用不同的類型,如下
<View  
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

 

Orignal From: Binding adapters (綁定轉接器)

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Affiliate Network Reviews