關於Paging + Room,RecyclerView重新整理時的空指標異常

愛穿襯衫的程式設計師發表於2019-02-25

最近開了一個新專案,使用的是Google 2018 IO大會 推薦的新的app架構,如下:

關於Paging + Room,RecyclerView重新整理時的空指標異常
官方地址

這裡主要講Paging + Room遇到的問題:
基礎的參考官方樣例: github.com/googlesampl…

PagedList的建立:

關於Paging + Room,RecyclerView重新整理時的空指標異常
完全照搬官方的樣例,傳入自定義的BoundaryCallBack:
關於Paging + Room,RecyclerView重新整理時的空指標異常

PagedList的DataSource.Factory:

關於Paging + Room,RecyclerView重新整理時的空指標異常
實現交由子類:
關於Paging + Room,RecyclerView重新整理時的空指標異常
Dao:
關於Paging + Room,RecyclerView重新整理時的空指標異常

OK,到這裡,基本上都是照搬的官方的樣例了,接下來就看執行結果了:
關於Paging + Room,RecyclerView重新整理時的空指標異常
資料能正確獲取,並正確刷UI。但是,當資料超過30條(後面交代為什麼是30)時,具體效果如下:
關於Paging + Room,RecyclerView重新整理時的空指標異常
PagedList的長度為50,但是隻有30item是有資料的,其他都用null來佔位了,
這會造成一個問題,當我需要刪除一條資料時,刪除後重新整理UI,adapter的getItem()方法是會返回null,而我之所以用Paging + Room的形式,就是為了刪除,因為PagedList不支援刪除,233333。痛哭流涕啊。。。。。。。。

解題思路:

為什麼PagedList裡的其他資料是null,能不能把null去除

嘗試一、發現PagedList的Config中有enablePlaceholders(支援佔位)屬性,預設為true,支援null,能不能改為false,這樣PagedList中就不會有null

關於Paging + Room,RecyclerView重新整理時的空指標異常
解決方案:
關於Paging + Room,RecyclerView重新整理時的空指標異常
結果:
設定PagedList的配置enablePlaceholders為false後,PagedList的Observer接收到的資料並不完整,等於是PagedList將null資料過濾了。

只能往Room的原始碼挖了,為什麼會返回null:
1、將Room與PagedList聯絡起來的是新建PagedList傳入的Room產生的DataSource.Factory

關於Paging + Room,RecyclerView重新整理時的空指標異常
2、尋找具體的DataSource
關於Paging + Room,RecyclerView重新整理時的空指標異常
從FavorVideoDao的具體實現類中,可以看出,getAll()方法返回的是LimitOffsetDataSource,而其中只有恰巧只有一個返回集合為List< Object>的方法,引數為Cursor,所以應該能斷定是從資料庫中查出資料後處理返回給PagedList的方法。

3、Debug後發現,Cursor的長度與PagedList的Observer接收到的資料長度一致,所以由表入裡,看看這個方法的上游是哪,為什麼cursor中會有null的資料

關於Paging + Room,RecyclerView重新整理時的空指標異常

LimitOffsetDataSource.loadRange()

關於Paging + Room,RecyclerView重新整理時的空指標異常
從這個方法,我們能發現Cursor的由來,mDb.query(sqLiteQuery);,Sqlite語句sqLiteQuery的由來就有意思了,
關於Paging + Room,RecyclerView重新整理時的空指標異常
limit ? offset ?,查詢多少個,偏移多少(從第幾個開始),到這裡其實就有點眉目了
而這兩個值其實來自與方法的引數:
關於Paging + Room,RecyclerView重新整理時的空指標異常
4、查詢limit和offset的由來 LimitOffsetDataSource.loadInitial():
關於Paging + Room,RecyclerView重新整理時的空指標異常
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(),來源於該方法的引數,還是得往上尋找:

關於Paging + Room,RecyclerView重新整理時的空指標異常
TiledPagedList的構造方法:
關於Paging + Room,RecyclerView重新整理時的空指標異常
來源於Config的initialLoadSizeHint屬性,所以最後回到了PagedList的Config的initialLoadSizeHint屬性。 此處正好回答前面為什麼只儲存30個資料了:
關於Paging + Room,RecyclerView重新整理時的空指標異常
當未設定Config的initialLoadSizeHint屬性時,預設為PageSize的3倍。

解決方案

初始化PagedList時,將initialLoadSizeHint屬性設定的足夠大:

關於Paging + Room,RecyclerView重新整理時的空指標異常

相關文章