Jetpack AAC完整解析(五)DataBinding 重新认知!

前面四篇介绍了Jetpack 架构组件中的 基础组件 以及它们的综合应用:Jetpack MVVM 架构模式,到这里已经基本满足标准化开发了。但 Jetpack 架构组件 除了 Lifecycle、LivaData、ViewModel,还有:

  • WorkManager,用于管理后台工作的任务,即使应用退出或重启时。
  • Paging,分页库,按需加载部分数据。
  • Startup,用于App启动速度优化的库,但只适用于库开发者, 郭霖这篇有详细介绍。
  • DataStore,用于替换SharedPreferences,目前还处于Alpha阶段。
  • DataBinding,将布局中的界面组件直接绑定到数据源,提供双向绑定,及高级绑定适配能力。
  • ViewBinding,用于替代findViewById,而DataBinding也包含ViewBinding的能力。
  • Room,实现本地存储 数据库管理,支持LiveData。

目前,就学习使用的必要性和库的功能性 来说,WorkManager、Paging、Startup都是非必须的,DataStore还未正式发布,ViewBinding的能力也包含在DataBinding中。Room,实际 功能和性能 同GreenDAO类似,有个好处是支持LivaData,但已使用GreenDao的项目,也不必切换为Room了。

DataBinding是比较有争议的一个库,这也是本篇的重点,相信会带你 重新认识 被误解的 DataBinding

一、重新认知 DataBinding

DataBinding的使用方法,参考官方文档就可以,介绍地很详细了,这里就不再搬运。(另外还找到一个慕课网的视频很不错:入门篇高级篇

1.1 DataBinding 的本质

应该不少人和我以前一样,对 DataBinding 的认知就是 在xml中写逻辑

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{!isFemale? user.name + ":男士": user.name + ":女士"}'/>

看到 xml 中 使用三元表达式 来计算view需要的值,然后就认为:“ DataBinding 不好用!在xml中写表达式逻辑,出错了debug不了啊,逻辑写在xml里面的话 xml 就承担了 Presenter/ViewModel 的职责 变得混乱了啊。”

如果是把逻辑写在xml中,确实如是:xml中是不能调试的、职责上确实是混乱了。

但,这就是 DataBinding 的本质了吗?

1.1.1 DataBinding 以前

在 DataBinding 出现以前,想要改变视图 就要引用该视图:

        TextView textView = findViewById(R.id.sample_text);
        if (textView != null && viewModel != null) {
            textView.setText(viewModel.getUserName());
        }

而要引用该视图就要先判空,textView 和 viewModel 都不能为空。textView为啥要判空呢?一种情况是 R.id.sample_text是定义在在其他页面中;一种情况是存在控件存在差异的 横、竖 两种布局,如横屏存在此 textView 控件,而竖屏没有,那么就需要对其做判空处理。

App内页面和控件数量繁多,一个控件可能会多处调用,这就会有出现空指针的可能,那如何完全避免呢?

1.1.2 数据绑定

DataBinding,含义是 数据绑定,即 布局中的控件可观察的数据 进行绑定。

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}"/>

布局中这个TextView是实实在在 存在的,就不需要判空了。而user是否为空 DataBinding也会自动处理:在表达式 @{user.name} 中,如果 user 为 Null,则为 user.name 分配默认值 null。

并且,当该 user.name 被 set 新值时,被绑定了该数据的控件即可获得通知和刷新。换言之,在使用 DataBinding 后,唯一的改变是,你无需手动调用视图来 set 新状态,你只需 set 数据本身。

所以,DataBinding 并非是 将 UI 逻辑搬到 XML 中写 导致而难以调试 ,只负责绑定数据, UI 控件 与 其需要的 终态数据 进行绑定。 终态数据是指 UI 控件 直接需要的数据(UI数据),string值、int值等,而不是一段逻辑(不然就叫 LogicBinding了 ,虽然DataBinding支持逻辑表达式)。

明确了 DataBinding 的 职责边界后 应该知道了:原本的逻辑代码 该怎么写还是怎么写,只不过不再需要 textView.setText(user.name),而是直接 user.setName()。

所以 DataBinding 的本质就是 终态数据 与 UI控件 的绑定,具有以下优势

  1. 无需多处调用控件,原本调用的地方只需要set数据即可
  2. 1的延伸,无需手动判空
  3. 1的延伸,完全不用写模板代码 findViewById
  4. 并且,引入DataBinding后,原本的 UI 逻辑无需改动,只需设置终态数据

上篇提到过 Jetpack MVVM 架构本质是数据驱动,这就是说,控件的状态及数据是 被分离到 ViewModel 中管理,并且 ViewModel 这一层只需负责状态数据本身的变化,至于该数据在布局中是 被哪些视图绑定、有没有视图来绑定、以及怎么绑定,ViewModel 是不用关心的。

那控件是如何做到被通知且更新状态的呢?

DataBinding 是通过 观察者模式 来管理控件刷新状态。当状态数据变化时,只需手动地完成 setValue,这将通知 DataBinding 去刷新 该数据 绑定的控件。

而,文章开头提到的把逻辑放入xml中的写法,是不建议的。数据值应 直接反映UI控件需要的结果,而不是作为逻辑条件放在 xml 中。所以,DataBinding 不负责 UI 逻辑,逻辑原本在哪里写,现在还是在哪里写,只不过,原本调用控件实例去刷新状态的方式,现在改成了数据驱动。 这就是DataBinding 的核心目标。

1.2 例子 - 绑定列表数据

来举个例子进行说明:在页面中展示用户信息(User)列表,同时还有两个按钮用于添加、移除用户:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="clickPresenter"
            type="com.hfy.demo01.module.jetpack.databinding.ListActivity.ClickPresenter" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".module.jetpack.databinding.ListActivity">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="添加user"
            android:onClick="@{clickPresenter::addUser}"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="移除user"
            android:onClick="@{clickPresenter::removeUser}"/>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_user_list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</layout>

我们知道,RecyclerView的所展示的列表数据, 是通过Adapter 对每一项数据 分别进行设置的,也就是说User是绑定到 Item的xml中:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="user"
            type="com.hfy.demo01.module.jetpack.databinding.bean.User" />
    </data>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="50dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.level}"/>
    </LinearLayout>
</layout>

我们看下,在Activity中是如何处理的:

public class ListActivity extends AppCompatActivity {
  
    private ActivityListBinding mViewDataBinding;
    private static UserListAdapter mAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mViewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_list);

        mViewDataBinding.rvUserList.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false));
        mAdapter = new UserListAdapter();
        mAdapter.setNewInstance(getUserList());
        mViewDataBinding.rvUserList.setAdapter(mAdapter);

        mViewDataBinding.setClickPresenter(new ClickPresenter());
    }

  //这里是假装 调用ViewModel能力 获取用户数据
    private List<User> getUserList() {
        List<User> list = new ArrayList<>();
        list.add(new User("小明","Lv1"));
        list.add(new User("小红","Lv2"));
        list.add(new User("小q","Lv3"));
        list.add(new User("小a","Lv4"));
        return list;
    }
  
    //点击监听处理
    public class ClickPresenter {
        public void addUser(View view) {
            Toast.makeText(ListActivity.this, "addUser", Toast.LENGTH_SHORT).show();
            mAdapter.addData(new User("小z","Lv5"));
        }
        public void removeUser(View view) {
            Toast.makeText(ListActivity.this, "removeUser", Toast.LENGTH_SHORT).show();
            mAdapter.remove(0);
        }
    }

    private static class UserListAdapter extends BaseQuickAdapter<User, UserItemViewHolder> {
        public UserListAdapter() {
            super(R.layout.item_user);
        }

        @Override
        protected void convert(@NonNull UserItemViewHolder holder, User user) {
            // 精髓所在1,不需要去一个个setText等等
            holder.getItemUserBinding().setUser(user);
            holder.getItemUserBinding().executePendingBindings();

            //当获取的DataBinding不是具体类时,只是ViewDataBinding,那就要使用setVariable了
//            holder.getViewDataBinding().setVariable(BR.user, user);
//            holder.getViewDataBinding().executePendingBindings();
        }
    }

    private static class UserItemViewHolder extends BaseViewHolder {

        // 精髓所在2,只需要持有 binding即可,不用去findViewById
        private final ItemUserBinding binding;
//        private final ViewDataBinding binding2;

        public UserItemViewHolder(View view) {
            super(view);
            binding = DataBindingUtil.bind(view);
//            binding2 = DataBindingUtil.bind(view);
        }

        public ItemUserBinding getItemUserBinding() {
            return binding;
        }

//        public ViewDataBinding getViewDataBinding() {
//            return binding2;
//        }
    }

}

RecyclerView的初始化、调用ViewModel对数据的获取,这些处理及逻辑 和之前一毛一样,不同点在于 Item数据的展示:

  1. 在UserItemViewHolder中,不用去findViewById了,而是直接DataBindingUtil.bind(view),ViewHolder只要Hold住 binding就可以了,之前是Hold住所有的view。
  2. 在UserListAdapter中,设置数据是,也只是使用 binding 去 setUser(user)即可。

二、自定义属性 - BindingAdapter

DataBinding 还有个强大功能:能为控件提供自定义属性的 BindingAdapter!

不懂?我们来看个例子。

        <ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:imageUrl="@{user.avatar}"
            app:placeHolder="@{@drawable/dog}"/>

其中的 app:imageUrl 、app:placeHolder 分别与 user.avatar、@drawable/dog 绑定了。 但我们知道ImageView本身是没有这两个属性的,并且我们也并不是 继承 ImageView 的自定义View,那为啥可以这样使用呢? 再来看:

    @BindingAdapter({"app:imageUrl", "app:placeHolder"})
    public static void loadImageFromUri(ImageView imageView, String imageUri, Drawable placeHolder){
        Glide.with(imageView.getContext())
                .load(imageUri)
                .placeholder(placeHolder)
                .into(imageView);
    }

在随便某个类中添加 public static 方法(方法名随意),增加注解@BindingAdapter,并且注明对应的"app:imageUrl", "app:placeHolder",然后方法参数是 控件类型 及 这两个属性对应 值。 然后在方法中写逻辑即可,这里就是使用Glide加载用户头像,其中placeHolder是占位图。

这样就完成了 图片的加载了!

使用确实相当简洁,相当于 直接自定义属性。你可以自定义 任何你想要的属性。

通常我们可以用 @BindingAdapter 方式,在模块 内部 来做一些公用逻辑。例如这个图片加载,@BindingAdapter注解的方法 只要写一次,那么 所有用到 ImageView 加载图片的地方 xml中都可以 直接使用属性 app:imageUrl 、app:placeHolder 直接绑定数据 。

三、结合 LiveData

DataBinding 还有个妙处在于: 可以结合 LiveData 使用

原本我们使用DataBinding,在xml中定义的variable数据 ,必须要继承BaseObservable 或者使用 ObservableField,还要添加 注解 @Bindable、调用notifyPropertyChanged(BR.name)。这是为了 user.setName(name) 字段发生变化时 通知 对应绑定View 也进行刷新。

而 我们 上一篇 中 MVVM 是使用 LiveData,实现数据驱动的,它包裹的 User 是没有继承BaseObservable的, 要继承嘛? 不用!

LiveData 的出现,就可以代替 ObservableField ,并且 还自动具备 生命周期管理。

不用侵入式的修改数据实体类了,直接使用LiveData,同样支持DataBinding的数据绑定!

DataBinding 结合 LiveData 使用步骤很简单:

  1. 要使用LiveData对象作为数据绑定来源,需要设置LifecycleOwner
  2. xml中 定义变量 ViewModel, 并使用 ViewModel 中的 LiveData 绑定对应控件
  3. binding设置变量ViewModel
        //结合DataBinding使用的ViewModel
        //1. 要使用LiveData对象作为数据绑定来源,需要设置LifecycleOwner
        binding.setLifecycleOwner(this);

        ViewModelProvider viewModelProvider = new ViewModelProvider(this);
        mUserViewModel = viewModelProvider.get(UserViewModel.class);
        //3. 设置变量ViewModel
        binding.setVm(mUserViewModel);
        <!-- 2. 定义ViewModel 并绑定-->
	<variable
            name="vm"
            type="com.hfy.demo01.module.jetpack.databinding.UserViewModel" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.userLiveData.name}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.userLiveData.level}"/>

这样就ok了,你会发现 我们不需要在 Activity中 拿到LivaData 去 observe(owner,observer)了,DataBinding 自动生成的代码,会帮我们去做这操作,所以需要设置LifecycleOwner。

也就是说,在上一篇中介绍的 Jetpack MVVM 中,如果要使用 DataBinding 的话,也是很简单的。

四、Jetpack MVVM 补充说明

讲完DataBinding,所有的 Jetpack 架构组件 的重点内容 就全部讲完了。

这里对 Jetpack AAC 及 MVVM ,做一些 补充 和 说明:

  • 一、ViewModel 和 View 职责分离,ViewModel中处理业务逻辑,View 仅展示数据及传递事件
  • 二、ViewModel 不引用 View 及 Context
  • 三、View 通过 LiveData 观察数据变化,不是直接向View 推送数据
  • 四、ViewModel中 除了 业务 LiveData 外,还应该提供 LiveData,表示数据 是加载中、加载成功、加载失败。
  • 五、使用SingleLiveEvent 来传递 事件类消息:仅在显式调用setValue()或call()时 才会通知观察者;只有一个观察者会收到更改通知。
  • 六、ViewModel 和 Repository 之间,建议 使用 LiveData 进行通信,就像 View 和 ViewModel 之间那样 使用回调的话,可能会有内存泄漏的风险。 并且在ViewModel中 使用 Transformations.switchMap 把 生命周期信息 传递到 Repository 的 LiveData 中。
  • 七、DataBinding中绑定的数据 直接使用 LivaData 即可, 而不是 BaseObservable
  • 八、xml中尽量只定义一个variable,那就是 页面对应的 ViewModel ,控件直接绑定 LivaData 的字段
  • 九、XML 中尽量 不使用逻辑表达式,把逻辑放在 ViewModel 中,控件绑定终态数据

五、总结

本篇 重点讲了 DataBinding 的重新认知:DataBinding的本质 " 终态数据 绑定到 View " ,而不是 ” 在xml写逻辑 ”;自定义属性 BindingAdapter;结合 LiveData的使用。可见DataBinding 在 Jetpack MVVM 架构中 还是 有很大优势的。 最后补充说明得了 Jetpack MVVM 架构 的使用注意事项和原则,在实际项目使用中 应该会很有体会。

到这里呢,整个Jetpack AAC系列 也就结束了,到这里是第五篇了。每篇文章都想着尽可能把内容 给介绍清楚,包括很多自己使用过后的理解。过程中也阅读了大量 相关优秀的文章 ,学习到了不同的观点。虽然整个系列是经过 阅读源码、实际使用、阅读其他优秀文章 之后输出的,但不免出现错误和遗漏,欢迎大家 留言讨论。

Demo地址

参考与感谢:

DataBinding官方文档

ViewModel 和 LiveData:为设计模式打 Call 还是唱反调?

重学安卓:从 被误解 到 真香 的 Jetpack DataBinding!

MVVM陷阱之DataBinding

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作者codyer,源码ElegantBus,ElegantBus 是一款 Android 平台,基于 LivaData 的消息总线框架,这是一款非常 优雅 的消息总线框架。如果对 ElegantBus 的实现过程,以及考虑点感兴趣的可以看看前几节自吹如果只是想先使用的,可以跳过,直接到跳到使用说明和常见 LivaData 实现的 EventBus 比较消息总线使用反射入侵系统包名进程内 Sticky跨进程 Sticky跨 APP Sticky事件可配置化线程分发消息分组跨 App 安全考虑常驻事件 StickyLiveEventBus:white_check_mark::white_check_mark::white_check_mark::x::x::x::x::x::x::x:ElegantBus:x::x::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark::white_check_mark:来龙去脉自吹ElegantBus 支持跨进程,且支持跨应用的多进程,甚至是支持跨进程间的粘性事件,支持事件管理,支持事件分组,支持自定义事件,支持同名事件等。之所以称之为最优雅的总线,是因为她不仅实现了该有的功能,而且尽量选用最合适,最轻量,最安全的方式去实现所有的细节。 更值得夸赞的是使用方式的优雅!前言随着 LifeCycle 的越来越成熟,基于 LifeCycle 的 LiveData 也随之兴起,业内基于 LiveData 实现的 EventBus 也如雨后春笋一般拔地而起。出于对技术的追求,看过了无数大牛们的实现,各位大神们思路也是出奇的神通,最基础的 LiveData 版 EventBus 其实大同小异,一个单例类管理所有的事件 LivaData 集合。如果不清楚的可以随便网上找找反正基本功能 LivaData 都支持了,实现 EventBus 只需要把所有事件管理起来就完事了。业内基于 LiveData 实现的 EventBus,其实考虑的无非就是下面提到的个挑战,有的人考虑的少,有的人考虑的多,于是各种方案都有。ElegantBus 主要是集合各家之优势,进行全方面的考虑而产生的。个挑战 之 路途险阻挑战一 : 粘性事件背景 LivaData 的设计之初是为了数据的获取,因此无论是观察开始之前产生的数据,还是观察开始之后产生的数据,都是用户需要的数据,只要是有数据,当 LifeCycle 处于激活状态,数据就会传递给观察者。这个我们称之为 粘性数据。 这种设计对于事件来说有时候就不那么友好了,之前的事件用户可能并不关心,只希望收到注册之后发生的事件。挑战二 : 多线程发送事件可能丢失背景 同样是因为使用场景的原因,LivaData 设计在跨线程时,使用 post 提交数据,只会保留最后一次数据提交的值,因为作为数据来说,用户只需要关心现在有的数据是什么。挑战三 : 跨进程事件总线背景 有时候我们应用需要设置多进程,不同模块可能允许在不同进程中,因为单例模式每个进程都有一份实体,所有无法达到跨进程,这时候设计 IP 方案选择。说明 这里提一下为什么不选用广播方式,对广播有一定了解的都知道,全局广播会有信息泄露,信息干扰等问题,而且开销也比较大,因此全局广播并不适合这种情况。 也许有人会说可以用本地广播,然而,本地广播目前来说并不是很好的选择。Google 官方也在 LocalBroadcastManager 的说明里面建议使用 LiveData 替代: 原文地址原文如下:2018 年 12 月 17 日版本 1.1.0-alpha01 中将弃用 androidx.localbroadcastmanager。原因LocalBroadcastManager 是应用级事件总线,在您的应用中使用了层违规行为;任何组件都可以监听来自其他任何组件的事件。 它继承了系统 BroadcastManager 不必要的用例限制;开发者必须使用 Intent,即使对象只存在且始终存在于一个进程中。由于同一原因,它未遵循功能级 BroadcastManager。 这些问题同时出现,会对开发者造成困扰。替换您可以将 LocalBroadcastManager 替换为可观察模式的其他实现。合适的选项可能是 LiveData 或被动流,具体取决于您的用例。更明显的原因是,本地广播好像并不支持跨进程~挑战四 : 跨应用(权限问题以及粘性问题)背景 跨进程相对来说还比较好实现,但是有的时候用户会有跨应用的需求,其实这个也是 IPC 范畴,为什么单独提出
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值