Jetpack-当 RecycleView 遇到 Databinding

  • 配合 Databinding Listadapter 实现极简 RecycleView

  • 资料来源:

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

  • 更新

    1
    20.04.17 初始化

导语

  • 两年前,写 MyPrivacy 的时候,就遇到过 recycleview 需要配合 databinding 的情况,当但是实现的那个乱啊..
  • 刷 Jetpack 官方例程,加上之前的了解,试图可以比较优雅的实现 recycleview .
  • 目标:
    • 重用 RecycleAdapter 和 ViewHolder,不需要每个类型都写一个.
    • UI 的局部刷新,不需要每次都通知整个列表刷新.
    • 便捷的事件处理,越简单越好.

绑定数据

  • 整体的思路依旧是从 借助 android databinding 框架,逃离 adapter 和 viewholder 的噩梦 (1) 而来再改进.

  • 回想一下 RecycleAdapter 和 ViewHolder 都干了那些工作?

    • Adapter 把数据集设置到整个 item 的List.
    • ViewHolder 则是数据到具体 item 的设置细节.
  • 那么 Databinding 呢?

    • 启用数据绑定后,每一个数据绑定的 xml 布局会生成一个绑定类.
    • 之后我们需要将对应的数据,设置到绑定类,之后的显示由绑定类自行完成.
  • 以往因为每一个 item 的布局都不一样,每个不同的 item 都要写一个 ViewHolder,在代码中完成数据到 UI . databinding 数据到 UI 的过程集成在xml中,代码中只剩下一个绑定操作,这样思路就出来了:

    • 使用 databinding ,使 ViewHolder 只剩下一个绑定数据的过程.数据到 UI 的过程都在 xml 布局中声明.
    • 这样就使不同的 item 共用同一个 ViewHolder 成为可能.
    • 问题: 不同的 xml 生成的绑定类都不同,如何把绑定数据这个操作统一?
  • 翻阅官方文档,我们可以通过 DataBindingUtil 类,传入 xml 布局直接生成绑定类,在 ViewHolder 中传入 ViewDataBinding 这个绑定类的基类完成绑定,只有一点副作用,需要所有 xml 中的变量名相同,这样通过才能正确通过绑定类的基类实现数据绑定.

    • 布局

      1
      2
      3
      4
      5
      <data>
      <variable
      name="item"
      type="com.js.nowakelock.data.db.entity.AppInfo" />
      </data>
    • 绑定

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class ViewHolder(var binding: ViewDataBinding) :
      RecyclerView.ViewHolder(binding.root) {
      fun bind(item: BaseItem?) {
      binding.setVariable(BR.item, item)
      binding.executePendingBindings()
      }
      }
      //创建 ViewHolder 实例时,生成绑定类.
      ViewHolder(
      DataBindingUtil.inflate(
      LayoutInflater.from(parent.context),
      layout, parent, false
      ), handler
      )
  • 这样 ViewHolder 就可以在不同的布局间通用了,但是要求我们必须传入 item 的 xml 布局.这里有两种方式.

    • 在 item 对应的类中增加 getLayout 方法,覆写 Adapter 的 getItemViewType 方法返回 item.getLayout.(原教程就是这么干的)
    • 因为我这里都是完全相同的 item,所以直接在 Adapter 传入了布局 xml.
  • 之后要处理 RecycleAdapter 了,这里有点搞笑,本来要实现 RecycleView 的局部刷新,需要写 N 多的方法,都快写完了…遇到了 Listadapter.

  • Listadapter 是 com.android.support:recyclerview-v7:27.1.0 引入的新方法,局部刷新这些事情 Listadapter 全部干了..只需要给 Listadapter 传入 DiffUtil.ItemCallback 实现类,实现比较两个 item 是否相同,和同一个 item 是否有更新的方法..具体用法使用更少代码的ListAdapter

  • 这里我们抽象出一个接口 BaseItem,作为所有 Item 的基类.

    1
    2
    3
    4
    5
    interface BaseItem {
    fun getID(): String = ""//两个item 是不是一个 item
    fun getContent(): Int = 0 //同一个 item 内容是否有更新
    // fun getLayout(): Int = 0 //如果需要传入布局
    }
  • 实现 DiffUtil.ItemCallback 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class DiffCallback : DiffUtil.ItemCallback<BaseItem>() {
    /*是否是同一对象*/
    override fun areItemsTheSame(
    oldItem: BaseItem,
    newItem: BaseItem
    ): Boolean {
    return oldItem.getID() == newItem.getID()
    }

    /*内容是否相同*/
    override fun areContentsTheSame(
    oldItem: BaseItem,
    newItem: BaseItem
    ): Boolean {
    return oldItem.getContent() == newItem.getContent()
    }
    }
  • 之后在 RecycleAdapter 声明 Listadapter 时传入 DiffCallback.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class RecycleAdapter(private val layout: Int) :
    ListAdapter<BaseItem, ViewHolder>(DiffCallback()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    return ViewHolder(
    DataBindingUtil.inflate(
    LayoutInflater.from(parent.context),
    layout, parent, false
    )
    )
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(getItem(position))
    }
    }
  • 上面就是 RecycleAdapter 的全部代码,简洁的不行..

  • 在 Fragment/Activiety 创建 adapter 的实例,并 adapter.submitList(items) 设置数据集,这样基础的绑定数据就做完了.

事件处理

  • 事件处理我更倾向于在 item 这个层级处理,而不是都扔到上层.

  • 事件处理 databinding 给了两种选择,方法引用监听器绑定.其中监听器绑定更加灵活,所以这里选择监听器绑定.

  • 以最简单的点击事件为例

    • 首先需要声明一个处理事件的处理类,如果是像 NoWakeLock 比较简单的布局,可以通过 Adapter 传入,如果布局比较复杂,可以像上文处理 Layout 一样,在 BaseItem 中增加一个 getHandler 的方法.

    • 与数据一样,为了能够正确绑定,需要将不同布局的事件处理方法变量名设为相同的,这里是 handler,绑定时就是 binding.setVariable(BR.handler, XXX).

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <data>
      <variable
      name="item"
      type="com.js.nowakelock.data.db.entity.AppInfo" />
      <variable
      name="handler"
      type="com.js.nowakelock.ui.appList.Handler" />
      </data>

      <xxxView ...
      android:onClick="@{(theView)->handler.onClick(theView,item)}"
      ...
      >
    • 监听器绑定可以给具体的方法,传入 View 对象.这里与 BaseItem 一样,声明一个处理事件的基类 BaseHandler.然后我在 Handler 中增加了一个 ViewModel 对象,这样可以直接借助 ViewModel 处理对应数据.如果 Handler 是集成在 BaseItem 中的需要用到 ViewModel,可以在 items 传入 Adapter 之前处理一下 items.或者借助依赖注入,注入全局单例的 ViewModel.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      open class BaseHandler {
      private val TAG = "BaseHandler"
      open fun Click() {
      LogUtil.d(TAG, "click")
      }
      }

      class Handler(private val viewModel: AppListViewModel) : BaseHandler() {
      fun onClick(view: View, appInfo: AppInfo) {()
      //do something
      }
      }
  • 最后 Adapter 和 ViewHolder 的代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class ViewHolder(var binding: ViewDataBinding, private val handler: BaseHandler) :
    RecyclerView.ViewHolder(binding.root) {
    fun bind(item: BaseItem?) {
    binding.setVariable(BR.item, item)
    binding.setVariable(BR.handler, handler)
    binding.executePendingBindings()
    }
    }

    class RecycleAdapter(private val layout: Int, private val handler: BaseHandler) :
    ListAdapter<BaseItem, ViewHolder>(DiffCallback()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    return ViewHolder(
    DataBindingUtil.inflate(
    LayoutInflater.from(parent.context),
    layout, parent, false
    ), handler
    )
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(getItem(position))
    }
    }

结语

  • 这一篇还是略显凌乱,该写的都写了,该说的都说了,就是读完,不能酣畅淋漓的一次性读完…功力还不够吧…
  • 借助 Jetpack 开发完初版的 NoWakeLock,总的感受就是,Jetpack 几乎处理了日常应用的全部,真方便.kotlin 真好用且简结,没有废话.(代价是必须写过代码才算是会用了)