Kotlin结合DataBinding简单封装一个RecyclerView的Adapter

这是项目总结的第二篇,上一篇在这:模仿Google News的TabLayout

在 GeekNews 中我尝试使用 DataBinding 来简化代码,本来是使用 BRVAH 这个优秀的开源框架作为基础 RecyclerView 的 Adapter使用的,但是写的过程中发现在 Adapter 里每次要写一堆样板代码,想着既然是实验项目,就干脆自己封装一个好了。

本篇blog主要是记录一下当时的思路和关于 Kotlin 的一些泛型知识。

需要哪些功能

  1. 通过最简单的方式的绑定数据
  2. item的点击事件
  3. 分页和预加载

简单的成果展示

下面这个 Adapter 是项目里代码最少的一个

class DataTypeAdapter(items: MutableList) : BaseAdapter(items, R.layout.item_type_adapter) {
    override fun bindItem(binding: ItemTypeAdapterBinding, item: Data.Results) {
        binding.data = item
    }
}
复制代码

实现的视图效果:

DataBinding 的预备知识

  1. 首先我们都知道当我们用 DataBinding 写完一个布局之后会生成一个相应的Binding类,比如布局文件是 R.layout.item_feed_adapter, 对应的类则是 ItemFeedAdapterBinding,而所有 Binding 类都继承自 ViewDataBinding 这个抽象类
  2. 根据官网文档,绑定视图的方式有两种:
    val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
    复制代码
    由于 ViewDataBinding 这个类没有 inflate 方法, 我们选择第二种方法
  3. 根据官网文档,对于单个布局绑定数据只有一种方式,比如你在布局里写了这样一个东西:
    <variable
        name="abc"
        type="Results" />
    复制代码
    那Binding类里就会生成这样一个方法:
    public abstract void setAbc(@Nullable Results abc);
    复制代码
    那绑定数据就只能这样:
    binding.abc = Results()
    复制代码
    可以看出来,完 全 没 有 规 律,所以这个绑定数据的过程必须要抽出来让子类自己实现了。
    (文档里其实还有一种 setVariable 的方式绑定数据,不过那个是用来共享变量名的,这里用不到)

实现

  1. 基于上面的分析,我们可以得到初步封装好的代码:

    abstract class BaseAdapter<T, B : ViewDataBinding>(private var items: MutableList, private var layoutRes: Int) : RecyclerView.Adapter.ViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            val inflater = LayoutInflater.from(parent.context)
            val binding = DataBindingUtil.inflate(inflater, layoutRes, parent, false)
            return ViewHolder(binding)
        }
        
        override fun getItemCount() = items.size
        
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            holder.bind(items[position])
        }
        
        fun setNewData(newItems: List<T>) {
            items.clear()
            items.addAll(newItems)
            notifyDataSetChanged()
        }
    
        inner class ViewHolder(private val binding: B) : RecyclerView.ViewHolder(binding.root) {
            fun bind(item: T) {
                bindItem(binding, item)
                binding.executePendingBindings()
            }
        }
        
        abstract fun bindItem(binding: B, item: T)
    }
    复制代码

    这里关于泛型B,要不要声明协变,即 out B:ViewDataBinding, 看起来这个类在 adapter 里是个生产者,可以写,但其实不用,因为Binding类都是自动生成的,互相之间不会有继承关系,不会出现子类 adapter 赋值给父类 adapter 的代码。

  2. 为防止绑定数据没有及时生效,我们最好不要把业务代码一起写在 bindItem 方法里,所以再抽出来一个方法 bindAfterExecute 用来写额外的业务,比如处理图片,不声明成 abstract 的原因是这个方法不是必须要实现的

    inner class ViewHolder(private val binding: B) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item: T) {
            bindItem(binding, item)
            binding.executePendingBindings()
            bindAfterExecute(binding, item)
        }
        fun getBinding() = binding
    }
    abstract fun bindItem(binding: B, item: T)
    open fun bindAfterExecute(binding: B, item: T) {}
    复制代码
  3. 再加上 Item 点击事件 声明一个 lambda 变量和参数为一个 lambda 的点击方法

    private var itemClickListener: ((item: T, binding: B) -> Unit)? = null
    ...
    fun setItemClick(click: (item: T, binding: B) -> Unit) {
        itemClickListener = click
    }
    复制代码

    然后在 ViewHolder 的 bind 里调用

    fun bind(item: T) {
        ...
        binding.root.setOnClickListener {
            itemClickListener?.invoke(item, binding)
        }
        ...
    }
    复制代码

    调用示例:

    adapter.setItemClick { item, _ ->
        ARouter.getInstance().build("/home/webActivity")
            .withString("web_url", item.url)
            .withString("title", item.title)
            .withString("cover_url", item.cover)
            .withBoolean("isArticle", true)
            .navigation()
    }
    复制代码

    那么问题来了,如果要对视图中某个view暴露点击事件该怎么办,思路和上面的实现方法一样,添加不同的 Lambda 就行了。

  4. 加上分页和预加载功能,这里因为项目里只有 LinearLayoutManager 的 RecyclerView,所以没对其他布局兼容,而且预加载的时机我直接写死是剩10条了(因为懒),注意要对 RecyclerView 滑动中和停止两种状态都做监听。setLoadComplete() 和 setLoadFail() 的实现完全一样是因为本来我想写个footer之类的东西展示失败的视图,就分成了两个方法,后来因为懒没写。

    private var isLoading = false
    
    fun setLoadMoreListener(recyclerView: RecyclerView, loader: () -> Unit) {
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(ry: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(ry, dx, dy)
                val lastVisibleItem = (ry.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
                val totalItemCount = ry.layoutManager.itemCount
                if (lastVisibleItem >= totalItemCount - 10 && dy > 0) {
                    if (!isLoading) {
                        loader()
                        isLoading = true
                    }
                }
            }
            override fun onScrollStateChanged(ry: RecyclerView, newState: Int) {
                super.onScrollStateChanged(ry, newState)
                val lastVisibleItem = (ry.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
                val totalItemCount = ry.layoutManager.itemCount
                if (lastVisibleItem >= totalItemCount - 10 && newState == 0) {
                    if (!isLoading) {
                        loader()
                        isLoading = true
                    }
                }
            }
        })
    }
    fun setLoadComplete() {
        isLoading = false
    }
    fun setLoadFail() {
        isLoading = false
    }
    fun addData(data: List<T>) {
        items.addAll(data)
        notifyItemRangeInserted(items.size - data.size, data.size)
    }
    复制代码

    调用示例:

    adapter.setLoadMoreListener(ryc_main) {
        page++
        viewModel.requestData(page)
    }
    ...
    viewModel.getFeed().observe(this, Observer {
        it?.let { data ->
            if (page == 1) {
                adapter.setNewData(data)
            } else {
                adapter.addData(data)
                adapter.setLoadComplete()
            }
        }
        ...
    })
    复制代码

总结

虽然这个东西很简单,实际上确实很简单。。。(强行水了一篇blog好惭愧)
完整的代码戳这里 BaseAdapter

你可能感兴趣的:(Kotlin结合DataBinding简单封装一个RecyclerView的Adapter)