MVVM陷阱之DataBinding(数据绑定库)

本文已经对《第一行代码》作者郭霖的公众号授权独家发布

一、什么是DataBinding?

官方文档的描述如下:

数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。

布局通常是使用调用界面框架方法的代码在 Activity 中定义的。例如,以下代码调用 findViewById() 来查找 TextView 微件并将其绑定到 viewModel 变量的 userName 属性:

 TextView textView = findViewById(R.id.sample_text);
 textView.setText(viewModel.getUserName());

以下示例展示了如何在布局文件中使用数据绑定库将文本直接分配到微件。这样就无需调用上述任何 Java 代码。请注意赋值表达式中 @{} 语法的使用:

<TextView
        android:text="@{viewmodel.userName}" />

借助布局文件中的绑定组件,您可以移除 Activity 中的许多界面框架调用,使其维护起来更简单、方便。还可以提高应用性能,并且有助于防止内存泄漏以及避免发生 Null 指针异常。


  • 目前在网络上听到过的质疑声主要有两点:
    1. 使用数据绑定会影响应用性能。
    2. 数据绑定写在布局里面维护起来会很困难。

针对以上两点,从上面的引用中可以看出,官方明确说了是可以提高应用的性能,并且会使项目维护起来更方便。

  • 为什么会有人提出以上质疑呢?
    他们的观点大抵可以分为以下两种:

    1. 数据绑定会生成额外的类进行布局和数据的绑定
    2. 数据绑定会维护一个绑定数据表 DataBinderMapperImpl

    其实从源码和实现原理上分析,上面的质疑好像并没有什么问题,但是仔细一想,针对第一点,其实没有数据绑定生成额外的类来进行数据绑定,我们自己不是也得写代码进行类似 findViewById()然后赋值等操作么?可以理解为这里只是运用了面向对象的单一职责原则进行了很好的封装。

    第二点就更神奇了,这个数据绑定表本身就是为了提高效率而设计的,用这一丁点的内存(这是一个SparseIntArray而已,size是布局数量),难道会比平时我们多用一张没有压缩过的图片消耗大吗?这点内存而带来的效率提高难道不值得吗?

    补充一点,如果因为所谓的应用的效率而拒绝使用可以提高开发效率的技术,那为什么我们不去用纯C写呢?native的效率不是更高么?其实我们都知道,对于大多是应用来说,和游戏或者其他高内存消耗的应用相比,电商类或者工具类的应用对内存的消耗是很小的,在技术的取舍上要分得清主次。

二、哪些情况下我可以或者应该用DataBinding?

在使用数据绑定之前你要清楚你要拿它来解决什么问题?

  1. 如果只是为了替代findViewById()那你可以去使用最新抽取出来的视图绑定(ViewBinding)因为这个相对于数据绑定来说更轻巧,在DataBinding刚出来的时候并没有单独区分数据 or 视图绑定,这是在新的版本中,为了更好的设计,将ViewBinding进行了单独的抽取。
    从最新版本的代码可以看出:

      public abstract class ViewDataBinding extends BaseObservable 
      implements ViewBinding {}
    

    抽象类ViewDataBinding实现了ViewBinding接口,这个名字也是取得恰到好处,View-Data-Binding,其实大家常说的DataBinding是包含ViewBinding的。
    说句题外话,如果只是为了替代findViewById(),并且是使用kotlin开发的话,也可以考虑Kotlin-android-extensionKAE具有和ViewBinding差不多的功能,具体使用方式在此也不介绍了,有兴趣的可以去找相关资料。`

  2. 为了实现数据绑定,类似数据驱动架构,MVVM架构等。恭喜你,这时候Databinding是不错的选择。
    说到这里又涉及到MVCMVPMVVM设计架构的区别,相信大家对这几个概念都有所了解。
    首先,明确一点,架构的目的就是为了提高开发效率,降低维护成本。
    利用面向对象的设计原则,对每个模块的职责进行合理的划分,为了让其他人更好的理解架构设计思想,然后给予每个模块一个通用的名词解释,为了更好的说清楚数据绑定,这里就简单解释一下目前常见的几种架构(以Android为例)。

    • MVC
      • 模型层(Model),负责处理数据逻辑,一般包含数据库、本地数据、网络获取的Bean等组成。
      • 视图层(View),负责处理视图显示,一般由XML布局承担此责任,基本组件和自定义View等充当视图层的补充元素。
      • 控制层(Control),负责处理业务逻辑,一般由ActivityFragment承担此责任。
    • MVP
      • 模型层(Model),负责处理数据逻辑,一般包含数据库、本地数据、网络获取的Bean等组成。
      • 视图层(View),负责处理视图显示,一般由XML布局承担此责任,基本组件和自定义View等充当视图层的补充元素,ActivityFragment充当视图层和控制层的粘合剂。
      • ??(Presenter),负责处理业务逻辑,由从原来MVC控制层中抽取出来的Presenter充当控制层(Presenter)。
    • MVVM
      • 模型层(Model),负责处理数据逻辑,一般包含数据库、本地数据、网络获取的Bean、(这里我单独抽取的视图数据ViewData概念也属于Model层)等组成。
      • 视图层(View),负责处理视图显示,一般由XML布局承担此责任,基本组件和自定义View等充当视图层的补充元素,ActivityFragment主要负责视图层绑定事件触发,熟练的话也可以直接在XML中绑定触发事件。
      • ??(ViewModel),通过数据绑定连接ViewModel(这里由ViewData充当视图模型被绑定到视图上)实现视图层和模型层的解藕,事件触发后通过ViewModel处理业务逻辑,并且通过数据驱动的方式修改视图数据,而达到间接修改视图的功能。注意:ViewModel一定不能持有视图层的引用,同样不能持有Context的引用!不然还是MVP!

    对于新手来说,看完上面说明,更让人觉得摸不着头脑,只是换一个名字而已,最终不还是分三层吗?视图层(View)、数据层(Model)、逻辑处理层(???),这么简单的东西,为什么搞得很高深莫测的样子?

    参考一下图示也许你就豁然开朗了:
    image
    其实不同的设计架构最终目的还是为了解耦,实现高内聚低耦合一直是架构师的理想,这种情况下,每个程序员只需要关心自己的模块就可以了。

    • 就拿MVVM来说,当一个项目足够大的时候,可能有的人负责界面绘制(XML),有的人负责业务逻辑处理(ViewModel),有的人负责数据逻辑处理(Model)。这时候,每个模块的人只需要关心自己的逻辑就可以了,而且每个模块都可以单独跑Use Case,每个模块并没有很强的依赖关系,而且当某个模块的逻辑变更了并不一定会影响到其他模块的变更。
有几点要注意:

1、官方的ViewModel库并不是实现MVVM架构的必备,MVVM的重点是解藕,通过一定方式解除ViewModel的耦合,比如使用数据绑定库DataBinding

2、也有不使用DataBinding实现的MVVM吗?其实也有,比如说郭神第三版的《第一行代码》中的方式,利用LiveData实现ViewModel的解藕,且ViewModel不依赖ViewContext,这里郭神把ActivityFragment当作View的主体,而我更倾向于把XML当作View的主体,所见即所得,看得到的当成View,会更直观一点。ActivityFragment只是当作一个粘合剂,比如进行事件绑定和一些复杂动画的处理等。所以DataBinding更多的是服务于XML这种View的。

3、ViewModel库是在DadaBinding库之后才有的,ViewModel类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存,这样可以更好的提升用户体验和提高应用性能。
image
上图说明了Activity经历屏幕旋转而后结束的过程中所处的各种生命周期状态。该图还在关联的 Activity生命周期的旁边显示了ViewModel的生命周期。此图表说明了 Activity 的各种状态。这些基本状态同样适用于 Fragment 的生命周期。
4、其实可以从官方的介绍中看出来,官方的ViewModel库和我们所说的MVVM架构中的ViewModel层并不是等价的东西,ViewModel层不止包含ViewModel数据(我更愿意称为ViewData),还应该包含视图模型的逻辑处理。

总的来说,为了提高开发效率,为了更好的在大型团队中协调开发,MVVM是一个不错的选择!目前为止,个人认为DataBinding、ViewModel,再加上LiveData,是搭建MVVM架构最完美的组合。

三、DataBinding的基本使用方式

  • 第一步:要将应用配置为使用数据绑定,请在应用模块的 build.gradle 文件中添加 dataBinding 元素,如以下示例所示:
    android {
        ...
        dataBinding {
            enabled = true
        }
    }
    

注意:即使应用模块不直接使用数据绑定,也必须为依赖于使用数据绑定的库的应用模块配置数据绑定。

  • 第二步:修改原来的XML布局文件,在原布局外层包裹一层layout标签,并且使用datavariable标签,添加需要绑定的数据,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data calss="MainBinding">
           <variable name="user" type="com.example.User"/>
       </data>
    <!-- 原布局开始-->
         <LinearLayout 
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.firstName}"/>
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.lastName, default=default_value}"/>
       </LinearLayout>
    <!-- 原布局结束-->
    </layout>
    
  • 第三步:设置布局页面以及绑定数据到页面
    @Override
    protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       MainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
       User user = new User("Test", "User");
       binding.setUser(user);
    }
    
目前为止,布局已经和数据进行了绑定,其实这时候和平时我们使用findViewById()然后再setText()差别并不大,精彩的是后面。

四、DataBinding的高级用法

上面虽然实现了基本的数据绑定,但是改变原来的数据对象,界面并不会发生改变,这个时候就需要引入一个新的概念,可观察字段

可观察性是指一个对象将其数据变化通知给其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。
通过数据绑定,数据对象可在其数据发生更改时通知其他对象,即监听器。可观察类有三种不同类型:对象、字段和集合
当其中一个可观察数据对象绑定到界面并且该数据对象的属性发生更改时,界面会自动更新。

  • 最早的时候使用ObservableField或者对基本数据类型封装好的ObservableField,比如ObservableBooleanObservableByte
    ObservableCharObservableInt等,更轻量级的还有自己通过调用notifyPropertyChanged控制数据刷新。

  • 后来LiveData的出现,可以使用LiveData代替ObservableField并获得更好的生命周期管理。因此这里主要讲讲使用LiveData实现ModelView的解藕。

  • 目前这些方式都是支持的,从代码中可以看出,最终都会通过注册,并统一由mLocalFieldObservers进行管理。
    ObservableField

       protected boolean updateRegistration(int localFieldId, Observable observable) {
          return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
      }
    

    LiveData调用差不多:

     protected boolean updateLiveDataRegistration(int localFieldId, LiveData<?> observable) {
          mInLiveDataRegisterObserver = true;
          try {
              return updateRegistration(localFieldId, observable, CREATE_LIVE_DATA_LISTENER);
          } finally {
              mInLiveDataRegisterObserver = false;
          }
      }
    

    最终都调用到同样的一个函数:

     protected void registerTo(int localFieldId, Object observable,
              CreateWeakListener listenerCreator) {
          if (observable == null) {
              return;
          }
          WeakListener listener = mLocalFieldObservers[localFieldId];
          if (listener == null) {
              listener = listenerCreator.create(this, localFieldId);
              mLocalFieldObservers[localFieldId] = listener;
              if (mLifecycleOwner != null) {
                  listener.setLifecycleOwner(mLifecycleOwner);
              }
          }
          listener.setTarget(observable);
      }
    

其实说白了就是观察这模式的灵活运用,最终实现了数据的绑定,对于双向绑定也没有什么特别的,自动生成的代码会根据我们在布局中是否设定了双向绑定,而主动帮我们设置一个InverseBindingListener监听,XML中格式如下:android:text="@={...}"

小知识点

  • 可以在XML中使用default(默认值)也可以不用。

  • 可以在data标签中,通过class属性指定要生成的DataBinding文件名字。

  • 可以在data标签中,通过import标签导入需要使用的类类型。

  • Null 合并运算符

    android:text="@{user.displayName ?? user.lastName}"
    

    这在功能上等效于:

    android:text="@{user.displayName != null ? user.displayName : user.lastName}"
    
  • 要使XML不含语法错误,您必须转义 < 字符。例如:不要写成 List<String> 形式,而是必须写成 List&lt;String>

  • BindingAdapter、InverseBindingAdapter等注解的使用
    例如设置一个斜体绑定适配器:

    
      @BindingAdapter({"strike"})
      public static void setStrike(TextView view, boolean strike) {
          if (strike) {
              view.setPaintFlags(view.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
          } else {
              view.setPaintFlags(view.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
          }
      }
    

    在布局中使用

    <TextView
      ...
      bind:strike="@{true}"
      ...
      />
    
  • 设置事件监听

    <!-- 在data标签添加监听器绑定-->
    <variable
            name="onClickListener"
            type="android.view.View.OnClickListener" />
    <!-- 在需要绑定点击事件的控件上添加绑定-->
    ...
    <View android:onClick="@{onClickListener::onClick}"/>
    ...
    

五、优雅的使用DataBinding实现MVVM

  • 通过对源码的分析可以得知,无论是使用ObservableField还是使用LivaData 最终都会在本地属性观察者mLocalFieldObservers中注册监听,其实数据绑定库的使用非常的灵活,除了在上面提到的使用方式,还有更多的使用方式,甚至可以在布局XML中进行逻辑判断,事件绑定,布局管理器设定,列表子布局设定等,可以说是无所不能。

  • 其实正是这种灵活,也遭受了很多使用者的诟病,说很多逻辑写在XML中,调试困难,维护麻烦,更有甚者,跳出来直接得出DataBinding不能用的结论,这就好比说菜刀能伤人就说菜刀不好一样。

有问题的不是工具,而是使用工具的方式!
  • 为了给DataBinding正名,因此总结一些使用原则,分享如下。

    • 原则一:能不用可观察变量尽量不要用。
    • 原则二:多个变量会同时改变的情况尽量使用一个可观察变量进行包装。
    • 原则三data标签能少导入一个变量尽量少导入。
    • 原则四:XML布局尽量少或者不使用过多的逻辑判断。
    • 原则五:避免对一个数据进行多次绑定(有人通过这种方式刷新界面,这个其实和DataBinding的初衷违背了)。
    • 原则六:严格遵守上述五条。
  • 基于以上六条使用原则,目前经过多次迭代,总结出了满足绝大多数场景的MVVM

    • 第一步
      整个XML使用统一的格式,无论是普通的布局,还是列表的Item布局,抑或是include的布局,都是使用同样的方式,这样就可以使用AndroidStudioFile Templates模版功能创建布局文件了。

      <?xml version="1.0" encoding="utf-8"?>
      <layout xmlns:android="http://schemas.android.com/apk/res/android">
         <data>
      <!-- 用于控制显示隐藏导入此类-->
             <import type="android.view.View" />
      <!-- 用于点击事件绑定-->
             <variable name="onClickListener" type="android.view.View.OnClickListener" />
      <!-- 用于视图数据绑定-->
             <variable name="viewData" type="com.example.UserViewData"/>
         </data>
      <!-- 原布局开始-->
           <LinearLayout 
             android:orientation="vertical"
             android:layout_width="match_parent"
             android:layout_height="match_parent">
             <TextView android:id="@+id/helloSomeOne"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:onClick="@{onClickListener::onClick}"
                 android:text="@{viewData.firstName}"/>
             <TextView android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:visibility="@{viewData.noLastName?View.GONE:View.VISIBLE}"
                 android:text="@{viewData.lastName, default=default_value}"/>
      <!-- 包含另一个布局 并传递事件绑定和视图数据绑定-->
              <include
                  android:id="@+id/includeViewId"
                  layout="@layout/include_view_layout"
                  bind:onClickListener="@{onClickListener}"
                  bind:viewData="@{viewData}" />
         </LinearLayout>
      <!-- 原布局结束-->
      </layout>
      
    • 第二步
      创建BaseBindActivityBaseBindFragment,实现底层的数据绑定,以及生命周期设定,以及事件绑定。

      public abstract class BaseBindActivity<B extends ViewDataBinding> extends Activity implements  View.OnClickListener {
      private B mBinding;
      
      /**
       * 数据绑定
       */
      protected abstract <ViewData> ViewData getViewData();
      
      /**
       * 子类提供有binding的资源ID
       */
      @LayoutRes
      protected abstract int getLayoutID();
      
      @Override
      protected void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          mBinding = DataBindingUtil.setContentView(this, getLayoutID());
          if (mBinding != null) {
              mBinding.setLifecycleOwner(this);
              mBinding.setVariable(BR.onClickListener, this);
              mBinding.setVariable(BR.viewData, getViewData());
          } else {
              setContentView(getLayoutID());
          }
      }
      
      @Override
      public B getBinding() {
          return mBinding;
      }
      }
      

      这样子页面只需要实现简单的逻辑处理就可以了。BaseBindFragment逻辑类似就不贴代码了。

    • 第三步
      在具体业务Activity中通过 ViewModelProviders获取ViewModel,并从ViewModel中获取ViewData,将ViewData绑定到视图中,子类通过实现 getViewData(),进行绑定操作。

      ViewModelProviders.of(this, factory).get(viewModelClass)
      
    • 第四步
      当用户操作(比如点击)导致一个事件产生,在具体业务Activity中,通过ViewModel的方法调用业务数据提供方,并实现业务逻辑,业务处理完成后,操作ViewData中的属性,实现动态更新界面的功能。

很多逻辑具有通用性,我们可以抽取很多模版代码作为基类使用,比如说列表的ListAdapter、数据库Room、数据差分类DiffUtil.ItemCallback、RecyclerView的ViewHolder都可以进行很好的封装,使用时就会变得很简单,以后再也不用处理那么多的AdapterViewHolder了。

因为篇幅所致,这里就不详细介绍了,有兴趣的可以参考github源码,源码里面的README也有一部分介绍。
源码传送门

GitHub地址:https://github.com/codyer/component

说明
因为component是实际开发时可能会使用到的一些组件,为了方便,我之前放在一个项目下统一管理的,因此这个项目包含很多模块,感兴趣的欢迎StarFork
如果只想看MVVM的代码可以只参考以上项目的一个modlue
MVVM-模块

GitHub地址:https://github.com/codyer/component/blob/master/app-core/README.md

至此,为DataBinding的正名就算完成了。

尾记

写这篇文章花了整整一天的时间,希望走过路过的伙伴们不要吝啬一个点赞和Star

另外,之前因为机缘巧合实现了一套基于LiveData的事件总线LiveEventBus,自己使用下来发现其实还是很不错的,之前一直做项目也不空闲,再加上觉得好像大家都可以实现,也没有什么太难的地方,也就没有想着写出来分享一下。

最近看见有人实现了一个类似的库,而且还在各种地方发表,首先很佩服他的分享精神,也希望大家多向这样的同学学习,但是呢,看了他的源码和说明,他的实现方式,感觉还是有优化的地方,而且还有些许问题,因此和这次为DataBinding正名一样,激起了我的保护欲,为了保护技术,保护大家优雅的使用姿势,决定花花时间整理一下,教教大家如何优雅的使用LiveData实现一套EventBus

如果有下一篇,它将是《如何优雅的使用LiveData实现一套EventBus

~敬请期待~
----------------------------------------------------------- by Cody.yi

谢谢阅读

写得越多发现自己不知道的越多,如有错误缺漏之处欢迎指正!

下一篇以及完成,欢迎阅读
https://www.jianshu.com/p/79d909b6f8bd

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值