使用 LiveData 进行数据绑定
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