“终于懂了“系列:Jetpack AAC完整解析(五)DataBinding 重新认知!

Jetpack AAC 系列文章:

“终于懂了“系列:Jetpack AAC完整解析(一)Lifecycle 完全掌握!

“终于懂了“系列:Jetpack AAC完整解析(二)LiveData 完全掌握!

“终于懂了“系列:Jetpack AAC完整解析(三)ViewModel 完全掌握!

“终于懂了“系列:Jetpack AAC完整解析(四)MVVM 架构探索!

“终于懂了“系列: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 也进行刷新。

继承BaseObservable

而 我们 上一篇 中 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<DataState>,表示数据 是加载中、加载成功、加载失败。
  • 五、使用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

.

你的 点赞、评论,是对我的巨大鼓励!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容