实现PagedListAdapter中Item动态增删的一种方法

作者在项目中引入Jetpack的Paging库,提升了列表加载体验,但发现PagedListAdapter不支持已加载列表灵活增删。作者多方尝试,先使用数据库转存返回数据,后实现内存缓存中转,初步解决了问题,还推荐了修改的支持Paging的BaseRecyclerViewAdapterHelper。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在项目中延用了Jetpack库中的一些库。Lifecycles、LiveData、ViewModel等,再尝试了Paging库流程的官方Demo之后,这一次也引入了项目之中。列表加载的体验提高了很多。但是在这一过程中也发现了一些问题,最主要的是不同于传统的RecycleView.adapter,PagedListAdapter不支持对已加载列表的灵活增删。这个在实际的开发中是一个很麻烦的问题,不可能每次增删都从服务器重新拉取数据。浪费流量和时间,体验也不太好。网上也没有搜索到相应的解决方案。本来都打算放弃。修改现有PagedListAdapter为传统Adapter了,但是又觉得很不甘心。多方尝试后,现在完成了一种方案。可以实现目标。不完美,但是还可用。现在记录一下尝试过程和实现方式。

关于Paging

这个库是Google去年还是前年推出的一个自动下拉加载的库,是Jetpack中众多的工具之一,下拉列表加载更多是安卓开发中使用频率很高的一个功能,但是此前Google官方貌似没有实现这样功能的库,都是使用一些第三方的库实现该功能。Paging推出Alpha版的时候我就进行了尝试,相比于其他的下拉加载库感觉更加流畅,而且配合Jetpack中的LiveData、Room食用更佳。不过当时因为还是Alpha阶段也没有深入的研究,Demo也没有跑起来,而且加载过程需要使用到:DiffUtil.ItemCallback、DataSource.Factory、LivePagedListBuilder多个对象,感觉比较繁杂。最近尝试了官方的Demo后,决定尝试引入新项目中。但是在官方的Demo总并没有涉及到网络返回数据列表的增删操作,但是万万没想到的是PagedList虽然继承自AbstractList但是没有对add、remove等方法进行实现,是不允许直接增删的,如果对PagedList进行add、remove操作的话将会抛出UnsupportedOperationException,在实际需要对返回数据动态增删的时候才发现这个问题。网上也只能搜索到零星的信息,都不是较完善的解决方案。网上关于Paging的文章大多是如何加载列表,并没有涉及到这个问题。不想半途而废,没有办法只好自己想办法解决。

尝试

1.使用数据库转存返回数据

在官方Demo中的第一项,是使用Room+Network的DataSource,先从数据库中读取,取不到了再调用接口获取网络数据,并缓存到数据库中,因为感觉无法及时获得最新的后端数据,我并不喜欢这种方式,现在用户的流量都是很充足的,没有必要为节省流量而牺牲数据的及时性。所以开始我并没有使用这种方式,而是直接从后端获得数据后进行展示。但是我发现使用这种方式的话,对数据库中Item的增删马上就会在RecycleView中体现出来,而不需要重新完全加载一遍数据。我便决定使用数据库进行转存,每次请求返回的数据马上存储到数据库中,需要对列表数据进行增删的时候,我就操作数据库,使RecycleView马上得到刷新。而且我使用的ObjectBox数据库也支持LiveData(安利一下我一直都在使用的这个数据库ObjectBox,NoSQL类型,greenDAO团队的作品,号称比SQLite快很多,使用也很方便,不用写SQL语句,还支持RxJava、LiveData等等,桌面环境也可以使用,我的api-debugger就是用的这个数据库。),为了实现这个目的根据官方Demo代码修改实现了WithDBByPageKeyRepository如下:

/**
 * 使用数据库缓存数据,现在用不到,这个类也还没有完善
 * Repository implementation that returns a Listing that loads data directly from network by using
 * the previous / next page keys returned in the query.
 */
class WithDBByPageKeyRepository<RESULT>(private val requestCallback: (requestMap: RequestMap,
    callback: (List<RESULT>) -> Unit, requestState: MutableLiveData<RequestState>) -> Unit):LifecycleObserver {

    fun setLifeOwner(lifecycleOwner: LifecycleOwner) {
        lifecycleOwner.lifecycle.addObserver(this)
    }
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun clear() {
        box.removeAll()
    }
    var currentPageIndex: String? = INIT_PAGE_INDEX
    lateinit var box :Box<RESULT>
    @MainThread
    fun getData(clazz: Class<RESULT> ,queryCallBack:(QueryBuilder<RESULT>)->QueryBuilder<RESULT>,pageSize: Int = DEFAULT_PAGE_SEZE): Listing<RESULT> {
        val netSourceFactory = object : DataSource.Factory<String, RESULT>() {
            val sourceLiveData = MutableLiveData<CommonPageKeyedDataSource<RESULT>>()
            override fun create(): DataSource<String, RESULT> {
                val source = CommonPageKeyedDataSource<RESULT>(requestCallback, pageSize)
                sourceLiveData.postValue(source)
                return source
            }
        }
        netSourceFactory.create()
        box = OB.get().boxFor(clazz)
        val query = queryCallBack.invoke(box.query()).build()
        val dbSourceFactory = ObjectBoxDataSource.Factory<RESULT>(query)
        val netLoadInitialCallback = object : PageKeyedDataSource.LoadInitialCallback<String, RESULT>() {
            override fun onResult(data: MutableList<RESULT>, position: Int, totalCount: Int, previousPageKey: String?, nextPageKey: String?) {
                box.store.runInTx {
                    data.forEachReversedByIndex {
                        box.put(it)
                    }
                }
                currentPageIndex = nextPageKey
            }
            override fun onResult(data: MutableList<RESULT>, previousPageKey: String?, nextPageKey: String?) {
                box.store.runInTx {
                    data.forEachReversedByIndex {
                        box.put(it)
                    }
                }
                currentPageIndex = nextPageKey
            }
        }
        val livePagedList = LivePagedListBuilder(dbSourceFactory, PagedList.Config.Builder()
            .setPageSize(pageSize)
            .setInitialLoadSizeHint(pageSize)
            .build())
            .setBoundaryCallback(
                object : PagedList.BoundaryCallback<RESULT>() {
                    override fun onZeroItemsLoaded() {
                        netSourceFactory.sourceLiveData.value?.loadInitial(PageKeyedDataSource.LoadInitialParams(pageSize, true), netLoadInitialCallback)
                        LogUtils.v(TAG, "Database returned 0 items. We should query the backend for more items.")
                    }
                    override fun onItemAtEndLoaded(itemAtEnd: RESULT) {
                        LogUtils.v(TAG, " User reached to the end of the list.")
                        if (currentPageIndex!=null) {
                            netSourceFactory.sourceLiveData.value?.loadAfter(PageKeyedDataSource.LoadParams<String>(currentPageIndex, pageSize), object : PageKeyedDataSource
                            .LoadCallback<String, RESULT>() {
                                override fun onResult(data: MutableList<RESULT>, adjacentPageKey: String?) {
                                    box.store.runInTx {
                                        data.forEachReversedByIndex {
                                            box.put(it)
                                        }
                                    }
                                    currentPageIndex = adjacentPageKey
                                }
                            })
                        }
                    }
                }
            )
            .build()

        val refreshState = Transformations.switchMap(netSourceFactory.sourceLiveData) {
            it.initialLoad
        }
        return Listing(
            pagedList = livePagedList,
            requestState = Transformations.switchMap(netSourceFactory.sourceLiveData) {
                it.networkState
            },
            refresh = {
                box.removeAll()
                netSourceFactory.sourceLiveData.value?.invalidate()
            },
            refreshState = refreshState,
            box = this.box
        )
    }

    val TAG = "WithDBByPageKeyRepository"
}


目的是勉强可以达到,但是在使用存在一些比较棘手的问题,比如在当前页退出时如何只清空数据库和该页面相关的数据的问题,如果不清空不能保证数据的时效性,如果清空需要针对当前页面在数据插入时,同时插入当前页面的相关识别信息,未进行深入的测试,也害怕频繁的数据库插入删除在效率方面会出现问题,遂放弃。

2.实现内存缓存中转

上面的方法失败后,又几乎想要放弃,但是忽然想到一个问题,数据库的DataSource是如何实现增删一条数据RecycleView就马上刷新的呢?抱着这个问题找到了ObjectBox的这个类:

public class ObjectBoxDataSource<T> extends PositionalDataSource<T> {
    private final Query<T> query;
    private final DataObserver<List<T>> observer;

    public ObjectBoxDataSource(Query<T> query) {
        this.query = query;
        this.observer = new DataObserver<List<T>>() {
            public void onData(@NonNull List<T> data) {
                ObjectBoxDataSource.this.invalidate();
            }
        };
        query.subscribe().onlyChanges().weak().observer(this.observer);
    }

    public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<T> callback) {
        int totalCount = (int)this.query.count();
        if (totalCount == 0) {
            callback.onResult(Collections.emptyList(), 0, 0);
        } else {
            int position = computeInitialLoadPosition(params, totalCount);
            int loadSize = computeInitialLoadSize(params, position, totalCount);
            List<T> list = this.loadRange(position, loadSize);
            if (list.size() == loadSize) {
                callback.onResult(list, position, totalCount);
            } else {
                this.invalidate();
            }

        }
    }

    public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<T> callback) {
        callback.onResult(this.loadRange(params.startPosition, params.loadSize));
    }

    private List<T> loadRange(int startPosition, int loadCount) {
        return this.query.find((long)startPosition, (long)loadCount);
    }

    public static class Factory<Item> extends android.arch.paging.DataSource.Factory<Integer, Item> {
        private final Query<Item> query;

        public Factory(Query<Item> query) {
            this.query = query;
        }

        public DataSource<Integer, Item> create() {
            return new ObjectBoxDataSource(this.query);
        }
    }
}

感觉就是对数据库的数据增删操作进行观察,一旦有修改立马刷新数据,因为DiffUtil.ItemCallback的使用Adapter会对新旧数据进行对比,排除掉相同数据的刷新操作,所以RecycleView能够及时的刷新单个Item的增删,到了这里后面的就不难了替换上面的Query为我自己的数据源就好了。

最终实现
  1. 可观察的List缓存类ArrayListWrap:

    /**
     * @describe 缓存包装
     * @author  XQ Yang
     * @date 5/21/2019  6:47 PM
     */
    class ArrayListWrap<T> {
        fun subscribeActual(observer: DataObserver<List<T>>) {
            observers.add(WeakReference(observer))
        }
    
        fun count(): Int {
            return cacheList.size
        }
    
        fun subList(startPosition: Int, loadCount: Int): List<T> {
            var start = if (startPosition < 0) 0 else startPosition
            var end = Math.max(startPosition + loadCount, cacheList.size - 1)
            return cacheList.subList(start, end)
        }
    
        fun clear() {
            cacheList.clear()
            notifyChanged()
        }
    
        fun notifyChanged() {
            observers.forEach { it.get()?.onData(cacheList) }
        }
    
        fun remove(it: T) {
            cacheList.remove(it)
            notifyChanged()
        }
    
        fun add2(index:Int = cacheList.size,data: T) {
            cacheList.add(index,data)
            notifyChanged()
        }
    
        fun add(data: T) {
            cacheList.add(data)
            notifyChanged()
        }
    
        fun add(data: Collection<T>) {
            if (!data.isNullOrEmpty()) {
                cacheList.addAll(data)
                notifyChanged()
            }
        }
    
        fun add2(index: Int, data: Collection<T>) {
            if (!data.isNullOrEmpty()) {
                cacheList.addAll(index,data)
                notifyChanged()
            }
        }
    
        fun set(index: Int, data: T) {
            cacheList[index] = data
            notifyChanged()
        }
    
        fun setNewData(data: Collection<T>?) {
            cacheList.clear()
            if (data!=null) {
                cacheList.addAll(data)
            }
            notifyChanged()
        }
    
        val cacheList: ArrayList<T> = ArrayList<T>()
        val observers = mutableListOf<WeakReference<DataObserver<List<T>>>>()
    }
    

    实现数据缓存中转。

  2. 使用缓存中转的DataSource类MemPositionalDataSource:

    class MemPositionalDataSource<T>(  val cacheList :ArrayListWrap<T> = ArrayListWrap<T>()) : PositionalDataSource<T>() {
        private val observer: DataObserver<List<T>>
        init {
            this.observer = DataObserver { this@MemPositionalDataSource.invalidate() }
            cacheList.subscribeActual(observer)
        }
        override fun loadInitial(params: PositionalDataSource.LoadInitialParams, callback: PositionalDataSource.LoadInitialCallback<T>) {
            val totalCount = cacheList.count()
            if (totalCount == 0) {
                callback.onResult(emptyList(), 0, 0)
            } else {
                val position = PositionalDataSource.computeInitialLoadPosition(params, totalCount)
                val loadSize = PositionalDataSource.computeInitialLoadSize(params, position, totalCount)
                val list = this.loadRange(position, loadSize)
                if (list.size == loadSize) {
                    callback.onResult(list, position, totalCount)
                } else {
                    this.invalidate()
                }
            }
        }
    
        override fun loadRange(params: PositionalDataSource.LoadRangeParams, callback: PositionalDataSource.LoadRangeCallback<T>) {
            callback.onResult(this.loadRange(params.startPosition, params.loadSize))
        }
    
        private fun loadRange(startPosition: Int, loadCount: Int): List<T> {
            return ArrayList(cacheList.subList(startPosition, loadCount))
        }
    
        class Factory<Item>( val cacheList :ArrayListWrap<Item> = ArrayListWrap<Item>()) : android.arch.paging.DataSource.Factory<Int, Item>() {
    
            override fun create(): DataSource<Int, Item> {
                return MemPositionalDataSource(cacheList)
            }
        }
    }
    
    

    adapter获取数据时先从这source获取。

  3. 协调缓存中转数据源和网络数据源的MemByPageKeyRepository:

class MemByPageKeyRepository<RESULT>(private val requestCallback: (requestMap: RequestMap,
    callback: (List<RESULT>) -> Unit, requestState: MutableLiveData<RequestState>) -> Unit):LifecycleObserver {
    var currentPageIndex: String? = INIT_PAGE_INDEX
    val wrap = ArrayListWrap<RESULT>()
    @MainThread
    fun getData(pageSize: Int = DEFAULT_PAGE_SEZE): Listing<RESULT> {
        val netSourceFactory = object : DataSource.Factory<String, RESULT>() {
            val sourceLiveData = MutableLiveData<CommonPageKeyedDataSource<RESULT>>()
            override fun create(): DataSource<String, RESULT> {
                val source = CommonPageKeyedDataSource<RESULT>(requestCallback, pageSize)
                sourceLiveData.postValue(source)
                return source
            }
        }
        netSourceFactory.create()

        val dbSourceFactory = MemPositionalDataSource.Factory<RESULT>(wrap)
        val netLoadInitialCallback = object : PageKeyedDataSource.LoadInitialCallback<String, RESULT>() {
            override fun onResult(data: MutableList<RESULT>, position: Int, totalCount: Int, previousPageKey: String?, nextPageKey: String?) {
                wrap.add(data)
                currentPageIndex = nextPageKey
            }

            override fun onResult(data: MutableList<RESULT>, previousPageKey: String?, nextPageKey: String?) {
                wrap.add(data)
                currentPageIndex = nextPageKey
            }
        }
        val livePagedList = LivePagedListBuilder(dbSourceFactory, PagedList.Config.Builder()
            .setPageSize(pageSize)
            .setInitialLoadSizeHint(pageSize)
            .build())
            .setBoundaryCallback(
                object : PagedList.BoundaryCallback<RESULT>() {
                    override fun onZeroItemsLoaded() {
                        if (currentPageIndex!=null) {
                            netSourceFactory.sourceLiveData.value?.loadInitial(PageKeyedDataSource.LoadInitialParams(pageSize, true), netLoadInitialCallback)
                        }
                        LogUtils.v(TAG, "Database returned 0 items. We should query the backend for more items.")
                    }
                    override fun onItemAtEndLoaded(itemAtEnd: RESULT) {
                        LogUtils.v(TAG, " User reached to the end of the list.")
                        if (currentPageIndex!=null) {
                            netSourceFactory.sourceLiveData.value?.loadAfter(PageKeyedDataSource.LoadParams<String>(currentPageIndex, pageSize), object : PageKeyedDataSource
                            .LoadCallback<String, RESULT>() {
                                override fun onResult(data: MutableList<RESULT>, adjacentPageKey: String?) {
                                    wrap.add(data)
                                    currentPageIndex = adjacentPageKey
                                }
                            })
                        }
                    }
                }
            )
            .build()
        val refreshState = Transformations.switchMap(netSourceFactory.sourceLiveData) {
            it.initialLoad
        }
        return Listing(
            pagedList = livePagedList,
            requestState = Transformations.switchMap(netSourceFactory.sourceLiveData) {
                it.networkState
            },
            refresh = {
                wrap.clear()
                netSourceFactory.sourceLiveData.value?.invalidate()
            },
            refreshState = refreshState,
            memWrap = wrap
        )
    }

    val TAG = "MemByPageKeyRepository"
}

修改自官方Demo,有点累了,原理讲不清,也懒得讲,自己看吧。

结尾

这个问题到现在初步得到了解决,获取还有更好的方法,想到了再进行完善,这个过程告诉自己,还是得多看多想,多学习。能够实现还得感谢Google和ObjectBox的优秀开源代码。我并不生产代码,我只是代码的搬运工,哈哈?。

另外推荐一下我修改的支持Paging的**BaseRecyclerViewAdapterHelper**相比原版拥有更好的易用性,比如从adapter获取的item不再需要强转等。以前也向原仓库提交过这个优化的pull request但是被拒绝,比较遗憾。强转在我看来是很恶心的。比较重要的就是添加了Paging的支持和兼容,实现Paging状态下的empty,loading,noMore等状态的显示,但是还存在着一些问题,有很大的优化空间。感谢原仓库优秀的开源代码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值