Jetpack--Data Binding

  • 旧文填坑 Data Binding

  • 资料来源:

    https://developer.android.com/topic/libraries/data-binding

  • 更新

    1
    2
    3
    20.02.19 初始化
    20.02.23 初步刷完文档
    20.04.28 添加几个坑

导语

  • 重刷 Android,最近这两年 Android 变化还真大,迁移到了 Kotlin ,当初接触的 data binding 和架构组件都成了 jetpack 的一部分.
  • 趁着这段时间刷刷 jetpack 水几篇博客.
  • 初始仅仅是速描官方文档,随时添加内容.

概览

  • 初始上手 Android 时候最烦的就是 findViewById 每一个子 view 都要来一遍.处理监听事件也非常麻烦.
  • 即使导入了 MVP 的架构,view 层还是存在大量冗余代码,这几年 kotlin 解决了一部分,剩下的可以交给 jetpack 了.
  • data binding 可以把可观察的数据和视图绑定,当数据变化时自动更新视图.但是视图部分逻辑在 data binding 中被放到了 xml 中,而且调试非常麻烦.
  • 如果你只是需要废掉 findViewById, 请尝试 视图绑定

基础

  • 启用 data binding,在应用的 build.gradle 文件中添加 dataBinding

    1
    2
    3
    4
    5
    6
    android {
    ...
    dataBinding {
    enabled = true
    }
    }
  • Android studio 的支持

    • 语法突出显示
    • 标记表达式语言语法错误
    • XML 代码完成
    • 包括导航(如导航到声明)和快速文档在内的引用
  • 对于一个这样的类(kt 真简结)

    1
    data class User(val firstName: String, val lastName: String)
  • 布局声明:

    • 要在 <data> 之间声明.

    • 要声明变量名和类型

    • 布局中通过 @{} 访问变量.并且可以直接访问属性.

    • 默认会检查 Null

    • 例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      <?xml version="1.0" encoding="utf-8"?>
      <layout xmlns:android="http://schemas.android.com/apk/res/android">
      <data>
      <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}"/>
      </LinearLayout>
      </layout>
  • 绑定数据

    • 每个布局文件都会生成一个绑定类.默认情况下,绑定类的名称称基于布局文件的名称驼峰大小写 + Binding 后缀.例如布局文件名 activity_main.xml,绑定类ActivityMainBinding.

    • 例:

      1
      2
      3
      val binding: ActivityMainBinding = DataBindingUtil.setContentView(
      this, R.layout.activity_main)
      binding.user = User("Test", "User")
    • Fragment ListView 或 RecyclerView 适配器中使用数据绑定.

      1
      2
      3
      val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
      // or
      val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
  • 所以使用 databinding 的流程就是:

    • 设计可观察类
    • 声明 xml 布局
    • 生成加载 xml 的绑定类实例.
    • 实例化数据类,绑定.剩下的显示数据等系统会搞定.

xml 访问

  • xml 中支持的表达式(非常不推荐在 xml 中堆积太多逻辑)

    • 算术运算符 + - / * %
    • 字符串连接运算符 +
    • 逻辑运算符 && ||
    • 二元运算符 & | ^
    • 一元运算符 + - ! ~
    • 移位运算符 >> >>> <<
    • 比较运算符 == > < >= <=(请注意,< 需要转义为 <)
    • instanceof
    • 分组运算符 ()
    • 字面量运算符 - 字符、字符串、数字、null
    • 类型转换
    • 方法调用
    • 字段访问
    • 数组访问 []
    • 三元运算符 ?:
  • xml 不支持的表达式

    • this
    • super
    • new
    • 显式泛型调用
  • 空合并运算符 ?? : android:text="@{user.displayName ?? user.lastName}" 等价于 android:text="@{user.displayName != null ? user.displayName : user.lastName}"

  • 可使用 [] 运算符访问常见集合,例如数组 列表 稀疏列表和映射.有一点限制是 < 必须转义成 &lt; 才能在 xml 中识别

  • 字符串: '@{map["firstName"]}' 或者 android:text="@{map[```firstName```]}".

事件处理

  • databinding 可以在 xml 上分派事件.一般的 View.OnClickListener -> onClick()

    • SearchView -> android:onSearchClick
    • ZoomControls -> android:onZoomIn
    • ZoomControls -> android:onZoomOut
  • 处理事件可以有方法引用或者监听器绑定.

  • 方法引用:大致是直接在 onClick() = 处理的方法.表达式中的方法名必须与监听器对象中的方法名完全一致.好处是编译器就处理.

    1
    2
    3
      class MyHandlers {
    fun onClickFriend(view: View) { ... }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    <variable name="handlers" type="com.example.MyHandlers"/>
    <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}"
    android:onClick="@{handlers::onClickFriend}"/>
    </LinearLayout>
    </layout>
    ...
  • 监听器绑定: 事件发生时进行求值的 lambda 表达式.要求 Gradle 2.0 版及更高版本.允许运行任意数据绑定表达式.

    1
    2
    3
    class Presenter {
    fun onSaveClick(task: Task){}
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    <variable name="task" type="com.android.example.Task" />
    <variable name="presenter" type="com.android.example.Presenter" />
    </data>
    <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
    <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:onClick="@{() -> presenter.onSaveClick(task)}" />
    </LinearLayout>
    </layout>
    ...
    • 上面例子中也可以写成 android:onClick="@{(view) -> presenter.onSaveClick(task)}"

    • 传入 View 参数: fun onSaveClick(view: View, task: Task){}android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

    • 多个参数: fun onCompletedChanged(task: Task, completed: Boolean){}android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}"

    • 监听的事件返回类型不是 void ,lambda 也必须返回对应的类型.

    • 监听器表达式功能非常强大,具体的业务逻辑必须在对应方法中而不是堆积在 xml 中.

  • 坑: 有一些绑定的属性,Android Studio 会提示不存在,但是实际上却又可用.包括并不限于

    1
    2
    3
    4
    "android:beforeTextChanged",
    "android:onTextChanged",
    "android:afterTextChanged",
    "android:textAttrChanged

可观察对象

  • 可观察性是指一个对象将其数据变化通知给其他对象的能力.数据变了通知 UI 更新.

  • 从数据类型上看分为 对象 字段 集合.

  • 可观察字段: 当只需要少数一些属性可观察时,不需要继承实现 Observable 接口.只需要把属性改为 ObservableXXX 类型即可.访问时使用 set/get 方法.(kt 中相当于直接访问)

    • ObservableBoolean
    • ObservableByte
    • ObservableChar
    • ObservableShort
    • ObservableInt
    • ObservableLong
    • ObservableFloat
    • ObservableDouble
    • ObservableParcelable
  • 可观察集合

    • ObservableArrayMap 当键值为引用类型.
    • ObservableArrayList 键值为整数
    • 都可以直接在 xml 中直接访问.
  • 可观察类

    • 如果非要从头设计数据结构.我们可以继承并实现 Observable 接口.get 使用 @Bindable 修饰.set 则在值更新后调用 notifyPropertyChanged() 通知 UI 更新.(kt 中因为语法要绕一点)

    • 数据绑定会在模块包中生成一个名为 BR 的类,该类包含用于数据绑定的资源的 ID.在编译期间,Bindable 注释会在 BR 类文件中生成一个条目,这样在 notifyPropertyChanged() 才会有 BR.x .所以如果 notifyPropertyChanged() 报错找不到 BR.x 先编译一下工程.

    • 例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
        class User : BaseObservable() {
      @get:Bindable
      var firstName: String = ""
      set(value) {
      field = value
      notifyPropertyChanged(BR.firstName)
      }
      @get:Bindable
      var lastName: String = ""
      set(value) {
      field = value
      notifyPropertyChanged(BR.lastName)
      }
      }

生成绑定类

  • 默认情况下,系统会为每个布局文件生成一个绑定类.类名称基于布局文件的名称驼峰写法 + Binding 后缀.例如 activity_main.xml 对应类为 ActivityMainBinding.绑定类包含从布局属性到布局视图的所有绑定,并且知道如何为绑定表达式指定值.

  • 绑定类的 inflate() 方法.有两个重载方法.

    1
    2
    3
    val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
    //增加 ViewGroup 参数
    val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false)
  • 布局使用其他机制扩充的,单独绑定.

    1
    val binding: MyLayoutBinding = MyLayoutBinding.bind(viewRoot)
  • 如无法预先知道绑定类型,可以使用 DataBindingUtil 类创建绑定类.

    1
    2
    val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
    val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)
  • 使用 DataBindingUtil 类 inflate 方法,在 Fragment ListView 或 RecyclerView 常用.

    1
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
  • 自定义绑定类名

    • 绑定类的默认规则上文已经提到了.有时候 xml 的名称驼峰写法 + Binding.默认位置在 模块包.databinding 下.

    • 调整 data 元素的 class 特性,可以自定义名称和路径.

    • 自定义类名,生成 ContactItem 绑定类.

      1
      2
      3
      <data class="ContactItem">

      </data>
    • 自定义路径,在类名前添加句点和前缀,示例即在模块包生成.

      1
      2
      3
      <data class=".ContactItem">

      </data>
    • 也可以使用完整软件包名称来生成绑定类.

      1
      2
      3
      <data class="com.example.ContactItem">

      </data>

自定义 set() 方法

  • 在 xml 中直接绑定调用函数是很爽,但是总有一些是默认方法覆盖不到的.例如传入 url 利用 Glide 设置 ImageView 显示,ImageView 就不可能有对应的方法.这个时候就需要我们自定义逻辑.

  • 自定义的 set() 适配器方法

    • 必须是 BindingAdapter 注释的静态绑定适配器方法.(kt 中沒有静态方法,所以如果是在普通类中需要 @JvmStatic 修饰).

    • 注释没有声作用域的默认调用是在 app:xx 下.如 @BindingAdapter("android:paddingLeft"),调用时就是 app:imageUrl="@{venue.imageUrl}".

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @JvmStatic
      @BindingAdapter("loadIcon")
      fun LoadIcon(imageView: ImageView, appInfo: AppInfo) {
      val options = RequestOptions()
      .error(R.drawable.sym_def_app_icon)
      .placeholder(R.drawable.sym_def_app_icon)
      val uri = Uri.parse("android.resource://" + appInfo.packageName + "/" + appInfo.icon)
      Glide.with(imageView.context)
      .applyDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
      .load(uri)
      .apply(options)
      .into(imageView)
      }
  • 在 xml 中直接调用 app:loadIcon="@{appInfo}"

双向数据绑定

  • databinding 不仅提供了数据到 UI 的绑定,同时还可以反向绑定.即 UI 改变时数据也跟着变动.例如一个 bool 类型数据绑定到了一个 Button 上,我们声明了双向的数据绑定,当 Button 的状态改变时,对应数据的值也会改变.

  • 实现双向绑定非常简单,在 xml 使用变量时由 @ 改为 @= 即可.

  • 双向数据绑定对数据类型有要求,如果是系统提供的 ObservableXXX ,无需修改类中已经实现了,但是我们继承并实现 Observable 接口的类,必须实现 setter 方法,且调用 notifyPropertyChanged() 通知 UI.

  • 转换器

    • 当需要双向绑定的数据类型不匹配时,可以自定义 Converter 对象,完成对应的数据转换.

    • 1
      2
      3
      4
      <EditText
      android:id="@+id/birth_date"
      android:text="@={Converter.dateToString(birthDate)}"
      />
    • 例子

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      object Converter {
      @InverseMethod("stringToDate")
      fun dateToString(
      view: EditText, oldValue: Long,
      value: Long
      ): String {
      // Converts long to String.
      }

      fun stringToDate(
      view: EditText, oldValue: String,
      value: String
      ): Long {
      // Converts String to long.
      }
      }
  • 还可以将双向绑定和自定义方法结合起来,详情见 使用自定义特性的双向数据绑定.

与 RecyclerView 连用

  • 详情见 Jetpack-当 RecycleView 遇到 Databinding

  • 简单描述一下,不再上代码了.参考了 借助 android databinding 框架,逃离 adapter 和 viewholder 的噩梦 (1)

  • 最主要的工作是要实现一个自定义的 Adapter 继承自 RecyclerView.Adapter.实现 3 个主要的方法.

    • public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType)//创建返回ViewHolder实例
    • public void onBindViewHolder(ViewHolder holder,int pisition)//数据与界面绑定
    • public int getItemCount() // 返回数据的数量
  • 我们先来看看 RecyclerView.Adapter 创建一个新 Item 时简单的工作流程.

    • 调用 onCreateViewHolder 创建一个 ViewHolder 持有 View.
    • 创建布局时,调用 onBindViewHolder 将 item 的数据显示到 View 上.
  • 对比 databinding 的流程.与 RecyclerView 配合有以下的想法.

    • onCreateViewHolder 不再返回 ViewHolder 了,改为返回一个根据 xml 布局生成的绑定类.
    • onBindViewHolder 不再是一个个具体的字段设定.改为将数据和绑定类绑定.具体字段的映射到 UI 已经定义在了绑定类中.
  • 问题

    • 不同的 xml 的绑定类默认都是不同类型的,一个 item 对应一个绑定类工作量太大.
    • 每个 item 对应的数据类都不相同.无法用简单的代码实现绑定.
  • 第一个问题:

    • 在 onCreateViewHolder 每个具体的 item 获取到 xml.利用 DataBindingUtil 类的 inflate 生成绑定类.
    • 绑定类的类型声明为 ViewDataBinding,ViewDataBinding 是所有绑定类的基类.
    • item 要实现一个 getType() 返回 xml 的接口,同时重写 getItemViewType 根据 position 调用 getType() 返回 xml 布局.
  • 第二个问题

    • 针对任意布局运行的 Adapter 并不知道特定绑定类,但调用 onBindViewHolder() 时,仍必须指定绑定值.
    • 需要用到 databinding 的动态变量.setVariable() 方法.
    • 例: mbinding.setVariable(BR.item, item) ,这样有个要求 xml 声明变量时都必须声明为 item

与架构组件配合

  • LiveData

    • LiveData 可以感知组件的生命周期,有一堆的好处,不再叙述😂.

    • Android Studio 3.1 以后可以使用 LiveData 替换可观察字段,即上文那些 ObservableXXX.

    • 因为 LiveData 对组件生命周期感知,需要指定生命周期的所有者.

    • 例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class ViewModelActivity : AppCompatActivity() {
      override fun onCreate(savedInstanceState: Bundle?) {
      // Inflate view and obtain an instance of the binding class.
      val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)

      // 生命周期的所有者
      binding.setLifecycleOwner(this)
      }
      }
  • 坑: Livedate 下kotlin 的 boolean 类型不支持双向绑定通知.

    • 例如,根据 boolean 变量的值显示或者隐藏 View.
      • android:checked="@={flag}"
      • android:visibility="@{flag ? View.VISIBLE : View.GONE}"
    • kotlin 的 Boolean 类型,当 flag 值变化时,View 没有任何变化.但是当 flag 是 java 类型的 Boolean 时,view 可以正常显示隐藏.
    • 原因未知,目前暂时以 ObservableBoolean 代替,可以正常使用..
  • ViewModel

    • 与 ViewModel 的配合有点奇怪.等刷完 jetpack 的 ViewModel 后再回头刷.