Data Binding 详解(四)-生成的绑定类

作者:木易
知是行之始,行是知之成。
文章配套的 Demohttps://github.com/muyi-yang/DataBindingDemo

Data Binding 生成用于访问布局变量和视图的绑定类,它将布局变量与布局中的视图链接起来。默认情况下,类的名称基于布局文件的名称,将其转换为Pascal大小写并向其添加Binding后缀。比如布局文件名是 activity_main.xml 相应生成的类 ActivityMainBinding,也可以自定义绑定类的名称和包。 所有以 <data> 为根标签的布局都会生成绑定类,都继承自 ViewDataBinding 类。这个类包含从布局属性(例如,声明的变量)到布局视图的所有绑定,并且知道如何为绑定表达式分配值,有兴趣的同学可以看看生成后的类是怎么实现的。

创建绑定对象

创建绑定类的对象有两种方式,一种是使用 DataBindingUtil 工具类,一种是直接使用绑定类的静态方法来获取。

在 Activity 中我们一般使用 DataBindingUtil 工具类来获取绑定对象,因为它有一个 setContentView 方法,里面调用了 Activity 的 setContentView 方法并返回了绑定类对象,比如:

  1. ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

也可以通过 DataBindingUtil 工具类的 inflate 方法获取:

  1. ActivityMainBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.activity_main, null, false);

但在其他地方(Fragment、ListView 或 RecyclerView 等)我们一般会直接使用绑定类的静态方法来获取,比如:

  1. LayoutCelebrityItemBinding binding = LayoutCelebrityItemBinding.inflate(inflater);
  2. // 或者
  3. LayoutCelebrityItemBinding binding = LayoutCelebrityItemBinding.inflate(inflater, viewGroup, false);

有时你可能需要使用老方式 inflate 一个布局,你可以这样获取绑定对象:

  1. View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
  2. MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot)

有时无法事先知道绑定类型,布局可能是动态的。 在这种情况下,可以使用 DataBindingUtil 类创建绑定,如下面的代码片段所示:

  1. View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
  2. ViewDataBinding binding = DataBindingUtil.bind(viewRoot)
  3. // 或者
  4. ViewDataBinding binding = DataBindingUtil.inflate(getLayoutInflater(), layoutId, parent, attachToParent);

至此我们了解了两个获取绑定类对象的两种方式,可以根据不同的场景运用不同的方式,其实绑定类中的 inflate 方法的最终也是调用 DataBindingUtil 的 inflate 方法,bind 方法也是一样,有兴趣的同学可以阅读一下生成的绑定类代码。

带 ID 的 View

Data Binding 在绑定类中为每个在布局中具有 ID 的 View 创建不可变字段。例如,Data Binding 从以下布局创建类型为 TextView 的 tvInfo,类型为 Button 的 btnBinding 字段:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android">
  3. ...
  4. <android.support.constraint.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
  5. xmlns:tools="http://schemas.android.com/tools"
  6. android:layout_width="match_parent"
  7. android:layout_height="match_parent"
  8. tools:context=".MainActivity">
  9. <TextView
  10. android:id="@+id/tv_info"
  11. ... />
  12. <Button
  13. android:id="@+id/btn_binding"
  14. ... />
  15. <Button
  16. android:id="@+id/btn_list"
  17. ... />
  18. </android.support.constraint.ConstraintLayout>
  19. </layout>

在绑定类中已经生成了相应的字段,我们不在需要调用 findViewById () 方法获取,可以直接从绑定类中获取:

  1. ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
  2. binding.tvInfo.setText("我是使用Data Binding的Demo");

变量

Data Binding 为布局中声明的每个变量生成访问方法。 例如,下面的布局在绑定类中为 user、 index 变量生成 setter 和 getter 方法:

  1. <!--activity_user.xml-->
  2. <?xml version="1.0" encoding="utf-8"?>
  3. <layout xmlns:android="http://schemas.android.com/apk/res/android"
  4. xmlns:bind="http://schemas.android.com/tools">
  5. <data>
  6. <import type="com.example.databindingdemo.bean.UserInfo" />
  7. ...
  8. <variable
  9. name="user"
  10. type="UserInfo" />
  11. <variable
  12. name="index"
  13. type="int" />
  14. ...
  15. </data>
  16. ...
  17. </layout>

在获取了绑定类对象的地方可以通过 setter 方法设置数据,通过 getter 方法获取数据:

  1. public class UserActivity extends AppCompatActivity {
  2. private ActivityUserBinding binding;
  3. @Override
  4. protected void onCreate(@Nullable Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
  7. ...
  8. binding.setUser(info);
  9. binding.setIndex(1);
  10. ...
  11. }
  12. }

ViewStub的使用

ViewStub 与普通 View 不同,它开始时是一个不可见的 View。 当设置可见或者调用 inflate() 方法时,它们会在布局中通过 inflate 另一个布局来替换自己。

因为 ViewStub 中的布局实际上在 View 层次结构中并没有加载,所以在绑定对象中也不能直接创建 ViewStub 中的布局对象,必须要在使用的时候再创建,所以绑定类中使用 ViewStubProxy 对象取代了 ViewStub,你可以使用它来访问 ViewStub,当 ViewStub 被创建和加载后你可以通过它来访问具体的布局结构。以下为布局:

  1. <!--activity_binding.xml-->
  2. <?xml version="1.0" encoding="utf-8"?>
  3. <layout xmlns:android="http://schemas.android.com/apk/res/android"
  4. xmlns:app="http://schemas.android.com/apk/res-auto">
  5. ...
  6. <ViewStub
  7. android:id="@+id/vs_bar"
  8. android:layout_width="match_parent"
  9. android:layout_height="wrap_content"
  10. android:layout="@layout/layout_bar" />
  11. <Button
  12. android:layout_width="wrap_content"
  13. android:layout_height="wrap_content"
  14. android:onClick="@{()->activity.showViewStub()}"
  15. android:text="@string/show_stub" />
  16. ...
  17. </layout>
  18. <!--layout_bar.xml-->
  19. <?xml version="1.0" encoding="utf-8"?>
  20. <layout xmlns:android="http://schemas.android.com/apk/res/android"
  21. xmlns:app="http://schemas.android.com/apk/res-auto">
  22. <data>
  23. <variable name="resId" type="int" />
  24. <variable name="name" type="String" />
  25. </data>
  26. <LinearLayout
  27. android:layout_width="match_parent"
  28. android:layout_height="wrap_content">
  29. <ImageView
  30. android:layout_width="100dp"
  31. android:layout_height="100dp"
  32. android:layout_margin="10dp"
  33. android:src="@drawable/default_mini_avatar"
  34. app:image="@{resId}" />
  35. <TextView
  36. android:layout_width="match_parent"
  37. android:layout_height="wrap_content"
  38. android:layout_gravity="center"
  39. android:layout_margin="10dp"
  40. android:gravity="center"
  41. android:text="@{name, default=@string/default_name}"
  42. android:textSize="20sp" />
  43. </LinearLayout>
  44. </layout>

这里在布局中声明了一个 ViewStub,并为它设置 layout_bar.xml 布局,同时声明了一个 Button 并为它设置了点击事件(调用 showViewStub() 方法)。

当你想使用 ViewStub 中的布局时,你需要获取到里面的绑定类对象,你可以向 ViewStubProxy 设置一个 OnInflateListener 监听器,然后在监听器回调中获取绑定类。比如:

  1. public class BindingClassActivity extends AppCompatActivity {
  2. private ActivityBindingBinding binding;
  3. @Override
  4. protected void onCreate(@Nullable Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. binding = DataBindingUtil.setContentView(this, R.layout.activity_binding);
  7. binding.vsBar.setOnInflateListener(new ViewStub.OnInflateListener() {
  8. @Override
  9. public void onInflate(ViewStub stub, View inflated) {
  10. LayoutBarBinding vsBarBinding = DataBindingUtil.bind(inflated);
  11. vsBarBinding.setName("木易");
  12. vsBarBinding.setResId(R.drawable.head);
  13. }
  14. });
  15. ...
  16. }
  17. ...
  18. public void showViewStub() {
  19. ViewStub viewStub = binding.vsBar.getViewStub();
  20. if (viewStub != null) {
  21. viewStub.inflate();
  22. }
  23. }
  24. ...
  25. }

可以看到 showViewStub() 方法中调用 ViewStub 的 inflate() 方法,当 Button 被点击时就会触发布局的加载,加载完成后会触发 OnInflateListener 的回调,然后在回调方法中通过 DataBindingUtil.bind(inflated)获取到了 ViewStub 中的布局绑定类,继而进行数据绑定。

立即绑定

当变量或 observable 对象数据发生变化时,数据绑定将在 View 的下一帧刷新之前更改。 但是,有时必须立即执行数据绑定。 若要强制执行,可以使用 executePendingBindings ()方法。一般情况不需要这么做。

动态变量

有时候你的布局文件是动态的,比如在一个 RecyclerView 中展现一系列新闻内容,有些内容是带图片的,有些是纯文本,这时我们可以采用多个 item 布局文件来呈现不一样的 UI 效果。比如:

  1. public class NewsAdapter extends RecyclerView.Adapter {
  2. ...
  3. @NonNull
  4. @Override
  5. public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
  6. LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
  7. ViewDataBinding binding;
  8. if (viewType == VIEW_TYPE_TEXT) {
  9. binding = LayoutNewsItemTextBinding.inflate(inflater, viewGroup, false);
  10. } else {
  11. binding = LayoutNewsItemPictureBinding.inflate(inflater, viewGroup, false);
  12. }
  13. return new NewsViewHolder(binding.getRoot(), binding);
  14. }
  15. ...
  16. }

这里通过类型判断,分别使用了 layout_news_item_text.xmllayout_news_item_picture.xml 布局文件。这两个布局文件 UI 展现不一样,但需要的数据类型都是 NewsInfo:

  1. <!--layout_news_item_picture.xml-->
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto">
  4. <data>
  5. <variable
  6. name="info"
  7. type="com.example.databindingdemo.bean.NewsInfo" />
  8. </data>
  9. ...
  10. </layout>
  11. <!--layout_news_item_text.xml-->
  12. <layout xmlns:android="http://schemas.android.com/apk/res/android"
  13. xmlns:app="http://schemas.android.com/apk/res-auto">
  14. <data>
  15. <variable
  16. name="info"
  17. type="com.example.databindingdemo.bean.NewsInfo" />
  18. </data>
  19. ...
  20. </layout>

这种情况下我们在 RecyclerView.Adapter 的 onBindViewHolder() 方法中就无法准确的知道绑定类类型,但是我们任然要为其绑定数据,我们可以这样做:

  1. public class NewsAdapter extends RecyclerView.Adapter {
  2. private List<NewsInfo> data = new ArrayList<>();
  3. ...
  4. @Override
  5. public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
  6. NewsViewHolder holder = (NewsViewHolder) viewHolder;
  7. ViewDataBinding binding = holder.binding;
  8. binding.setVariable(BR.info, data.get(position));
  9. }
  10. ...
  11. }

在这里我们并没有获取具体的绑定类,而是获取了 ViewDataBinding 类,它是一个抽象类,是所有绑定类的父类。它提供了一个 setVariable(int variableId, Object value) 方法(第一个参数为布局中声明的绑定变量 ID,第二个参数为要绑定的数据),通过这个方法我们可以动态的为布局中的变量绑定相应的数据。

BR 是 Data Binding 自动生成的资源 ID 文件,它包含所有的数据绑定变量的 ID,类似于 Android 的 R 文件。在上面例子中,RB.info 是布局中 info 变量的 ID。

后台线程

你可以在后台线程中更改数据,只要它不是一个集合。Data Binding 会在计算时将每个变量/字段在各个线程中做一份数据拷贝,以避免同步问题。

自定义绑定类名称

默认情况下 Data Binding 将根据布局文件的名称生成绑定类,以大写字母开头,删除下划线,驼峰格式命名,并添加 Binding 后缀。该类放在模块包下的 databinding 包中。例如,布局文件 layout_custom.xml 生成 LayoutCustomBinding类,放在 com.example.databindingdemo.databinding包中。

通过调整 data 标签的 class 属性,可以重命名绑定类或将绑定类放在不同的包中。例如,以下布局在当前模块的 databinding 包中生成绑定类 MyCustom:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android">
  3. <data class="MyCustom">
  4. </data>
  5. <LinearLayout
  6. android:layout_width="match_parent"
  7. android:layout_height="match_parent">
  8. <TextView
  9. android:layout_width="match_parent"
  10. android:layout_height="wrap_content" />
  11. </LinearLayout>
  12. </layout>

效果图:image.png 你可以通过在类名前加一个句点来使生成绑定类在模块包中:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android">
  3. <data class=".MyCustom">
  4. </data>
  5. ...
  6. </layout>

效果图:image.png 你还可以在类名前使用完整的包名。下面的示例在 com.custom 包中创建 MyCustom 绑定类:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <layout xmlns:android="http://schemas.android.com/apk/res/android">
  3. <data class="com.custom.MyCustom">
  4. </data>
  5. ...
  6. </layout>

效果图:image.png