这是项目总结的第二篇,上一篇在这:模仿Google News的TabLayout
在 GeekNews 中我尝试使用 DataBinding
来简化代码,本来是使用 BRVAH 这个优秀的开源框架作为基础 RecyclerView 的 Adapter使用的,但是写的过程中发现在 Adapter 里每次要写一堆样板代码,想着既然是实验项目,就干脆自己封装一个好了。
本篇blog主要是记录一下当时的思路和关于 Kotlin 的一些泛型知识。
需要哪些功能
- 通过最简单的方式的绑定数据
- item的点击事件
- 分页和预加载
简单的成果展示
下面这个 Adapter 是项目里代码最少的一个
class DataTypeAdapter(items: MutableList) : BaseAdapter(items, R.layout.item_type_adapter) {
override fun bindItem(binding: ItemTypeAdapterBinding, item: Data.Results) {
binding.data = item
}
}
复制代码
实现的视图效果:
DataBinding 的预备知识
- 首先我们都知道当我们用
DataBinding
写完一个布局之后会生成一个相应的Binding类,比如布局文件是 R.layout.item_feed_adapter, 对应的类则是 ItemFeedAdapterBinding,而所有 Binding 类都继承自ViewDataBinding
这个抽象类 - 根据官网文档,绑定视图的方式有两种:
由于val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false) // or val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false) 复制代码
ViewDataBinding
这个类没有inflate
方法, 我们选择第二种方法 - 根据官网文档,对于单个布局绑定数据只有一种方式,比如你在布局里写了这样一个东西:
那Binding类里就会生成这样一个方法:<variable name="abc" type="Results" /> 复制代码
那绑定数据就只能这样:public abstract void setAbc(@Nullable Results abc); 复制代码
可以看出来,完 全 没 有 规 律,所以这个绑定数据的过程必须要抽出来让子类自己实现了。binding.abc = Results() 复制代码
(文档里其实还有一种 setVariable 的方式绑定数据,不过那个是用来共享变量名的,这里用不到)
实现
-
基于上面的分析,我们可以得到初步封装好的代码:
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 的代码。 -
为防止绑定数据没有及时生效,我们最好不要把业务代码一起写在
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) {} 复制代码
-
再加上 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 就行了。
-
加上分页和预加载功能,这里因为项目里只有 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