背景
相信大家都已经在使用kotlin了,可我们使用最频繁的Adapter缺很少有人用kotlin做扩展,即使有,但给我的感觉还是不够,第一不够简洁,第二功能耦合在一起,第三不够完善,于是我决定自己做一个,经过这段时间的研究,前面也写了三篇博客了,都是我这段时间的劳动成果,可之前的设计还是会有一些不好的地方,也是经过几次的验证后,目前有了稳定版,对于这个版本我还是比较满意的,下面有请我厚脸皮给你们讲一讲
源码地址
Gradle依赖
allprojects { repositories { // 首先项目根目录的build.gradle文件中加入这一行 maven { url 'https://jitpack.io' } } } def adapterVersion = 'v1.2.0' //核心库 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-core:$adapterVersion" //下面都是可选项 //anko layout 扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-anko:$adapterVersion" //diffutil 扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-diff:$adapterVersion" //data binding扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-binding:$adapterVersion" // paging3 扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-paging:$adapterVersion" // sortedlist 扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-sorted:$adapterVersion" // flexbox 扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-flex:$adapterVersion" // UI 扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-ui:$adapterVersion" // Selectable 扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-selectable:$adapterVersion" // Expandable 扩展 implementation "com.github.ibaozi-cn.RecyclerViewAdapter:adapter-expandable:$adapterVersion"
当前版本库大小
名字 | release aar size | 其他 |
Core | 28kb | 核心库目前包含ListAdapter的实现,最基础且最实用的扩展 |
Anko | 13kb | 适用本项目所有Adapter扩展 |
DataBinding | 20kb | 适配DataBinding布局,适用本项目所有Adapter扩展 |
Sorted | 10kb | SortedListAdapter扩展实现 |
Paging | 13kb | PagingListAdapter扩展适配 |
Diff | 6kb | 适配DiffUtil,目前适用ListAdapter |
FlexBox | 9kb | 适配FlexBox布局 |
Selectable | 8kb | 动态扩展单选、多选、最大可选项功能 |
Expandable | 8kb | 动态扩展可展开功能,支持仅单展开或多展开配置 |
UI | 17kb | 扩展空布局 |
对Adapter扩展类图
上面的内容我大致描述一下
- IAdapter 最底层的抽象接口
- ViewHolderCacheAdapter 统一处理ViewHolder的缓存和ViewModel的回调
- ListAdapter 扩展ViewHolderCacheAdapter,实现对ArrayList数据结构的处理
- SortedListAdapter 扩展ViewHolderCacheAdapter,实现对SortedList数据结构的处理
- PagingListAdapter 扩展ViewHolderCacheAdapter,实现对AsyncPagingDataDiffer的处理
- IAdapter Expandable 动态扩展 这里用的Kotlin的扩展函数实现,类似组合继承,解耦方便
- IAdapter Selectable 动态扩展 同上
- ViewModel 这个是对Adapter每一个Item的高度抽象,负责配置Model数据,负责获取ViewHolder的实例,缓存itemViewType等
- DefaultViewModel 负责创建DefaultViewHolder,并提供ViewHolder初始化InitView的回调,这个很关键,这是我们可以直接在ViewModelDSl中findView的关键
- LayoutViewModel 扩展自DefaultViewModel,实现LayoutInflater加载View
- AnkoViewModel 扩展自DefaultViewModel,实现AnkoComponent加载View
- BindingViewModel 扩展自DefaultViewModel,实现DataBindingUtil加载View和ViewDataBinding绑定数据
- WrapAdapter 专门负责装饰老的Adapter适配器,一种很好的设计模式,比起继承实现要好太多了,EmptyAdapter就是一个很好的实现,只需要将以前的Adapter包裹一层EmptyAdapter,就可以轻松实现空布局,这里我想啰嗦一句,如果是头尾布局,不建议这么做,我们RecyclerView升级1.2.0以后就会有一个ConcatAdapter,这个适配器才是我们实现头尾布局的神器,期待ing
这么好的框架如何使用呢?
源码中提供全面的例子,Demo目录如图哦
一些效果图
下载源码,跑一下Demo就行了哦,下面教你如何快速的上手
快速上手
LayoutViewModel 例子
//xml <?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/cardItem" android:layout_margin="5dp"> <LinearLayout android:background="?attr/selectableItemBackground" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="25dp" android:orientation="horizontal"> <TextView android:id="@+id/tv_title" android:text="@string/app_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/colorPrimary" android:textSize="22sp" /> <TextView android:id="@+id/tv_subTitle" android:text="@string/test" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/colorAccent" android:layout_marginStart="10dp" android:textSize="18sp" /> </LinearLayout> </androidx.cardview.widget.CardView> // 传入布局 和 Model 数据 layoutViewModelDsl(R.layout.item_test, ModelTest("title", "subTitle")) { // 初始化 View,这里只会调用一次哦,放心初始化,放心的setOnClickListener val titleText = getView<TextView>(R.id.tv_title) val subTitleText = getView<TextView>(R.id.tv_subTitle) itemView.setOnClickListener { val vm = getViewModel<LayoutViewModel<ModelTest>>() //修改Model数据 vm?.model?.title = "测试更新${Random.nextInt(10000)}" //用Adapter更新数据 getAdapter<ListAdapter>()?.set(adapterPosition, vm) } //数据触发更新的时候,绑定新的Model数据 onBindViewHolder { val model = getModel<ModelTest>() titleText.text = model?.title subTitleText.text = model?.subTitle } }
AnkoViewModel 例子
// view class AnkoItemView : AnkoComponent<ViewGroup> { var tvTitle: TextView? = null var tvSubTitle: TextView? = null var view: View? = null @SuppressLint("ResourceType") override fun createView(ui: AnkoContext<ViewGroup>) = with(ui) { cardView { layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT ).apply { margin = dip(5) } verticalLayout { val typedValue = TypedValue() context.theme .resolveAttribute(android.R.attr.selectableItemBackground, typedValue, true) val attribute = intArrayOf(android.R.attr.selectableItemBackground) val typedArray = context.theme.obtainStyledAttributes(typedValue.resourceId, attribute) background = typedArray.getDrawable(0) layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT ).apply { padding = dip(10) } tvTitle = textView { textSize = px2dip(60) textColorResource = R.color.colorPrimary }.lparams(matchParent, wrapContent) tvSubTitle = textView { textSize = px2dip(45) textColorResource = R.color.colorAccent }.lparams(matchParent, wrapContent) } } } } // 传入Model和AnkoView对象 ankoViewModelDsl(ModelTest("title", "ankoViewModelDsl"), { AnkoItemView() }) { //数据更新 onBindViewHolder { _ -> val model = getModel<ModelTest>() val ankoView = getAnkoView<AnkoItemView>() ankoView?.tvTitle?.text = model?.title ankoView?.tvSubTitle?.text = model?.subTitle } //点击事件处理 itemView.setOnClickListener { val viewModel = getViewModel<AnkoViewModel<ModelTest, AnkoItemView>>() viewModel?.model?.title = "点击更新${Random.nextInt(10000)}" getAdapter<ListAdapter>()?.set(adapterPosition, viewModel) } }
与LayoutViewModel的不同就在于无需在DSL中初始化View,因为已经在AnkoView中做了缓存,它唯一的优势就是比LayoutViewModel更快的加载速度,但Anko Layout已经不维护了,你是不是不敢用了呢?在我看来,问题不大,因为我可以自定义AnkoView,自己来做扩展,性能的提升远大于代码的数量,你说呢?
BindingViewModel 例子
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" > <data> <variable name="model" type="com.julive.adapter_demo.sorted.ModelTest" /> </data> <androidx.cardview.widget.CardView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/cardItem" android:layout_margin="5dp"> <LinearLayout android:background="?attr/selectableItemBackground" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="25dp" android:orientation="horizontal"> <TextView android:id="@+id/tv_title" android:text="@{model.title}" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/colorPrimary" android:textSize="22sp" /> <TextView android:id="@+id/tv_subTitle" android:text="@{model.subTitle}" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/colorAccent" android:layout_marginStart="10dp" android:textSize="18sp" /> </LinearLayout> </androidx.cardview.widget.CardView> </layout> //传入layout、BR、Model bindingViewModelDsl(R.layout.item_binding_layout, BR.model, ModelTest("title", "bindingViewModelDsl")) { //设置点击事件 itemView.setOnClickListener { val viewModel = getViewModel<BindingViewModel<ModelTest>>() viewModel?.model?.title = "${java.util.Random().nextInt(100)}" getAdapter<ListAdapter>()?.set(adapterPosition, viewModel) } }
没有findView,没有onBindViewHolder,代码缩减了很多,如果你追求的就是高效率,请使用它,准没错,三种加载ItemView的方式就完了
如何加载到Adapter中呢
listAdapter { addAll(createViewModelList(3)) addAll(createAnkoViewModelList(3)) addAll(createBindingViewModelList(3)) // 绑定 RecyclerView into(rv_list_dsl) } fun createViewModelList(max: Int = 10) = (0..max).map { _ -> layoutViewModelDsl(R.layout.item_test, ModelTest("title", "subTitle")) { val titleText = getView<TextView>(R.id.tv_title) val subTitleText = getView<TextView>(R.id.tv_subTitle) itemView.setOnClickListener { val vm = getViewModel<LayoutViewModel<ModelTest>>() //修改Model数据 vm?.model?.title = "测试更新${Random.nextInt(10000)}" //用Adapter更新数据 getAdapter<ListAdapter>()?.set(adapterPosition, vm) } onBindViewHolder { val model = getModel<ModelTest>() titleText.text = model?.title subTitleText.text = model?.subTitle } } } 省略Anko、Binding...
如何实现一个Selectable呢
class SelectableActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportActionBar?.title = "ListAdapter" setContentView(R.layout.activity_selectable) //同样是ListAdapter val adapter = listAdapter { //添加一堆ViewModel数据 (0..10).forEach { _ -> add( layoutViewModelDsl( R.layout.item_test, ModelTest("title", "subTitle") ) { //初始化View val title = getView<TextView>(R.id.tv_title) val subTitle = getView<TextView>(R.id.tv_subTitle) //设置监听 itemView.setOnClickListener { //改变选择状态 toggleSelection(adapterPosition) { if (it) { longToast("可选项已达到最大值") } } Log.d("isMultiSelectable", "isMultiSelectable$isMultiSelect") } onBindViewHolder { val model = getModel<ModelTest>() title.text = model?.title subTitle.text = model?.subTitle // 获取选择状态,来适配不同UI val isSelect = isSelected(adapterPosition) if (isSelect) { itemView.setBackgroundResource(R.color.cardview_dark_background) title.textColorResource = R.color.cardview_light_background } else { itemView.setBackgroundResource(R.color.cardview_light_background) title.textColorResource = R.color.cardview_dark_background } } }) } into(rv_list_selectable) } btn_left.setText("切换单选").setOnClickListener { // 多选和单选之间切换 if (!adapter.isMultiSelect) { btn_left.setText("切换单选") } else { btn_left.setText("切换多选") } adapter.setMultiSelectable(!adapter.isMultiSelect) } btn_middle.isVisible = false btn_right.setText("设置最大可选").setOnClickListener { //配置最大的可选择项 val random = Random().nextInt(6) btn_right.setText("设置最大可选$random") adapter.setSelectableMaxSize(random) } } }
有没有前所未有简单呢?这就是Kotlin动态扩展函数带来的便利,下面请看下实现的源码:
//根据列表实例缓存已选择项,只缓存选中的,未选中的会被清理掉节省内存,用弱引用来提高内存回收率 private val selectedItemsCache = SparseArray<WeakReference<SparseBooleanArray?>?>() private val selectConfigCache = SparseArray<WeakReference<SparseArray<Any>?>?>() //可选项默认配置 private val defaultSelectedConfig by lazy { SparseArray<Any>().apply { append(0, true) // is Multi Selectable append(1, Int.MAX_VALUE) // Selectable Max Size Default Int.Max } } // 获取已选择列表 private fun getSelectedItems(key: Int): SparseBooleanArray { val wr = selectedItemsCache[key] val sba by lazy { SparseBooleanArray() } return if (wr == null) { selectedItemsCache.append(key, WeakReference(sba)) sba } else { val expandedItems = wr.get() if (expandedItems == null) { selectedItemsCache.append(key, WeakReference(sba)) } expandedItems ?: sba } } // 获取选择项配置信息 private fun getSelectConfig(key: Int): SparseArray<Any> { val wr = selectConfigCache[key] return if (wr == null) { selectConfigCache.append(key, WeakReference(defaultSelectedConfig)) defaultSelectedConfig } else { val expandConfig = wr.get() if (expandConfig == null) { selectConfigCache.append(key, WeakReference(defaultSelectedConfig)) } expandConfig ?: defaultSelectedConfig } } // 动态扩展IAdapter 判断当前是否多选 var IAdapter<*>.isMultiSelect get() = getSelectConfig(hashCode())[0] as Boolean private set(value) {} // 动态扩展IAdapter 获取最大可选择数 var IAdapter<*>.selectedMaxSize: Int get() = getSelectConfig(hashCode())[1] as Int private set(value) {} // 动态扩展IAdapter 获取已选择项的大小 var IAdapter<*>.selectedCount: Int get() = getSelectedItems(hashCode()).size() private set(value) {} // 动态扩展IAdapter 配置多选和单选的状态 fun IAdapter<*>.setMultiSelectable(enable: Boolean) { getSelectConfig(hashCode()).setValueAt(0, enable) if (!enable && selectedCount > 1) { clearSelection() } } // 动态扩展IAdapter 配置最大可选择数 fun IAdapter<*>.setSelectableMaxSize(size: Int) { getSelectConfig(hashCode()).setValueAt(1, size) } // 动态扩展IAdapter 获取已选择列表 fun IAdapter<*>.getSelectedItems(): List<Int> { val si = getSelectedItems(hashCode()) val itemSize = si.size() val items: MutableList<Int> = ArrayList(itemSize) for (i in 0 until itemSize) { items.add(si.keyAt(i)) } return items } // 动态扩展IAdapter 判断当前是否已选择 fun IAdapter<*>.isSelected(position: Int) = getSelectedItems().contains(position) // 动态扩展IAdapter 清空已选择项 fun IAdapter<*>.clearSelection() { val selection = getSelectedItems() getSelectedItems(hashCode()).clear() for (i in selection) { notifyItemChanged(i) } } //动态扩展IAdapter 改变选择状态 fun IAdapter<*>.toggleSelection(position: Int, isMaxSelect: ((Boolean) -> Unit)? = null) { val si = getSelectedItems(hashCode()) val isSelect = si.get(position, false) if (selectedCount >= selectedMaxSize && !isSelect) { isMaxSelect?.invoke(true) return } isMaxSelect?.invoke(false) if (!isMultiSelect) { clearSelection() } if (isSelect) { si.delete(position) } else { si.put(position, true) } notifyItemChanged(position) }
没有继承,没有组合,就是动态扩展,这样的解耦方式,是不是比以前更加的好用呢?我认为还可以,不知道你怎么想,欢迎留言。Expandable实现原理同上,就不再描述了哦
WrapAdapter的实现
为什么要有WrapAdapter?还是以前的例子,来看下那个截图
看到没,这里面就有一个EMPTY_VIEW,不光这些还有头尾布局,这样的逻辑你敢用吗?如果有了WrapAdapter是什么样子呢?
override fun getItemViewType(position: Int): Int { return if (displayEmptyView(emptyState)) { viewModel.itemViewType } else { super.getItemViewType(position) } }
就这样就行了,简单明了,这么简洁的代码你不点个赞吗?哈哈,其实这就是装饰者模式的魅力,其实它的核心就是将真实适配器的调用权交给了WrapAdapter,然后在合适的时机再调用真实的Adapter来展示数据。其实WrapAdapter的实现很简单,来看下一段代码
// 继承自RecyclerView.Adapter 可以传入一个新的Adapter open class WrapAdapter<VH : RecyclerView.ViewHolder>(private var mWrappedAdapter: RecyclerView.Adapter<VH>) : RecyclerView.Adapter<VH>() // 注册observer,实现notifyDataChange一系列相关回调 mWrappedAdapter.registerAdapterDataObserver(wrapAdapterDataObserver) // 一些关键函数的调用实现,这里没写全哦,详细还请跳入源码查看 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { return mWrappedAdapter.onCreateViewHolder(parent, viewType) } override fun getItemId(position: Int): Long { return mWrappedAdapter.getItemId(position) } override fun getItemViewType(position: Int): Int { return mWrappedAdapter.getItemViewType(position) } override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) { mWrappedAdapter.onBindViewHolder(holder, position, payloads) }
WrapAdapter这么好,空布局如何用的呢?
class EmptyActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_empty) val emptyAdapter = EmptyAdapter( listAdapter { addAll(createViewModelList()) } ).apply { into(rv_list_empty) } btn_left.setText("空布局").setOnClickListener { emptyAdapter.emptyState = EmptyState.NotLoading } btn_middle.setText("加载中").setOnClickListener { emptyAdapter.emptyState = EmptyState.Loading Handler().postDelayed({ emptyAdapter.emptyState = EmptyState.Loaded },2000) } btn_right.setText("加载失败").setOnClickListener { emptyAdapter.emptyState = EmptyState.Error } } }
也是超级简单是吧,很容易就理解了对吗?
如何使用SortedListAdapter呢?
我们先来看一段代码
/** * sortedId 排序用 * title 作为uniqueId ,RecyclerView ItemView 更新的时候,唯一值,注意列表是可以出现一样的uniqueId的, * 如果想更新请调用Adapter updateItem 这样才能保证列表中uniqueId唯一 */ data class SortedModelTest( val sortedId: Int, var title: String, var subTitle: String, override var uniqueId: String = title ) : SortedModel { override fun <T : SortedModel> compare(model: T): Int { if (sortedId > (model as? SortedModelTest)?.sortedId ?: 0) return 1 if (sortedId < (model as? SortedModelTest)?.sortedId ?: 0) return -1 return 0 } } class SortedItemViewModelTest : LayoutViewModel<SortedModelTest>(R.layout.item_test) { init { onCreateViewHolder { itemView.setOnClickListener { val vm = getAdapter<SortedListAdapter>()?.getItem(adapterPosition) as SortedItemViewModelTest vm.model?.subTitle = "刷新自己${Random.nextInt(100)}" getAdapter<SortedListAdapter>()?.set(adapterPosition, vm) } } } override fun bindVH(viewHolder: DefaultViewHolder, payloads: List<Any>) { viewHolder.getView<TextView>(R.id.tv_title).text = model?.title viewHolder.getView<TextView>(R.id.tv_subTitle).text = model?.subTitle } }
我们抽象的ViewModel是在任何Adapter中都可以做到通用的,这点你可以放心哦,SorteList我们都知道它是需要对数据进行比较的,所以我们提供了SortedModel接口,你只需要实现SortedModel接口就可以将其放入ViewModel中,然后再放入Adapter中就行了,SortedModel实现SameModel,这里是接口继承,在kotlin里面接口是可以有实现的,
interface SameModel { var uniqueId: String //是否是同一个Model fun <T : SameModel> isSameModelAs(model: T): Boolean { return this.uniqueId == model.uniqueId } //同一个Model的话,数据是否有变化 fun <T : SameModel> isContentTheSameAs(model: T): Boolean { return this == model } //局部刷新时使用 fun <T : SameModel> getChangePayload(newItem: T): Any? = null } interface SortedModel : SameModel { /** * 排序使用 */ fun <T : SortedModel> compare(model: T): Int }
由于是继承接口实现,所以侵入性不高,对于一般的业务都可以适用,你可以放心大胆的使用哦。在Activity中使用方法如下:
class SortedActivity : AppCompatActivity() { private val mSortedListAdapter by lazy { SortedListAdapter() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportActionBar?.title = "SortedListAdapter" setContentView(R.layout.activity_array_list) mSortedListAdapter.into(rv_list) // 初始化数据 (0..10).map { mSortedListAdapter.add(SortedItemViewModelTest().apply { model = SortedModelTest(it, "标题$it", "副标题$it") }) } var index = 100 btn_left.setText("新增").setOnClickListener { // 要想根据uniqueId更新数据,需要调用updateItem方法 mSortedListAdapter.add(SortedItemViewModelTest().apply { model = SortedModelTest(index++, "标题$index", "新增$index") }) } btn_middle.setText("删除").setOnClickListener { if (mSortedListAdapter.size > 0) { val randomInt = Random.nextInt(0, mSortedListAdapter.size) mSortedListAdapter.removeAt(randomInt) } } btn_right.setText("替换").setOnClickListener { // 根据uniqueId替换 如果sortId不一样就会触发排序 if (mSortedListAdapter.size > 0) { val randomInt = Random.nextInt(0, mSortedListAdapter.size) mSortedListAdapter.set(randomInt, mSortedListAdapter.getItem(randomInt).also { it as SortedItemViewModelTest it.model?.subTitle = "替换副标题" }) } } } }
未来更多的规划
- 上啦加载更多,滚动底部或头部回调、获取可见项
- 拖动处理、滑动删除
- 基础动画
- Item边距处理
- 树的展开扩展,目前展开只是支持了一层,未来实现多层展开
- StickyHeader扩展,列标题实现
- 等等吧
这么全面的Adapter你见过几个?还不动动小手关注一哈,嘿嘿,谢谢🙏
总结
我这期针对稳定版本,写的不是很多,主要就是为了让你们知道如何使用,以及一些源码的展示,其实我们在做开发的同时,真的会遇到各种各样的列表,当然它不能覆盖业务中的各个场景,但我希望能在某些实现的角度上能让你收益,用更加合理的实现方式来解决业务中各种各样的复杂场景。
感谢
特别感谢这些优秀设计者的项目,是他们的经验积累,让我有了更多的想法和实现。