再android app开发中,多个ViewType类型的RecyclerView场景还是很常见的。
常规的实现逻辑:把所有类型item的创建、数据绑定等逻辑都放到同一个RecyclerView.Adapter子类中。随着ViewType类型数量的增加,Adapter类的代码量就急剧膨胀。
该如何优化呢?
把每个ViewType对应的逻辑都独立出来是个不错的尝试:
-
为每个ViewType准备一个抽象类SubAdapter的子类,该类中也有onCreateViewHolder、onBindViewHolder等抽象方法,然后把本来写在RecyclerView.Adapter中的代码拷贝到SubAdapter对应的方法中来。这样不同ViewType的逻辑都汇聚到各自的SubAdapter中啦。
-
把被掏空的RecyclerView.Adapter改造成MultiItemTypeAdapter,该主adapter中有每个SubAdapter子类的唯一实例,当主adapter的onCreateViewHolder、onBindViewHolder等方法执行时,就把逻辑委托给对应的SubAdapter执行。
先来看下使用时的完整流程:
class MultiViewTypeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_multi_view_type)
initView()
}
private fun initView(){
recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
val adapter = MultiItemTypeAdapter<ValueBean>(NormalSubAdapter(), HeaderSubAdapter()) //构造函数中注册
adapter.registerSubAdapter(ADSubAdapter()) //也可以调用方法注册
adapter.registerSubAdapter(FooterSubAdapter())
adapter.syncData(genListData())
recycler_view.adapter = adapter
}
//Header布局相关的所有逻辑都封装到这个独立的类中。
class HeaderSubAdapter: SubAdapter<ValueBean, HeaderSubAdapter.HeaderItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup): HeaderItemViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_multi_viewtype_header, parent, false)
return HeaderItemViewHolder(itemView)
}
override fun onBindViewHolder(holder: HeaderItemViewHolder, position: Int) {
holder.contentView.text = "this is header"
}
//判断列表中position这个位置是否Header item
override fun isSameViewType(position: Int): Boolean {
val listData = getData() //SubAdapter中可以通过getData()方法获取列表数据
return listData[position] is HeaderWrapper
}
//Header布局对应的ViewHolder
class HeaderItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val contentView = itemView.findViewById<TextView>(R.id.content_view)
}
}
//Footer布局
class FooterSubAdapter: SubAdapter<ValueBean, FooterSubAdapter.FooterItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup): FooterItemViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_multi_viewtype_footer, parent, false)
return FooterItemViewHolder(itemView)
}
override fun onBindViewHolder(holder: FooterItemViewHolder, position: Int) {
holder.contentView.text = "this is Footer"
}
override fun isSameViewType(position: Int): Boolean {
return getData()[position] is FooterWrapper
}
class FooterItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val contentView = itemView.findViewById<TextView>(R.id.content_view)
}
}
//广告布局
class ADSubAdapter: SubAdapter<ValueBean, ADSubAdapter.ADItemViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup):ADItemViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_multi_viewtype_ad, parent, false)
return ADItemViewHolder(itemView)
}
override fun onBindViewHolder(holder: ADItemViewHolder, position: Int) {
holder.contentView.text = "this is AD Unit"
}
override fun isSameViewType(position: Int): Boolean {
return getData()[position] is ADWrapper
}
class ADItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val contentView = itemView.findViewById<TextView>(R.id.content_view)
}
}
//常规布局
//DefaultSubAdapter默认可以匹配任何一个position,所以不需要实现isSameViewType()方法;
//匹配规则:优先匹配其他SubAdapter,都匹配不上在用这个兜底。
//通常用于其他ViewType都有详细的匹配规则,但是自己的匹配规则不明确或繁杂(先把其他的都排除,剩下的就是自己的)
class NormalSubAdapter: DefaultSubAdaper<ValueBean, NormalSubAdapter.NormalItemViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup): NormalItemViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_multi_viewtype_layout_normal, parent, false)
return NormalItemViewHolder(itemView)
}
override fun onBindViewHolder(holder: NormalItemViewHolder, position: Int) {
holder.contentView.text = "this is normal ${getData()[position].content} data"
}
class NormalItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val contentView = itemView.findViewById<TextView>(R.id.content_view)
}
}
//模拟列表数据
private fun genListData(): List<ValueBean>{
val result = arrayListOf<ValueBean>()
result.add(HeaderWrapper())
result.add(ValueBean("aaa"))
result.add(ValueBean("bbb"))
result.add(ValueBean("ccc"))
result.add(ADWrapper())
result.add(ValueBean("ddd"))
result.add(ValueBean("eee"))
result.add(ValueBean("fff"))
result.add(ValueBean("ggg"))
result.add(ValueBean("hhh"))
result.add(FooterWrapper())
return result
}
}
效果如果下:
可以看到示例中SubAdapter子类中的方法和RecyclerView.Adapter中并不是严格的一一对应(缺少getItemViewType()方法):
所以在SubAdapter中只需要实现 isSameViewType() 方法就足够啦。
然而在某些情况下,isSameViewType() 的判断条件并不太好确定,如上述代码示例中,特殊item(header、footer、广告)都有简单的判断条件,反而主要item类型没有明确的判断方法。
这种情况可以继承 DefaultSubAdaper类(继承SubAdapter,它的 isSameViewType() 返回值恒为true);当某个position其他SubAdapter都匹配不上时,则最后使用它来兜底。
使用configSpanSizeLookup()可以简化GridLayoutManager spanSize设置,同时继承SubAdapter时复写getSpanSize()指定item的spansize。
其他建议:
-
MultiItemTypeAdapter只应用在列表viewType较多,后续可能还会增加的场景。
-
list列表中的数据都要求数据类型一致;一些特殊Item可以扩展主Item的数据类型(例如示例中的HeaderWrapper、ADWrapper等类)。 这样可以便于统一管理,如果特殊item还需要携带数据,可以在xxxWrapper类中增加data字段携带数据.
-
如果不想列表数据保持类型一致,即list<Object>列表,创建adapter时指定Object类型即可(例如:MultiItemTypeAdapter<Any>())。
好啦 经过简单的封装,Adapte类膨胀的问题得到解决,同时也尽量保留了常规方式代码编写流程。
源码代码量较少,感兴趣可以了解:
MultiItemTypeAdapter: github.com/High-Power-…
SubAdapter: github.com/High-Power-…
DefaultSubAdaper: github.com/High-Power-…
写完博客后才发现已经有类似的封装库 参考:github.com/drakeet/Mul…
如果内容有错误、某些场景下使用不便,或者优化建议,欢迎在评论中指出!!!