diffadapter

Introduction: A high-performance , easy-to-use Adapter for RecyclerView ,using diffutil
More: Author   ReportBugs   
Tags:

一款针对 RecyclerView 高效刷新,多类型列表,异步数据更新,崩溃等各种复杂难处理场景的高性能易用的列表库

Demo

diffadapter.apk

图像 url,名称,价格都是异步或者通知变化的数据

image

Introduce

如何实现一个高效、高性能的、异步数据实时刷新的列表

diffadapter 就是根据实际项目中各种复杂的列表需求,同时为了解决 DiffUtil 使用不方便,容易出错而实现的一个高效,高性能的列表库 ,侵入性低,方便接入,致力于将列表需求的开发精力用于具体的 Item Holder 上,而不用花时间在一些能通用的和业务无关的地方。 使用 DiffUtil 作为来做最小更新,屏蔽外部调用DiffUtil的接口。无需自行实现 DiffUtil,只用实现简单的数据接口和展示数据的 Holder, 不用自己去实现 Adapter 来管理数据和 Holder 之间的关系,不用考虑 DiffUtil 的实现细节,就能快速的开发出一个高性能的复杂列表需求。

Feature

  • 无需自己实现 Adapter,简单配置就可实现没有各种 if-else 判断类型的多 Type 视图列表
  • 使用 DiffUtil 来找出最小需要更新的 Item 集合,使用者无需做任何 DiffUtil 的配置即可实现高效的列表
  • 提供方便,稳定的更新、删、插入、查询方法,适用于各种非常频繁,复杂的场景(如因为异步或通知的原因同时出现插入,删除,全量设置的情况)
  • 更友好方便的异步数据更新方案

Using

基本用法

Step 1:继承BaseMutableData,主要实现areUISame(newData: AnyViewData)uniqueItemFeature()

class AnyViewData(var id : Long ,var any : String) : BaseMutableData<AnyViewData>() {

    companion object {
         //数据展示的 layout,也是和 Holder 一一对应的唯一特征
         const val VIEW_ID = R.layout.holder_skins
    }

    override fun getItemViewId(): Int {

        return VIEW_ID
    }

    override fun areUISame(newData: AnyViewData): Boolean {
         // 判断新旧数据是否展示相同的 UI,如果返回 True,则表示 UI 不需要改变,不会 updateItem,可以理解为
        如果新数据 newData 是什么样才不需要更新 UI

        return this.any == newData.any
    }

    override fun uniqueItemFeature(): Any {
        // 返回可以标识这个 Item 的特征,比如 uid,id 等,用来做动态更改单个 item 查找的条件
        return this.id
    }

}

Step 2:继承BaseDiffViewHolder<T extends BaseMutableData>,泛型类型传入上面定义的AnyViewData

class AnyHolder(itemView: View, recyclerAdapter: DiffAdapter): BaseDiffViewHolder<AnyViewData>( itemView,  recyclerAdapter){

    override fun getItemViewId(): Int {
        return AnyViewData.VIEW_ID
    }


    override fun updateItem(data: AnyViewData, position: Int) {
        根据 AnyViewData.VIEW_ID 对应的 layout 来更新 Item
        Log.d(TAG,"updateItem $data")
    }
}

Step 3:注册,显示到界面

val diffAdapter = DiffAdapter(this)

//注册类型,不分先后顺序
diffAdapter.registerHolder(AnyHolder::class.java, AnyViewData.VIEW_ID)
diffAdapter.registerHolder(AnyHolder2::class.java, AnyViewData2.VIEW_ID)
diffAdapter.registerHolder(AnyHolder3::class.java, AnyViewData3.VIEW_ID)

val linearLayoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = linearLayoutManager
recyclerView.adapter = diffAdapter

//监听数据变化

fun onFetchedData(datas : List<BaseMutableData<*>>) {
    diffAdapter.datas = adapterListData
}

只需要上面几步,就可以完成如类似下图的多 type 列表,其中数据源里的每个 BaseMutableData 的 getItemViewId()决定着用哪个 Holder 展示 UI。 (以上均用kotlin实现,Java使用不受任何限制)

增、插入、删除、修改(更新)

public <T extends BaseMutableData> void addData(T data) 

public void deleteData(BaseMutableData data)

public void deleteData(int startPosition, int size)

void insertData(int startPosition ,List<? extends BaseMutableData> datas)

public void updateData(BaseMutableData newData)

上述接口在调用的时机,频率都很复杂的场景下也不会引起崩溃

使用 updateData(BaseMutableData newData)时,newData 可以是新 new 的对象,也可以是修改后的原对象,不会出现使用 DiffUtil 更新单个数据无效 的问题

基本上就提供了上述很少的几个接口,主要是为了功能更清晰,侵入性更低,你可以根据自己的需要组合更多的功能,像下拉刷新,动画等。

高阶用法

基本用法中Data 和 Holder 绑定的模式并没什么特殊之处,早在两年前的项目KnowWeather就已经用上这种思想,现在只是结合 DiffUtil 以及其他的疑难问题解决方案将其开源,diffadapter 最核心的地方在于高性能和异步获取数据或者通知数据变化时列表的更新上

多数据源异步更新

以一个类似的 Item 为例,这里认为服务器返回的数据列表只包含 uid,也就是List<Long> uids,个人资料,等级,贵族等都属于不同的协议。下面展示的是异步获取个人资料展示的头像和昵称的情况,其他的可以类比。

Step 1:定义 ViewData

data class ItemViewData(var uid:Long, var userInfo: UserInfo?, var anyOtherData: Any ...) : BaseMutableData<ItemViewData>() {

    companion object {
        const val VIEW_ID = R.layout....
    }

    override fun getItemViewId(): Int {
        return VIEW_ID
    }

    override fun areUISame(newData: UserInfo): Boolean {
        return this.userInfo?.portrait == newData.userInfo?.portrait && this.userInfo?.nickName == newData.userInfo?.nickName && this.anyOtherData == newData.anyOtherData
    }

    override fun uniqueItemFeature(): Any {
       return this.uid
    }

}

数据类 ItemViewData 包含所有需要显示到 Item 上的信息,这里只处理和个人资料相关的数据,anyOtherData: Any ...表示 Item 所需的其他数据内容

BaseMutableData 里有个默认的方法allMatchFeatures(@NonNull Set<Object> allMatchFeatures),不需要显示调用,这里当外部有异步数据变化时,提供当前 BaseMutableData 用来匹配变化的异步数据的对象

public void appendMatchFeature(@NonNull Set<Object> allMatchFeatures) {
    allMatchFeatures.add(uniqueItemFeature());
}

默认添加了 uniqueItemFeature(),allMatchFeatures 是个 Set,可以重写方法添加多个用来匹配的特征。

Step 2:定义 View Holder,同基本用法

Step 3:监听数据变化,更新列表

//用于监听请求的异步数据,userInfoData 变化时与此相关的数据
private val userInfoData = MutableLiveData<UserInfo>()

//在 adapter 里监听数据变化
diffAdapter.addUpdateMediator(userInfoData, object : UpdatePayloadFunction<UserInfo, ItemViewData> {
    override fun providerMatchFeature(input: UserInfo): Any {
        return input.uid
    }

    override fun applyChange(input: UserInfo, originalData: ItemViewData,, payloadKeys: MutableSet<String>): ItemViewData {

       return originalData.userInfo = input

    }
})

// 任何通知数据获取到的通知
fun asyncDataFetch(userInfo : UserInfo) {
    userInfoData.value = userInfo
}

这样当 asyncDataFetch 接收到数据变化的通知的时候,改变 userInfoData 的值,adapter 里对应的 Item 就会更新。其中找到 adapter 中需要更新的 Item 是关键部分,主要由实现UpdatePayloadFunction来完成,实现UpdatePayloadFunction也很简单。

public abstract class UpdatePayloadFunction<I, R extends BaseMutableData> implements UpdateFunction<I,R > {

    /**
     * 匹配所有数据,及返回类型为 R 的所有数据
     */
    Object MATCH_ALL = new Object();

    /**
     * 提供一个特征,用来查找列表数据中和此特征相同的数据
     * @param input 用来提供查找数据和最终改变列表的数据 ,最终匹配的是 allMatchFeatures 里的数据,默认情况下就是 uniqueItemFeature()
     * @return 用来查找列表中的数据的特征项
     */
    Object providerMatchFeature(@NonNull I input);

    /**
     * 匹配到对应的数据,如果符合条件的数据有很多个,可能会被回调多次,不需要新建对象,主需要根据 Input 把 originalData 改变相应的值就行了
     * @param input 是数据改变的部分数据源
     * @param originalData 需要改变的数据项
     * @param payloadKeys 用来标识改变后的数据哪些部分发生了改变,if payloadKeys is not empty  ,
     * {@link com.silencedut.diffadapter.holder.BaseDiffViewHolder#updatePartWithPayload(BaseMutableData, Bundle, int)}
     *                    will be call rather than
     * {@link com.silencedut.diffadapter.holder.BaseDiffViewHolder#updateItem(BaseMutableData, int)}
     * @return 改变后的数据项,
     *
     */

     public abstract R applyChange(@NonNull I input, @NonNull R originalData, @NonNull Set<String> payloadKeys);

}

UpdatePayloadFunction用来提供异步数据获取到后数据用来和列表中的数据匹配的规则和根据规则找到需要更改的对象后如果改变原对象,剩下的更新都由diffadapter来处理。如果符合条件的数据有很多个,applyChange(@NonNull I input, @NonNull R originalData, @NonNull Set<String> payloadKeys)会被回调多次。 如下时:

Object providerMatchFeature(@NonNull I input) {
    return UpdateFunction.MATCH_ALL
}

applyChange回调的次数就和列表中的数据量一样多。

如果同一种匹配规则providerMatchFeature对应多种 Holder 类型,UpdateFunction<I,R>的返回数据类型 R 就可以直接设为基类的BaseMutableData,然后再 applyChange 里在具体根据类型来处理不同的 UI。

UpdateFunction已废弃,payloadKeys可以用来解决 payload 方式更新 item 时每次需要 new 对象的问题。

最高效的 Item 局部更新方式 —— payload

DiffUtil 能让一个列表中只更新部分数据变化的 Item,payload 能让同一个 Item 只更新需要变化的 View,这种方式非常适合同一个 Item 有多个异步数据源的,同时又对性能有更高要求的列表。看具体更新需求来判断是否有必要。

有两种情况的局部更新

第一种是全量数据对比的情况,也就是同一个业务可能会多次调用diffadapter.setData(List),可使用如下的方式

Step 1:重写 BaseMutableData 的 appendDiffPayload

data class ItemViewData(var uid:Long, var userInfo: UserInfo?, var anyOtherData: Any ...) : BaseMutableData<ItemViewData>() {

    companion object {
        const val KEY_BASE_INFO = "KEY_BASE_INFO"
        const val KEY_ANY = "KEY_ANY"
    }

    ...

    /**
     * 最高效的更新方式,如果不是全量频繁更新的可以不实现这个方法
     */
     override fun appendPayloadKeys(newData: LegendViewData, payloadKeys: MutableSet<String>) {
        super.appendPayloadKeys(newData, payloadKeys)
         if(this.userInfo!= newData.userInfo) {
            payloadKeys.add(KEY_BASE_INFO)
        }
        if(this.anyData != newData.anyData) {
                 payloadKeys.add(KEY_ANY)

        }
        ...
    }

}

默认用 Bundle 存取变化,无需存具体的数据,只需类似设置标志位,表明 Item 的哪部分数据发生了变化。

第二种是异步动态更新一个 Item 的时候,比如个人资料获取,中途单个 Item 数据变化的情况,可使用如下的 Step1

public R applyChange(@NonNull I input, @NonNull R originalData, @NonNull Set<String> payloadKeys){
    ...
    originalData.*** = input.***                     
    payloadKeys.add("自定义 String 类型的 Key 值")
    ...
}

这两种方式不是互斥的,但也没什么关联,也可以根据自己的业务场景自行选择,如果不需要 payload 更新,两种方式都不需要。后续的步骤两种方式相同。

Step 2 :需要重写 BaseDiffViewHolder 里的updatePartWithPayload

class ItemViewHolder(itemViewRoot: View, recyclerAdapter: DiffAdapter): BaseDiffViewHolder<ItemViewData>( itemViewRoot,  recyclerAdapter){

    override fun updatePartWithPayload(data: ItemViewData, payloadKeys: MutableSet<String>, position: Int) {

    if(payloadKeys.contains(ItemViewData.KEY_BASE_INFO)) {
        updateBaseInfo(data)
    }

    if(payloadKeys.contains(ItemViewData.KEY_ANY)) {
        updateAnyView(data)
    }
}

Step 3:监听数据变化,更新列表,这个只是异步数据更新 Item 需要也就是第二种场景,如果每次diffadapter.setData(List)的数据已经是是有所有的数据信息,不需要以下的动态更新方案

//用于监听请求的异步数据,userInfoData 变化时与此相关的数据
private val userInfoData = MutableLiveData<UserInfo>()

//在 adapter 里监听数据变化
diffAdapter.addUpdateMediator(userInfoData, object : UpdateFunction<UserInfo, ItemViewData> {
    override fun providerMatchFeature(input: UserInfo): Any {
        return input.uid
    }

    override fun applyChange(@NonNull I input, @NonNull R originalData, @NonNull Set<String> payloadKeys): ItemViewData {
       //不再需要新建对象
       originalData.*** = input.***                     
       payloadKeys.add("自定义 String 类型的 Key 值")
       return originalData

    }
})

// 任何通知数据获取到的通知
fun asyncDataFetch(userInfo : UserInfo) {
    userInfoData.value = userInfo
}

More

一些探讨:

  1. 为什么没有提供类似 onItemClickLisener 用来处理点击事件的接口

    不是因为不好实现,其实现实起来非常简单。首先尝试去理解为什么 RecyclerView.Adapter 没有提供像 listview 那样的点击事件的 listener,我的理解是大而全的公用点击监听不是一个好的设计方式,尤其对于多类型的 view 来说,因为点击的是不同的 holder,要在回调里根据类型来处理不同的逻辑,少不了各种if-else的代码块,不同 holder 相关的数据,逻辑耦合到一块,试想如果有四五种类型,处理统一点击回调的地方是多大的一块代码,后期的维护又是一个问题。我认为好的方式应该是在各自的 holder 的构造函数里来各自处理,每个 holder 都有自己的数据和类型,很好的隔离开不同类型数据的耦合,每个 holder 各司其职:显示数据,监听点击,维护方便。

  2. 为什么没有下拉刷新、加载更多、动画、分割线等更多的功能

    首先 diffadapter 主要就是为了提供高性能刷新,异步数据更新,高效的配置多类型列表的功能,这也是绝大多数列表最常见的功能,像上面说的那些功能以及 onItemClickLisener 都是一些额外的添加项,不想做一个为了看起来更多功能但没有任何难度,堆积代码的开源库,不想为了看起来大而全来吸引别人使用。就是职责很单一,目的很明确,diffadapter 侵入性很低,不影响任何其他功能的引入,包括不限于上面提到的那些。而且上面提到的那些都有很多很好的开源库,你可以根据任何自己的需要来定制。

更详细,多样的使用方式和细节见diffadapter demo,有详细的 demo 和使用说明,demo 用 kotlin 实现,使用了mvvm模块化的框架方式。

这种方式也是目前能想到的比较好的异步数据更新列表的方式,非常欢迎一起探讨更多的实现方式。

引入

Step1.Add it in your root add build.gradle at the end of repositories:

allprojects {
    repositories {
        ..
        maven { url 'https://jitpack.io' }
    }
}

Step2. Add the dependency:

dependencies {
    implementation 'com.github.silencedut:diffadapter:latestVersion'
}

ProGuard

-keep class * extends com.silencedut.diffadapter.holder.BaseDiffViewHolder {*;}
-keep class * extends com.silencedut.diffadapter.data.BaseMutableData {*;}

License

Copyright 2017-2018 SilenceDut

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools