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

    xiaoxiao2022-07-14  148

    最近在项目中延用了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为我自己的数据源就好了。

    最终实现

    可观察的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>>>>() }

    实现数据缓存中转。

    使用缓存中转的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获取。

    协调缓存中转数据源和网络数据源的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等状态的显示,但是还存在着一些问题,有很大的优化空间。感谢原仓库优秀的开源代码。

    最新回复(0)