点击上方胡飞洋可以关注哦~
前面四篇介绍了Jetpack 架构组件中的 基础组件 以及它们的综合应用:Jetpack MVVM 架构模式,到这里已经基本满足标准化开发了。但 Jetpack 架构组件 除了 Lifecycle、LivaData、ViewModel,还有:
目前,就学习使用的必要性和库的功能性 来说,WorkManager、Paging、Startup都是非必须的,DataStore还未正式发布,ViewBinding的能力也包含在DataBinding中。Room,实际 功能和性能 同GreenDAO类似,有个好处是支持LivaData,但已使用GreenDao的项目,也不必切换为Room了。
DataBinding是比较有争议的一个库,这也是本篇的重点,相信会带你 重新认识 被误解的 DataBinding。
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 的本质了吗?
在 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内页面和控件数量繁多,一个控件可能会多处调用,这就会有出现空指针的可能,那如何完全避免呢?
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控件 的绑定,具有以下优势:
上篇提到过 Jetpack MVVM 架构本质是数据驱动,这就是说,控件的状态及数据是 被分离到 ViewModel 中管理,并且 ViewModel 这一层只需负责状态数据本身的变化,至于该数据在布局中是 被哪些视图绑定、有没有视图来绑定、以及怎么绑定,ViewModel 是不用关心的。
那控件是如何做到被通知且更新状态的呢?
DataBinding 是通过 观察者模式 来管理控件刷新状态。当状态数据变化时,只需手动地完成 setValue,这将通知 DataBinding 去刷新 该数据 绑定的控件。
而,文章开头提到的把逻辑放入xml中的写法,是不建议的。数据值应 直接反映UI控件需要的结果,而不是作为逻辑条件放在 xml 中。所以,DataBinding 不负责 UI 逻辑,逻辑原本在哪里写,现在还是在哪里写,只不过,原本调用控件实例去刷新状态的方式,现在改成了数据驱动。这就是DataBinding 的核心目标。
来举个例子进行说明:在页面中展示用户信息(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数据的展示:
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 直接绑定数据 。
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 使用步骤很简单:
//结合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 的话,也是很简单的。
讲完DataBinding,所有的 Jetpack 架构组件 的重点内容 就全部讲完了。
这里对 Jetpack AAC 及 MVVM ,做一些 补充 和 说明:
本篇 重点讲了 DataBinding 的重新认知:DataBinding的本质 " 终态数据 绑定到 View " ,而不是 ” 在xml写逻辑 ”;自定义属性 BindingAdapter;结合 LiveData的使用。可见DataBinding 在 Jetpack MVVM 架构中 还是 有很大优势的。 最后补充说明得了 Jetpack MVVM 架构 的使用注意事项和原则,在实际项目使用中 应该会很有体会。
到这里呢,整个Jetpack AAC系列 也就结束了,到这里是第五篇了。每篇文章都想着尽可能把内容 给介绍清楚,包括很多自己使用过后的理解。过程中也阅读了大量 相关优秀的文章 ,学习到了不同的观点。虽然整个系列是经过 阅读源码、实际使用、阅读其他优秀文章 之后输出的,但不免出现错误和遗漏,欢迎大家 留言讨论。
如果觉得文章还不错,想第一时间收到文章推送,欢迎关注我的公众号。如果有问题或者想进群,号内有加我微信的入口,我可以拉你入群。在技术学习的道路上,我们一起前进!
Demo地址:https://github.com/hufeiyang/AndroidLeaning/blob/master/app/src/main/java/com/hfy/demo01/module/jetpack/databinding/BindingAdapterHelper.java
参考与感谢:
《DataBinding官方文档》
https://developer.android.com/topic/libraries/data-binding
《ViewModel 和 LiveData:为设计模式打 Call 还是唱反调?》
https://juejin.cn/post/6844903509893054471
《重学安卓:从 被误解 到 真香 的 Jetpack DataBinding!》
https://xiaozhuanlan.com/topic/9816742350
《MVVM陷阱之DataBinding》
https://www.jianshu.com/p/741103ba2ff1
.
你的 点赞、评论,是对我的巨大鼓励!