7-分頁庫

分頁庫

分頁庫可幫助您一次加載和顯示一小塊數據。按需載入部分數據會減少網絡帶寬和系統資源的使用量。

聲明依賴項

    dependencies {
      def paging_version = "2.1.1"

      implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

      // alternatively - without Android dependencies for testing
      testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

      // optional - RxJava support
      implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
    }

概覽

庫架構

PageList

分頁庫的關鍵組件是 PagedList 類,用於加載應用數據塊或頁面。隨着所需數據的增多,系統會將其分頁到現有的 PagedList 對象中。如果任何已加載的數據發生更改,會從 LiveData 或基於 RxJava2 的對象向可觀察數據存儲器發出一個新的 PagedList 實例。隨着 PagedList 對象的生成,應用界面會呈現其內容,同時還會考慮界面控件的生命週期。

示例

以下代碼段展示瞭如何配置應用的視圖模型,以便使用 PagedList 對象的 LiveData 存儲器加載和顯示數據:

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: LiveData<PagedList<Concert>> =
                concertDao.concertsByDate().toLiveData(pageSize = 50)
    }

數據

每個 PagedList 實例都會從對應的 DataSource 對象加載應用數據的最新快照。數據從您應用的後端或數據庫流向 PagedList 對象。

以下示例使用 Room 持久性庫來整理應用數據,但如果要通過其他方式存儲數據,也可以提供自己的數據源工廠:

    @Dao
    interface ConcertDao {
        // The Int type parameter tells Room to use a PositionalDataSource object.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        fun concertsByDate(): DataSource.Factory<Int, Concert>
    }

界面

PagedList 類使用 PagedListAdapter 將項加載到 RecyclerView。這些類共同作用,在內容加載時抓取和顯示內容,預取不在視線範圍內的內容以及針對內容更改添加動畫。

支持不同的數據架構

網絡

要顯示來自後端服務器的數據,請使用同步版本的 Retrofit API,將信息加載到您自己的自定義 DataSource 對象中。

注意:由於不同的應用處理和顯示錯誤界面的方式不同,因此分頁庫的 DataSource 對象不提供任何錯誤處理。如果發生錯誤,請遵循結果回調,並在稍後重試請求。有關此行爲的示例,請參閱 PagingWithNetwork 示例

數據庫

設置您的 RecyclerView 以觀察本地存儲空間,最好使用 Room 持久性庫。這樣,無論您何時在應用數據庫中插入或修改數據,這些更改都會自動反映在顯示此數據的 RecyclerView 中。

網絡和數據庫

在開始觀察數據庫之後,您可以使用 PagedList.BoundaryCallback 監聽數據庫中的數據何時耗盡。然後,您可以從網絡中獲取更多項目並將它們插入到數據庫中。如果界面正在觀察數據庫,則您只需執行此操作即可

處理網絡錯誤

通過分頁庫,使用網絡對要顯示的數據進行抓取或分頁時,請務必不要始終將網絡視爲“可用”或“不可用”,因爲許多連接會斷斷續續或不穩定:

  • 特定服務器可能無法響應網絡請求。
  • 設備可能連接到速度較慢或信號較弱的網絡。

您的應用應檢查每個請求是否失敗,並在網絡不可用的情況下儘可能正常恢復。例如,如果數據刷新步驟不起作用,您可以提供“重試”按鈕供用戶選擇。如果在數據分頁步驟中發生錯誤,則最好自動重新嘗試分頁請求。

更新現有應用

自定義分頁解析

如果您使用自定義功能從應用的數據源加載較小的數據子集,則可以將此邏輯替換爲 PagedList 類中的邏輯。PagedList 實例提供了與常見數據源的內置連接。這些實例還爲應用界面中可能包含的 RecyclerView 對象提供了適配器。

使用列表而不是網頁加載的數據

如果您使用內存中列表作爲界面適配器的後備數據結構,並且列表中的項目數量可能會變得非常大,請考慮使用 PagedList 類觀察數據更新。

__PagedList 實例可以使用 LiveDataObservable 向您的應用界面傳遞數據更新,從而最大限度地縮短加載時間並減少內存用量。__在應用中將 List 對象替換成 PagedList 對象會得到更理想的結果,因爲後者不需要對應用界面結構或數據更新邏輯進行任何更改。

使用CursorAdapter將數據光標與列表視圖相關聯

您的應用可能會使用 CursorAdapter 將 Cursor 的數據與 ListView 相關聯。

在這種情況下,您通常需要:

  • 從 ListView 遷移到 RecyclerView,以後者作爲應用的列表界面容器,

  • 將 Cursor 組件替換爲 Room 或 PositionalDataSource,具體取決於 Cursor 實例是否會訪問 SQLite 數據庫。

    在某些情況下,例如在使用 Spinner 的實例時,您只需提供適配器本身。然後,庫將獲取加載到該適配器中的數據,併爲您顯示這些數據。

    在這類情況下,請將適配器的數據類型更改爲 LiveData,然後將此列表封裝到 ArrayAdapter 對象中,再嘗試讓庫類擴充界面中的這些項目。

使用AsyncListUtil異步加載內容

如果您使用 AsyncListUtil 對象來異步加載和顯示信息組,則通過分頁庫可以更輕鬆地加載數據:

  • **您的數據無需固定位置。**通過分頁庫,您可以使用網絡提供的密鑰直接從後端加載數據。
  • **您的數據可能會非常龐大。**通過分頁庫,您可以將數據加載到網頁中,直到沒有剩餘數據爲止。
  • **您可以更輕鬆地觀察數據。**分頁庫可以爲您呈現應用 ViewModel 存儲在可觀察數據結構中的數據。

數據庫示例

使用LiveData觀察分頁數據

以下代碼段顯示了完整代碼。隨着在數據庫中添加、移除或更改 concert 事件,RecyclerView 中的內容會自動且高效地更新:

    @Dao
    interface ConcertDao {
        // The Int type parameter tells Room to use a PositionalDataSource
        // object, with position-based loading under the hood.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        fun concertsByDate(): DataSource.Factory<Int, Concert>
    }

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: LiveData<PagedList<Concert>> =
                concertDao.concertsByDate().toLiveData(pageSize = 50)
    }

    class ConcertActivity : AppCompatActivity() {
        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val viewModel = ViewModelProviders.of(this)
                    .get<ConcertViewModel>()
            val recyclerView = findViewById(R.id.concert_list)
            val adapter = ConcertAdapter()
            viewModel.livePagedList.observe(this, PagedList(adapter::submitList))
            recyclerView.setAdapter(adapter)
        }
    }

    class ConcertAdapter() :
            PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
        fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
            val concert: Concert? = getItem(position)

            // Note that "concert" is a placeholder if it's null.
            holder.bindTo(concert)
        }

        companion object {
            private val DIFF_CALLBACK = object :
                    DiffUtil.ItemCallback<Concert>() {
                // Concert details may have changed if reloaded from the database,
                // but ID is fixed.
                override fun areItemsTheSame(oldConcert: Concert,
                        newConcert: Concert) = oldConcert.id == newConcert.id

                override fun areContentsTheSame(oldConcert: Concert,
                        newConcert: Concert) = oldConcert == newConcert
            }
        }
    }

使用RxJava2觀察分頁數據

如果您傾向於使用 RxJava2 而不是 LiveData,則可以改爲創建 ObservableFlowable 對象:

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: Observable<PagedList<Concert>> =
                concertDao.concertsByDate().toObservable(pageSize = 50)
    }
    

然後,您可以使用以下代碼段中的代碼來開始和停止觀察數據:

    class ConcertActivity : AppCompatActivity() {
        private val adapter: ConcertAdapter()
        private lateinit var viewModel: ConcertViewModel

        private val disposable = CompositeDisposable()

        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val recyclerView = findViewById(R.id.concert_list)
            viewModel = ViewModelProviders.of(this)
                    .get<ConcertViewModel>()
            recyclerView.setAdapter(adapter)
        }

        override fun onStart() {
            super.onStart()
            disposable.add(viewModel.concertList
                    .subscribe(adapter::submitList)))
        }

        override fun onStop() {
            super.onStop()
            disposable.clear()
        }
    }

對於基於 RxJava2 的解決方案,ConcertDaoConcertAdapter 的代碼是相同的,對於基於 LiveData 的解決方案也是如此。

顯示分頁列表

本指南基於_分頁庫概覽_,介紹瞭如何在應用界面中向用戶顯示信息列表,尤其是在此信息發生變化時。

將界面與視圖模型關聯

LiveData 的實例連接到 PagedListAdapter,如以下代碼段所示:

    private val adapter = ConcertAdapter()
    private lateinit var viewModel: ConcertViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        viewModel = ViewModelProviders.of(this).get(ConcertViewModel::class.java)
        viewModel.concerts.observe(this, Observer { adapter.submitList(it) })
    }
    

當數據源提供 PagedList 的新實例時,Activity 會將這些對象發送到適配器。PagedListAdapter 實現定義了更新的計算方式,並自動處理分頁和列表差異。因此,您的 ViewHolder 只需要綁定到提供的特定項即可:

    class ConcertAdapter() :
            PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
        override fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
            val concert: Concert? = getItem(position)

            // Note that "concert" is a placeholder if it's null.
            holder.bindTo(concert)
        }

        companion object {
            private val DIFF_CALLBACK = ... // See Implement the diffing callback section.
        }
    }

PagedListAdapter 使用 PagedList.Callback 對象處理網頁加載事件。當用戶滾動時,PagedListAdapter 會調用 PagedList.loadAround() 來向底層 PagedList 提供關於應從 DataSource 獲取哪些項的提示。

注意PagedList 具有內容不可變特性。這意味着即使可以將新內容加載到 PagedList 的實例中,但加載的項目本身不會在加載後立即改變。因此,如果 PagedList 中的內容更新,則 PagedListAdapter 對象會收到包含更新後信息的全新 PagedList

實現差異回調

以下示例展示了用於比較相關對象字段的 areContentsTheSame() 的手動實現:

    private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Concert>() {
        // The ID property identifies when items are the same.
        override fun areItemsTheSame(oldItem: Concert, newItem: Concert) =
                oldItem.id == newItem.id

        // If you use the "==" operator, make sure that the object implements
        // .equals(). Alternatively, write custom data comparison logic here.
        override fun areContentsTheSame(
                oldItem: Concert, newItem: Concert) = oldItem == newItem
    }
    

由於適配器包含比較項的定義,因此適配器會在有新的 PagedList 對象加載時自動檢測這些項的更改。這樣,適配器就會在您的 RecyclerView 對象內觸發有效的項目動畫。

使用其它適配器類型實現差異回調功能

如果您選擇不從 PagedListAdapter 繼承(例如,當您使用的庫提供自己的適配器時),仍可以直接通過 AsyncPagedListDiffer 對象來使用分頁庫適配器的差異化功能。

在界面中提供佔位符(placeholder)

如果您希望界面在應用完成數據獲取前顯示列表,可以向用戶顯示佔位符列表項。PagedList 對這種情況的處理方式是將列表項數據顯示爲 null,直到加載了數據爲止。

注意:默認情況下,分頁庫支持這種佔位符行爲。

佔位符具有以下優點:

  • 支持滾動條PagedList 可向 PagedListAdapter 提供列表項數量。此信息允許適配器繪製滾動條來傳達整個列表大小。有新頁面載入時,滾動條不會跳到指定位置,因爲列表不會改變大小。
  • 無需加載旋轉圖標:由於列表大小已知,因此無需提醒用戶正在加載更多項。佔位符本身會傳達這一信息。

不過,在添加對佔位符的支持之前,請注意以下前提條件:

  • 需要可計數的數據集Room 持久性庫 中的 DataSource 實例可以有效地計算項的數量。但如果您使用的是自定義本地存儲解決方案或網絡專用數據架構,確定數據集包含多少項可能會開銷極大,甚至根本無法確定。
  • 適配器必須考慮未加載的項:爲準備列表以應對增長而使用的適配器或呈現機制需要處理 Null 列表項。例如,將數據綁定到 ViewHolder 時,您需要提供默認值來表示未加載數據。
  • 需要同樣大小的項視圖:如果列表項大小會隨着內容而變(例如社交網絡更新),則項之間的交叉漸變效果並不理想。在這種情況下,我們強烈建議停用佔位符。

加載分頁數據

討論如何自定義應用的數據加載解決方案以滿足應用的架構需求

構造可觀察列表

通常,您的界面代碼會觀察 LiveData 對象(如果您使用 RxJava2,則會觀察 FlowableObservable 對象),該對象位於您應用的 ViewModel 中。此可觀察對象搭起應用列表數據的呈現與內容之間的關聯。

爲了創建其中一個可觀察的 PagedList 對象,請將 DataSource.Factory 的實例傳遞到 LivePagedListBuilderRxPagedListBuilder 對象。DataSource 對象會加載單個 PagedList 的頁面。Factory 類會創建新的 PagedList 實例來響應內容更新,例如數據庫表失效和網絡刷新。Room 持久性庫可爲您提供 DataSource.Factory 對象,您也可以構建自己的對象

以下代碼段展示瞭如何使用 Room 的 DataSource.Factory 構建功能在應用的 ViewModel 類中創建新的 LiveData 實例:

ConcertDao

    @Dao
    interface ConcertDao {
        // The Int type parameter tells Room to use a PositionalDataSource
        // object, with position-based loading under the hood.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        fun concertsByDate(): DataSource.Factory<Int, Concert>
    }
    

ConcertViewModel

    // The Int type argument corresponds to a PositionalDataSource object.
    val myConcertDataSource : DataSource.Factory<Int, Concert> =
           concertDao.concertsByDate()

    val concertList = myConcertDataSource.toLiveData(pageSize = 50)
    

自定義分頁配置

要進一步爲高級用例配置 LiveData,您還可以定義自己的分頁配置。特別是,您可以定義以下特性:

  • 頁面大小:每個頁面中的項數。
  • 預取距離:給定應用界面中的最後一個可見項,分頁庫應嘗試提前獲取的超出此最後一項的項數。此值應是頁面大小的數倍大。
  • 佔位符存在:確定界面是否對尚未完成加載的列表項顯示佔位符。有關使用佔位符的優缺點的探討,請參閱如何在界面中提供佔位符

如果您希望更好地控制分頁庫何時從應用數據庫加載列表,請將自定義 Executor 對象傳遞給 LivePagedListBuilder,如以下代碼段所示:

ConcertViewModel

    val myPagingConfig = Config(
            pageSize = 50,
            prefetchDistance = 150,
            enablePlaceholders = true
    )

    // The Int type argument corresponds to a PositionalDataSource object.
    val myConcertDataSource : DataSource.Factory<Int, Concert> =
            concertDao.concertsByDate()

    val concertList = myConcertDataSource.toLiveData(
            pagingConfig = myPagingConfig,
            fetchExecutor = myExecutor
    )
    

選擇合適的數據源類型

請務必連接到能最好地處理源數據結構的數據源:

  • 如果您加載的網頁嵌入了上一頁/下一頁的鍵,請使用 PageKeyedDataSource。例如,如果您從網絡中獲取社交媒體帖子,則可能需要將一個 nextPage 令牌從一次加載傳遞到後續加載。

  • 如果您需要使用項目 N 中的數據來獲取項目 N+1,請使用 ItemKeyedDataSource。例如,如果您要爲討論應用獲取會話式評論,則可能需要傳遞最後一條評論的 ID 以獲取下一條評論的內容。

  • 如果您需要從數據存儲區中選擇的任意位置獲取數據頁,請使用 PositionalDataSource。該類支持從您選擇的任意位置開始請求一組數據項。例如,該請求可能會返回從位置 1500 開始的 50 個數據項。

數據無效時發送通知

當使用分頁庫時,由數據層在表或行已過時通知應用的其他層。爲此,請從您爲應用選擇的 DataSource 類中調用 invalidate()

注意:應用界面可以使用下拉刷新模型觸發此數據失效功能。

自定義數據源

如果您使用自定義本地數據解決方案,或直接從網絡加載數據,則可以實現其中一個 DataSource 子類。

以下代碼段展示了從指定音樂會開始時間開始的數據源:

    class ConcertTimeDataSource() :
            ItemKeyedDataSource<Date, Concert>() {
        override fun getKey(item: Concert) = item.startTime

        override fun loadInitial(
                params: LoadInitialParams<Date>,
                callback: LoadInitialCallback<Concert>) {
            val items = fetchItems(params.requestedInitialKey,
                    params.requestedLoadSize)
            callback.onResult(items)
        }

        override fun loadAfter(
                params: LoadParams<Date>,
                callback: LoadCallback<Concert>) {
            val items = fetchItemsAfter(
                date = params.key,
                limit = params.requestedLoadSize)
            callback.onResult(items)
        }
    }
    

然後,您可以通過創建具體的 DataSource.Factory 子類,將此自定義數據加載到 PagedList 對象中。

以下代碼段展示瞭如何生成前面代碼段中定義的自定義數據源的新實例:

    class ConcertTimeDataSourceFactory :
            DataSource.Factory<Date, Concert>() {
        val sourceLiveData = MutableLiveData<ConcertTimeDataSource>()
        var latestSource: ConcertDataSource?
        override fun create(): DataSource<Date, Concert> {
            latestSource = ConcertTimeDataSource()
            sourceLiveData.postValue(latestSource)
            return latestSource
        }
    }

考慮內容更新的運作方式

構建可觀察的 PagedList 對象時,請考慮內容更新的運作方式。如果直接從 Room 數據庫加載數據,則更新會自動推送至應用界面。

使用分頁網絡 API 時,您通常需要使用“下拉刷新”這樣的用戶互動,以指示系統讓最近使用的 DataSource 的失效。然後請求該數據源的新實例。

以下代碼段演示了此行爲:

    class ConcertActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            // ...
            concertTimeViewModel.refreshState.observe(this, Observer {
                // Shows one possible way of triggering a refresh operation.
                swipeRefreshLayout.isRefreshing =
                        it == MyNetworkState.LOADING
            })
            swipeRefreshLayout.setOnRefreshListener {
                concertTimeViewModel.invalidateDataSource()
            }
        }
    }

    class ConcertTimeViewModel(firstConcertStartTime: Date) : ViewModel() {
        val dataSourceFactory = ConcertTimeDataSourceFactory(firstConcertStartTime)
        val concertList: LiveData<PagedList<Concert>> =
                dataSourceFactory.toLiveData(
                    pageSize = 50,
                    fetchExecutor = myExecutor
                )

        fun invalidateDataSource() =
                dataSourceFactory.sourceLiveData.value?.invalidate()
    }

提供數據映射

分頁庫支持基於項目或基於頁面轉換由 DataSource 加載的項目。

在以下代碼段中,音樂會名稱和音樂會日期的組合映射到同時包含名稱和日期的單個字符串:

    class ConcertViewModel : ViewModel() {
        val concertDescriptions : LiveData<PagedList<String>> {
            init {
                val concerts = database.allConcertsFactory()
                        .map "${it.name} - ${it.date}" }
                        .toLiveData(pageSize = 50)
            }
        }
    }

如果您希望在項目加載後進行換行、轉換或準備,這將非常有用。由於這項工作是在提取執行程序上完成的,因此您可以執行開銷可能很大的工作,如從磁盤讀取或查詢單獨的數據庫。

注意:JOIN 查詢作爲 map() 的一部分,總是能夠更高效地進行重新查詢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章