android开发技巧Android进阶之路Android应用开发那些事

后现代化RecyclerView Adapter稳定版本终于来了

2020-08-02  本文已影响0人  i校长

背景

相信大家都已经在使用kotlin了,可我们使用最频繁的Adapter缺很少有人用kotlin做扩展,即使有,但给我的感觉还是不够,第一不够简洁,第二功能耦合在一起,第三不够完善,于是我决定自己做一个,经过这段时间的研究,前面也写了三篇博客了,都是我这段时间的劳动成果,可之前的设计还是会有一些不好的地方,也是经过几次的验证后,目前有了稳定版,对于这个版本我还是比较满意的,下面有请我厚脸皮给你们讲一讲

源码地址

https://github.com/ibaozi-cn/RecyclerViewAdapter

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扩展类图

image

上面的内容我大致描述一下

这么好的框架如何使用呢?

源码中提供全面的例子,Demo目录如图哦

image

一些效果图

image image image image

下载源码,跑一下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?还是以前的例子,来看下那个截图

image

看到没,这里面就有一个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 = "替换副标题"
                })
            }
        }
    }
}

未来更多的规划

这么全面的Adapter你见过几个?还不动动小手关注一哈,嘿嘿,谢谢🙏

总结

我这期针对稳定版本,写的不是很多,主要就是为了让你们知道如何使用,以及一些源码的展示,其实我们在做开发的同时,真的会遇到各种各样的列表,当然它不能覆盖业务中的各个场景,但我希望能在某些实现的角度上能让你收益,用更加合理的实现方式来解决业务中各种各样的复杂场景。

感谢

https://github.com/mikepenz/FastAdapter

https://github.com/DevAhamed/MultiViewAdapter

https://github.com/davideas/FlexibleAdapter

https://github.com/liangjingkanji/BRV

https://github.com/h6ah4i/android-advancedrecyclerview

https://github.com/evant/binding-collection-adapter

特别感谢这些优秀设计者的项目,是他们的经验积累,让我有了更多的想法和实现。

开发者

i校长

上一篇 下一篇

猜你喜欢

热点阅读