最近在项目中延用了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等状态的显示,但是还存在着一些问题,有很大的优化空间。感谢原仓库优秀的开源代码。