使用 LiveData 进行数据绑定

2019-07-08  本文已影响0人  风雪围城
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 有以下优势:

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 的观察或者说是订阅,理由如下:

 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

上一篇下一篇

猜你喜欢

热点阅读