关于Paging + Room,RecyclerView刷新时的空指针异常

最近开了一个新项目,使用的是Google 2018 IO大会 推荐的新的app架构,如下:

官方地址

这里主要讲Paging + Room遇到的问题:
基础的参考官方样例: github.com/googlesampl…

PagedList的创建:

完全照搬官方的样例,传入自定义的BoundaryCallBack:

PagedList的DataSource.Factory:

实现交由子类:
Dao:

OK,到这里,基本上都是照搬的官方的样例了,接下来就看运行结果了:
数据能正确获取,并正确刷UI。但是,当数据超过 30条(后面交代为什么是30)时,具体效果如下:
PagedList的长度为50,但是只有30item是有数据的,其他都用null来占位了,
这会造成一个问题,当我需要删除一条数据时,删除后刷新UI,adapter的getItem()方法是会返回null,而我之所以用Paging + Room的形式,就是为了删除,因为PagedList不支持删除,233333。痛哭流涕啊。。。。。。。。

解题思路:

为什么PagedList里的其他数据是null,能不能把null去除

尝试一、发现PagedList的Config中有enablePlaceholders(支持占位)属性,默认为true,支持null,能不能改为false,这样PagedList中就不会有null

解决方案:
结果:
设置PagedList的配置enablePlaceholders为false后,PagedList的Observer接收到的数据并不完整,等于是PagedList将null数据过滤了。

只能往Room的源码挖了,为什么会返回null:
1、将Room与PagedList联系起来的是新建PagedList传入的Room产生的DataSource.Factory

2、寻找具体的DataSource
从FavorVideoDao的具体实现类中,可以看出,getAll()方法返回的是LimitOffsetDataSource,而其中只有恰巧只有一个返回集合为List< Object>的方法,参数为Cursor,所以应该能断定是从数据库中查出数据后处理返回给PagedList的方法。

3、Debug后发现,Cursor的长度与PagedList的Observer接收到的数据长度一致,所以由表入里,看看这个方法的上游是哪,为什么cursor中会有null的数据

LimitOffsetDataSource.loadRange()

从这个方法,我们能发现Cursor的由来, mDb.query(sqLiteQuery);,Sqlite语句sqLiteQuery的由来就有意思了,
limit ? offset ?,查询多少个,偏移多少(从第几个开始),到这里其实就有点眉目了
而这两个值其实来自与方法的参数:
4、查找limit和offset的由来 LimitOffsetDataSource.loadInitial():
PositionalDataSource.computeInitialLoadPosition(): offset:

    public static int computeInitialLoadPosition(@NonNull LoadInitialParams params,
            int totalCount) {
        int position = params.requestedStartPosition;
        int initialLoadSize = params.requestedLoadSize;
        int pageSize = params.pageSize;

        int roundedPageStart = Math.round(position / pageSize) * pageSize;  // 这里肯定是大于0的

        // maximum start pos is that which will encompass end of list
        int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize; // 所以必须保证maximumLoadPage小于等于0,所以必须保证 initialLoadSize必须足够大
        roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);

        // minimum start position is 0
        roundedPageStart = Math.max(0, roundedPageStart);  // 所以,roundedPageStart必须小于等于0

        return roundedPageStart;
    }
复制代码

如果想要将所有数据都加载出来,offset必须的保证为0,具体看上面代码注释,所以关键在于params.requestedLoadSize

PositionalDataSource.computeInitialLoadSize(): limit:

    public static int computeInitialLoadSize(@NonNull LoadInitialParams params,
            int initialLoadPosition, int totalCount) {
        return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize); // 总的个数减去加载的起始位置  params.requestedLoadSize 两数去最小值,为加载的总个数
    }
复制代码

所以当params.requestedLoadSize足够大时,数据库中的所有数据都会被取出
Room加上limit逻辑,也是为了效率更高,但是因为有删除的业务需求,导致异常,所以还是每次全部都取出,RecyclerView刷新时,页只会刷新可见的Item,所以性能上还是OK的

5、查找params.requestedLoadSize的由来
PositionalDataSource.dispatchLoadInitial(),来源于该方法的参数,还是得往上寻找:

TiledPagedList的构造方法:
来源于Config的initialLoadSizeHint属性,所以最后回到了PagedList的Config的initialLoadSizeHint属性。 此处正好回答前面为什么只保存30个数据了:
当未设置Config的initialLoadSizeHint属性时,默认为PageSize的3倍。

解决方案

初始化PagedList时,将initialLoadSizeHint属性设置的足够大:

猜你喜欢

转载自juejin.im/post/5c73a3c051882562c550866f