?LiveData 是對可觀察數據的封裝。不像其他可觀察對象(例如 ObservableField) , LiveData 可以感知到生命周期。這就意味著它可以關聯到其他擁有生命周期的組件上,比如 Activity、Fragment 或者 Service。這種感知,可以確保 LinveData 的更新只發生在一個組件的活動
狀態上。如下圖所示:
?對于一個觀察者類而言,所謂的激活狀態就是 STARTED或者 RESUMED 狀態。非激活狀態并不更新。
?對于 Activity 來說,在 onStart 之后,到 onPause 之前,就是 STARTED;在 onResume 調用之后,就是 RESUMED 狀態。
?通常,我們總是定義一個實現了 LifecyclerOwner 接口對象作為觀察者。這種關系,會使得其在 DESTROY 狀態時,自動移除對數的觀察。
LiveData 的優勢
使用 LiveData 有以下優勢:
- 確保 UI 和當前的數據狀態匹配:LiveData 提供了一種觀察者模式。當觀察者的生命周期狀態發生變化時,它會適時更新將數據更新到 UI 上。而并非是任何時候,都會對 UI 進行更新。
- 避免內存泄漏:觀察者是一個 Lifecycle 對象。當 LiveData 所關聯的觀察者被銷毀時,LiveData 會自動清理自己。
- 避免因 stop activity 造成的奔潰:當觀察者對象處于非活動狀態時,比如 activity 返回到回退棧中,此時,它將無法接收到 LiveData 的數據更新事件。
- 不用手動處理生命周期:UI 組件觀察相關的數據,但是并不會主動停止或者繼續這種觀察。當觀察者生命周期發生變化時,LiveData 會自動管理自己。
- 總是更新到最新的數據:當組件從 非活動 狀態轉換到 活動 狀態時,他講更新到最新的數據。
- 正確的處理 configuration 的變化:當 activity 或者 fragment 由于 configuration(比如說屏幕旋轉) 的變化而被創建時,它會自動接收到最新的可用數據。
- 資源共享:我們可以使用單例模式繼承一個 LiveData,當然將它綁定到一個系統服務中,這種這個 LiveData 就可以共享了。
LiveData 的使用
- 首先,創建一個持有數據的 LiveData 對象。這一步通常是在 ViewModel 中完成。
- 創建一個 Observer 對象,并定義其 onChange() 方法。該方法將控制在 LiveData 所持有的數據發生變化時,觀察者將發生怎樣的變化。我們通常創建在 UI controller 中創建 Observer。而這類 UI controller 諸如 activity 和 fragment。
- 通過 observe() 方法,將 Observer(觀察者)和 LiveData(被觀察者)綁定在一起。這樣以來,當 LiveData 數據發生變化時,只要 Observer 處于 活動 狀態,將自動通知 Observer 。
創建 LiveData 對象
?LiveData 可以包裹任何數據,包括集合類,比如 List。LiveData 通常存儲在 ViewModel 中,通過 getter 方法提供給觀察者。
public class UserViewModel extends ViewModel {
MutableLiveData<String> userName;
UserViewModel(){
userName = new MutableLiveData<>();
}
public LiveData<String> getUserName(){
return userName;
}
public void setUserName(String name){
userName.setValue(name);
}
}
?綜上,我們看到 UI controller,比如 activity 或者 fragment 僅僅負責顯示數據,而不再管理數據狀態。如此一來,將大大避免了 UI controller 的臃腫。
訂閱 LiveData 對象
?通常,組件的 onCreate() 方法,是個合適的地方以建立對 LiveData 的觀察或者說是訂閱,理由如下:
- onCreate() 方法在創建的時候,只會調用一次。
- 確保 UI controller 處于 活動 狀態時,能夠有數據顯示。
?LiveData 只會在數據變化,同時觀察者處于 活動 狀態時,才會通知觀察者更新。當然,第一次初始顯示數據除外,數據被初始化,直接通知處于 活動狀態的 UI controller 進行數據更新。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
mainViewModel.getEncryptedFileNum().observe(this, num -> {
encryptedFileNumText.setText(String.format("文件 %d 個", num));
});
}
?當 observe() 方法調用后,onChange() 方法被立即調用,為 encyptedFileNumText 提供最新的值。隨后,只有 mainViewModel 中的 encryptedFileNum 發生變化,且該 UI controller 處于 活動 狀態,encyptedFileNumText 才會更新相應 UI。
更新 LiveData 對象
?LiveData 本身沒有公開可用的方法用以更新數據。MultableLiveData 則暴露了 setValue(T) 和 postValue(T) 方法來更新 LiveData 中的數據。注意,setValue 方法用于在主線程中更新值,而 postValue 則用于在工作線程中更新值。
private MutableLiveData<String> addressName ;
public void setAddressName(String name) {
addressName.setValue(name);
}
one-way data binding VS two-way data binding
?在單向綁定中,我們通過改變 LiveData 中的值,來更新 UI 。通常,我們還需要當用戶對 UI 進行了操作之后,所帶了的變化能反饋到 LiveData 的值上,即自動更新 LiveData 中的值。這一點,在 LiveData 中很容易做到。
單向綁定:
<CheckBox
android:layout_width="18dp"
android:layout_height="18dp"
android:background="@drawable/checkbox_style"
android:button="@null"
android:checked="@{pickerBean.selected}"
android:visibility="@{pickerBean.checkVisible? View.VISIBLE:View.GONE,default=visible}"/>
雙向綁定:
<CheckBox
android:layout_width="18dp"
android:layout_height="18dp"
android:background="@drawable/checkbox_style"
android:button="@null"
android:checked="@={pickerBean.selected}"
android:visibility="@{pickerBean.checkVisible? View.VISIBLE:View.GONE,default=visible}"/>
?注意,單向綁定和雙向綁定在 XML 中的唯一區別,就是 android:checked="@={pickerBean.selected}" 中 @ 后面是否有等號。
使用自定義屬性進行雙向綁定
?上個代碼塊中,我們對 checked 屬性使用了雙向綁定。那么,如果是我們自定義的屬性該如何處理?
?為了達到這個目的,需要使用 @InverseBindingAdapter 和 @InverseBindingMethod 注解。
?以為 MyView 綁定設置 時間 為例。首先,需要使用 @BindingAdapter
@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue;
}
}
?然后,使用 @InverseBindingAdapter
注解,告訴它當 MyView 的屬性發生變化時,該調用哪個方法:
@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
return view.getTime();
}
?應當注意,當使用雙向綁定時,不要發生的無限調用的陷阱。當用戶改變了 View 的屬性,@InverseBindingAdapter
被調用了。LiveData 中的值發生了變化,這將導致 @BindingAdapter
所注解的方法被調用。如此一來,可能會在 @InverseBindingAdapter
和 @BindingAdapter
兩個注解方法中無限循環下去。為了防止這種事情發生,可以參考上述 setTime
方法中的應用。
應用場景
?觀察者模式的應用場景本身就很豐富。訂閱-發布,通過消息
或者說事件
將組件之間,組件和數據之間關聯起來,這種應用體驗非常友好。業務邏輯將更加清楚;同時,將少大量的冗余代碼,使開發者更加關注和處理業務邏輯。以下,記錄一些實例,做一些展開說明。
在 Room 中使用
?Room 是 Google 提供的組件庫之一,是對 SQLite 的封裝。它對 LiveData 的支持,使得操作數據庫的數據,可以直接反應到為用戶提供的 UI 展示上。進一步說,它的查詢方法可以返回一個 LiveData 對象,這個對象的泛型可以是基礎類型的包裝類,例如 Integer 、Boolean、String、Long 這些包裝類,也可以是 List。
@Query(" SELECT " +
" a.* ," +
" b.transStatus , " +
" b.fileLength , " +
" b.progress , " +
" b.needDecrypted , " +
" b.id as transId, " +
" b.uuid as transUuid, " +
" b.localFilePath as transPath , " +
" MAX(b.date) as transDate " +
" FROM FileShareEntity a " +
" LEFT JOIN FileTransEntity b " +
" ON a.uuid = b.uuid " +
" WHERE a.isRec == 1 AND a.gid=:gid" +
" group by a.uuid order by a.date desc"
)
LiveData<List<FileShareSendItem>> getFileShareSendItems(String gid);
?通過查詢,得到了一個 LiveData 對象,然后通過 ViewModel,將其和上層 UI 綁定在一起。
public class ShareSendModule extends AndroidViewModel {
...
LiveData<List<FileShareSendItem>> getFileShareSendItems(String gid) {
return shareDao.getFileShareSendItems(gid);
}
...
}
?最后,在 Fragment 中完成綁定(訂閱):
module.getFileShareSendItems(gid).observe(this, adapter::setData);
?此時,當 List 數據發生任何變化,如果 Fragment 處于活動
狀態,就會被更新。注意到這里的 setData 方法,將更改 adapter 中的數據,結合 DiffUtil.Callback
,RecyclerView 的使用將變得非常非常清爽。
在 RecyclerView 中使用
?其實上面已經提到了 Room 和 RecyclerView 的結合。我們可以做進一步的綁定。將 List 中的數據和每個 Item 綁定在一起。直接操作數據變化,不在單獨處理 UI 展示。
public AddressAdapter(AppCompatActivity activity) {
addressModel = new AddressModel();
addressModel.getAddresses().observe(activity, addressEntities -> {
if (mItems.size() != 0) {
AddressDiffCallback postDiffCallback = new AddressDiffCallback(mItems, addressEntities);
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(postDiffCallback, true);
transformEntities2Beans(addressEntities, mItems);
diffResult.dispatchUpdatesTo(this);
// notifyDataSetChanged();
} else {
transformEntities2Beans(addressEntities, mItems);
notifyDataSetChanged();
}
});
setHasStableIds(true); // this is required for swiping feature.
mItems = new ArrayList<>();
}
@NonNull
@Override
public AddressViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == ITEM_TYPE_NORMAL) {
ActivityAddressItemBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.activity_address_item, parent, false);
binding.setLifecycleOwner((LifecycleOwner) parent.getContext());
return new AddressViewHolder(binding);
} else {
View header = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_address_item_add, null);
return new AddressViewHolder(header);
}
}
@Override
public void onBindViewHolder(@NonNull AddressViewHolder holder, int position) {
AddressBean item = mItems.get(position);
holder.bind(item);
}
@Override
public int getItemCount() {
return mItems.size();
}
class AddressViewHolder extends RecyclerView.ViewHolder {
ActivityAddressItemBinding binding;
private boolean isHeader;
AddressViewHolder(View root) {
super(root);
this.root = root;
isHeader = true;
}
AddressViewHolder(ActivityAddressItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
isHeader = false;
}
void bind(AddressBean bean) {
if (isHeader) {
bindHeader();
} else {
bindItem(bean);
}
}
void bindHeader() {
.....
}
void bindItem(AddressBean bean) {
binding.setAddressBean(bean);
......
}
}
一些小技巧
?在使用過程中,還有一些小技巧,記錄在此。
和方法的綁定
public class AddressBean extends ViewModel {
...
public void onDelete(View view){
...
}
...
}
// 在 xml 中
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#cfcfcf"
android:text="刪除"
android:textSize="12sp"
android:textColor="@color/white"
android:gravity="center"
android:onClick="@{addressBean::onDelete}"/>
View 可見性綁定
<data>
<variable
name="phoneBean"
type="com.yuegs.AddressPhoneBean" />
<import type="android.view.View" />
</data>
<CheckBox
android:layout_width="17dp"
android:layout_height="17dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="12dp"
android:background="@drawable/checkbox_style"
android:button="@null"
android:checked="@={phoneBean.selected}"
android:visibility="@{phoneBean.checkVisible? View.VISIBLE:View.GONE,default=visible}"/>
總結
?綁定的基礎,是觀察者模式。只不過,這種觀察者模式的細節實現,由這類 LiveData 和 ViewModel 幫助我們實現了。
參考
LiveData Overview
LiveData beyond the ViewModel?—?Reactive patterns using Transformations and MediatorLiveData
Android Architecture Patterns Part 3:
Model-View-ViewModel
AndroidViewModel vs ViewModel
MediatorLiveData
Advanced Data Binding: Binding to LiveData (One- and Two-Way Binding)
Two-way data binding