美文网首页
使用 LiveData 进行数据绑定

使用 LiveData 进行数据绑定

作者: 风雪围城 | 来源:发表于2019-07-08 23:59 被阅读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 有以下优势:

    • 确保 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

    相关文章

      网友评论

          本文标题:使用 LiveData 进行数据绑定

          本文链接:https://www.haomeiwen.com/subject/amnzhctx.html