使用 LiveData 進行數據綁定

livedata-observe.png

?LiveData 是對可觀察數據的封裝。不像其他可觀察對象(例如 ObservableField) , LiveData 可以感知到生命周期。這就意味著它可以關聯到其他擁有生命周期的組件上,比如 Activity、Fragment 或者 Service。這種感知,可以確保 LinveData 的更新只發生在一個組件的活動狀態上。如下圖所示:

viewmodel_scope.png

?對于一個觀察者類而言,所謂的激活狀態就是 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 的使用

  1. 首先,創建一個持有數據的 LiveData 對象。這一步通常是在 ViewModel 中完成。
  2. 創建一個 Observer 對象,并定義其 onChange() 方法。該方法將控制在 LiveData 所持有的數據發生變化時,觀察者將發生怎樣的變化。我們通常創建在 UI controller 中創建 Observer。而這類 UI controller 諸如 activity 和 fragment。
  3. 通過 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容