1. 簡介
課程內容
- 哪些是分頁程式庫的主要元件。
- 如何在專案中新增分頁程式庫。
建構項目
在本程式碼研究室中,您會從已能顯示文章清單的範例應用程式開始操作。這份靜態清單包含 500 篇文章,且所有文章都儲存在手機記憶體中:
跟著本程式碼研究室的步驟操作,您將:
- ...瞭解分頁的概念。
- ...瞭解分頁程式庫的核心元件。
- ...得知使用分頁程式庫實作分頁的方法。
完成課程後,您將建構一款符合以下條件的應用程式:
- ...可成功實作分頁。
- ...在擷取更多資料時,可有效地向使用者說明。
以下快速看一下我們將打造的使用者介面:
軟硬體需求
建議條件
- 熟悉下列架構元件:ViewModel、View Binding,以及《應用程式架構指南》中建議採用的架構。如需架構元件的簡介,請參閱「具有檢視畫面的 Room」程式碼研究室。
- 熟悉協同程式和 Kotlin Flow。如需 Flow 的簡介,請參閱使用 Kotlin Flow 和 LiveData 的進階協同程式相關程式碼研究室。
2. 設定環境
在這個步驟中,您將下載適用於整個程式碼研究室的程式碼,然後執行簡單的範例應用程式。
我們準備了範例專案,方便您盡快上手並進行建構。
如果您已安裝 Git,只要執行下列指令即可。如要檢查 Git 是否已安裝完成,請在終端機或指令列中輸入 git --version
,並確認該指令可正確執行。
git clone https://github.com/googlecodelabs/android-paging
如果您沒有 Git,可以按一下下方按鈕,下載本程式碼研究室的所有程式碼:
系統會將程式碼整理在兩個資料夾中:basic
和 advanced
。但在這個程式碼研究室,我們只會用到 basic
資料夾。
basic
資料夾中還有兩個資料夾:start
和 end
。我們將開始處理 start
資料夾中的程式碼;在程式碼研究室課程最後,start
資料夾中的程式碼應該會與 end
資料夾中的程式碼相同。
- 在 Android Studio 的
basic/start
目錄中開啟專案。 - 在裝置或模擬器上執行
app
執行設定。
現在應該會顯示文章清單!請捲動至頁面底部,確認這是靜態清單,也就是說,當我們捲動到清單結尾時,系統不會擷取更多項目。接著請捲動回頂端,確認所有項目仍完好如初。
3. 分頁簡介
向使用者顯示資訊的方法很多,最常見的是使用清單。不過有時候,這些清單只會透過小型視窗讓使用者瀏覽所有內容。當使用者捲動瀏覽可用的資訊時,往往會預期系統擷取更多資料來補充已顯示的資訊。因此每次擷取資料時都必須注重效率和流暢性,這樣逐步增量載入作業就不會對使用者體驗造成負面影響。另外,逐步增量載入功能也能提升效能,因為應用程式不需要一次將大量資料保存在記憶體中。
這個逐步擷取資訊的程序稱為「分頁」,其中每個「頁面」會對應要擷取的資料區塊。要求頁面時,需要分頁的資料來源通常必須使用「查詢」,藉此定義所需資訊。本程式碼研究室的其餘部分將介紹分頁程式庫,並示範這個程式庫如何協助您快速有效率地在應用程式中實作分頁。
分頁程式庫的核心元件
分頁程式庫的核心元件如下:
PagingSource
- 為特定頁面查詢載入資料區塊的基礎類別。這是資料層的一部分,通常會透過DataSource
類別揭露,且後續由Repository
揭露,以用於ViewModel
中。PagingConfig
- 這個類別用於定義決定分頁行為的參數,包括頁面大小、是否啟用預留位置等等。Pager
- 這個類別負責產生PagingData
資料流。這項作業視PagingSource
而定,且應在ViewModel
中建立。PagingData
- 分頁資料的容器。每次重新整理資料時,系統都會產生獨立且對應的PagingData
發射項目,並由其專屬的PagingSource
做為支援。PagingDataAdapter
-RecyclerView.Adapter
子類別,會在RecyclerView
中顯示PagingData
。PagingDataAdapter
可以使用 Factory 方法連結至 KotlinFlow
、LiveData
、RxJavaFlowable
、RxJavaObservable
,甚至是靜態清單。PagingDataAdapter
會監聽內部PagingData
載入事件,並在頁面載入時有效更新使用者介面。
在以下各節中,您將實作上述各項元件的範例。
4. 專案總覽
應用程式目前的形式會顯示一份靜態文章清單。每篇文章都有標題、說明和建立日期。靜態清單適用於少數項目的情況,但隨著資料集越來越大,這份清單便無法妥善調整規模。我們將透過分頁程式庫實作分頁,藉此修正這個問題,但首先回顧一下應用程式中現有的元件。
應用程式採用的是《應用程式架構指南》中建議採用的架構。以下是您會在各套件中看到的內容:
資料層:
ArticleRepository
:負責提供文章清單並將其儲存在記憶體中。Article
:這個類別代表「資料模型」,表示從資料層中擷取的資訊。
使用者介面層:
Activity
、RecyclerView.Adapter
和RecyclerView.ViewHolder
:這些類別負責在使用者介面中顯示清單。ViewModel
:這個狀態容器負責建立使用者介面要顯示的狀態。
存放區會在具有 articleStream
欄位的 Flow
中公開顯示所有相關文章,這會依次由使用者介面層中的 ArticleViewModel
讀取,並讓其就緒,以便於 ArticleActivity
中的使用者介面透過 state
欄位 (亦即 StateFlow
) 使用。
將文章當做來自存放區的 Flow
公開顯示後,存放區就能在已顯示的文章有所變動時加以更新。舉例來說,假如文章標題有所更動,系統可以輕鬆將這項更動傳達給 articleStream
的收集器。而針對 ViewModel
中的使用者介面狀態使用 StateFlow
,可確保即使系統停止收集使用者介面狀態 (例如在更改設定期間重新建立 Activity
時),還是可以在再次開始收集時,從中斷處繼續。
如前文所述,存放區中目前的 articleStream
只會顯示當天的新聞。雖然這可能足以符合部分使用者的需求,但其他使用者或許會希望先瀏覽當天提供的所有文章,之後再瀏覽較舊的文章。正因為他們有這種期望,顯示文章時才更適合使用分頁。應該透過文章來探索分頁的其他原因包括:
ViewModel
會將所有載入到記憶體中的項目保留在items
StateFlow
中。當資料集日趨龐大時,這會是首要考量,因為這樣會影響效能。- 當清單中的一或多篇文章出現變動,我們就必須更新,但隨著文章清單越來越大,更新的成本就越來越高。
分頁程式庫能協助解決所有這些問題,同時提供一致的 API,在應用程式中以逐步增量的方式 (分頁) 擷取資料。
5. 定義資料來源
實作分頁時,我們希望確保同時滿足下列條件:
- 妥善處理來自使用者介面的資料要求,確保不會同時對同一查詢觸發多項要求。
- 在記憶體中保留一定限度的擷取資料。
- 觸發擷取更多資料的要求,以補充已擷取的資料。
我們可以透過 PagingSource
滿足所有這些條件。PagingSource
會指定如何透過逐步增量的區塊擷取資料,定義資料來源。接著,PagingData
物件會擷取 PagingSource
中的資料,以便對使用者在 RecyclerView
中捲動時產生的載入提示做出回應。
PagingSource
將載入文章。在 data/Article.kt
中,您會發現模型的定義如下::
data class Article(
val id: Int,
val title: String,
val description: String,
val created: LocalDateTime,
)
如要建構 PagingSource
,您必須定義下列項目:
- 分頁金鑰的類型 - 這會定義我們用來要求更多資料的頁面查詢類型。在這個範例中,ID 必然會按遞增順序排序,因此我們會擷取特定文章 ID 前後的文章。
- 載入的資料類型 - 每個頁面都會傳回文章的
List
,因此類型為Article
。 - 資料擷取來源 - 這通常是資料庫、網路資源或任何其他分頁式資料的來源。不過,在本程式碼研究室中,我們使用的是本機產生的資料。
現在要在 data
套件中,透過名為 ArticlePagingSource.kt
的新檔案建立 PagingSource
實作項目:
package com.example.android.codelabs.paging.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
TODO("Not yet implemented")
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
TODO("Not yet implemented")
}
}
PagingSource
要求我們實作 load()
和 getRefreshKey()
這兩個函式。
分頁程式庫會呼叫 load()
函式,以非同步方式擷取更多資料,以便在使用者捲動畫面時顯示。LoadParams
物件則保留與載入作業相關的資訊,包括:
- 要載入頁面的金鑰 - 如果這是系統第一次呼叫
load()
,LoadParams.key
會是null
。在本範例中,您必須定義初始頁面金鑰。就我們的專案而言,我們會使用文章 ID 做為金鑰。同時,請在ArticlePagingSource
檔案的頂端新增STARTING_KEY
常數0
,做為初始頁面金鑰。 - 載入大小 - 要求載入的項目數。
load()
函式會傳回 LoadResult
。LoadResult
可以是以下其中一種類型:
LoadResult.Page
(如果結果成功)。LoadResult.Error
(發生錯誤時)。LoadResult.Invalid
(如果PagingSource
因無法再保證其結果的完整性而失效)。
LoadResult.Page
含有三個必要的引數:
data
:擷取到的項目List
。prevKey
:load()
方法在需要擷取當前頁面「背後」的項目時使用的金鑰。nextKey
:load()
方法在需要擷取當前頁面「之後」的項目時使用的金鑰。
...另外還有兩個選用引數:
itemsBefore
:載入資料前方顯示的預留位置數量。itemsAfter
:載入資料後方顯示的預留位置數量。
載入金鑰為 Article.id
欄位。我們可以將這個欄位當做金鑰使用的原因在於,每篇文章的 Article
ID 都會加一;也就是說,文章 ID 是連續遞增的整數。
如果對應方向沒有其他資料可供載入,nextKey
或 prevKey
就會是 null
。在本例中,prevKey
會有以下情況:
- 如果
startKey
與STARTING_KEY
相同,系統會傳回空值,原因是我們無法在這個金鑰背後載入更多項目。 - 否則,我們會擷取清單中的第一個項目,並在其背後載入
LoadParams.loadSize
,以確保傳回的金鑰絕不會小於STARTING_KEY
。只要定義ensureValidKey()
方法,即可完成上述操作。
新增以下函式來檢查分頁金鑰是否有效:
class ArticlePagingSource : PagingSource<Int, Article>() {
...
/**
* Makes sure the paging key is never less than [STARTING_KEY]
*/
private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)
}
就 nextKey
而言:
- 由於我們支援載入無限量的項目,因此會在
range.last + 1
中傳遞。
此外,由於每篇文章都有 created
欄位,我們還需要為該欄位產生一個值。請將下列程式碼新增到檔案頂端:
private val firstArticleCreatedTime = LocalDateTime.now()
class ArticlePagingSource : PagingSource<Int, Article>() {
...
}
備妥上述所有程式碼後,我們現在就可以實作 load()
函式:
import kotlin.math.max
...
private val firstArticleCreatedTime = LocalDateTime.now()
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
// Start paging with the STARTING_KEY if this is the first load
val start = params.key ?: STARTING_KEY
// Load as many items as hinted by params.loadSize
val range = start.until(start + params.loadSize)
return LoadResult.Page(
data = range.map { number ->
Article(
// Generate consecutive increasing numbers as the article id
id = number,
title = "Article $number",
description = "This describes article $number",
created = firstArticleCreatedTime.minusDays(number.toLong())
)
},
// Make sure we don't try to load items behind the STARTING_KEY
prevKey = when (start) {
STARTING_KEY -> null
else -> ensureValidKey(key = range.first - params.loadSize)
},
nextKey = range.last + 1
)
}
...
}
接下來,我們需要實作 getRefreshKey()
。當分頁程式庫因幕後 PagingSource
中的資料發生變動而必須重新載入 UI 所用的項目時,就需要呼叫這個方法。在此情況下,PagingSource
的基本資料已有所變動,而且需要在使用者介面中更新,這就是所謂的「失效」。失效情形發生時,分頁程式庫會建立新的 PagingSource
來重新載入資料,並發出新的 PagingData
來通知使用者介面。如果想進一步瞭解「失效」,請參閱稍後的章節。
從新的 PagingSource
載入時,系統會呼叫 getRefreshKey()
來提供新的 PagingSource
開始載入時要使用的金鑰,以確保在重新整理畫面後,使用者還是能找到他們在清單中的目前位置。
分頁程式庫失效的原因有兩種:
- 您在
PagingAdapter
上呼叫了refresh()
。 - 您在
PagingSource
上呼叫了invalidate()
。
傳回的金鑰 (在本範例中為 Int
) 會透過 LoadParams
引數傳送給新 PagingSource
中 load()
方法的下一次呼叫。為防止項目在失效後跳轉,我們需要確保傳回的金鑰能載入足夠的項目來填滿畫面。這會造成新項目集較有可能出現無效資料中的項目,有助於維持目前的捲動位置。現在我們來看看如何在應用程式中實作:
// The refresh key is used for the initial load of the next PagingSource, after invalidation
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
// In our case we grab the item closest to the anchor position
// then return its id - (state.config.pageSize / 2) as a buffer
val anchorPosition = state.anchorPosition ?: return null
val article = state.closestItemToPosition(anchorPosition) ?: return null
return ensureValidKey(key = article.id - (state.config.pageSize / 2))
}
在上方的程式碼片段中,我們使用的是 PagingState.anchorPosition
。如果您不清楚分頁程式庫如何知道要擷取更多項目,這是一個線索!當使用者介面嘗試讀取 PagingData
中的項目時,會在特定索引進行讀取。如果資料已讀取完畢,系統便會在使用者介面中顯示該資料。但在沒有資料的情況下,Paging 程式庫會知道自身必須擷取資料,才能執行失敗的讀取要求。上次在讀取時成功擷取資料的索引是 anchorPosition
。
重新整理畫面時,我們會找出最靠近 anchorPosition
的 Article
,並拿取其中的金鑰做為載入金鑰。如此一來,當我們從新的 PagingSource
重新開始載入時,已擷取的項目集就會包含已載入的項目,以確保能提供流暢且一致的使用者體驗。
完成上述步驟表示您已充分定義了 PagingSource
,下一步是將其連結至使用者介面。
6. 為 UI 產生 PagingData
在目前的實作項目中,我們是透過在 ArticleRepository
中使用 Flow<List<Article>>
,向 ViewModel
揭露已載入的資料。反過來,ViewModel
會透過 stateIn
運算子,將資料維持在隨時可用的狀態,以便顯示在使用者介面中。
透過分頁程式庫,我們會改為從 ViewModel
公開 Flow<PagingData<Article>>
。PagingData
這種類型能納入我們已載入的資料,協助分頁程式庫判定要在什麼時候擷取更多資料,同時確保我們不會對同一頁面提出兩次要求。
為了建構 PagingData
,我們會根據要使用哪個 API 將 PagingData
傳遞至應用程式的其他層,決定採用 Pager
類別的其中一個建構工具方法:
- Kotlin
Flow
- 使用Pager.flow
。 LiveData
- 使用Pager.liveData
。- RxJava
Flowable
- 使用Pager.flowable
。 - RxJava
Observable
- 使用Pager.observable
。
由於我們已在應用程式中採用 Flow
,因此將繼續採用這種做法;但我們會改用 Flow<PagingData<Article>>
,而不是 Flow<List<Article>>
。
無論您使用哪一種 PagingData
建構工具,都必須傳遞下列參數:
PagingConfig
。此類別用於設定如何從PagingSource
載入內容的相關選項,例如要提早多久進行載入、初始載入的大小要求等。您必須定義的唯一必要參數是頁面大小,也就是每個頁面應載入的項目數量。根據預設,分頁會將所有載入的頁面保存在記憶體中。如要確保在使用者捲動頁面時不浪費記憶體,請在PagingConfig
中設定maxSize
參數。根據預設,如果分頁可以計算已卸載的項目,且enablePlaceholders
設定標記為true
,則分頁將針對尚未載入的內容傳回空值做為預留位置。這樣一來,您就可以在轉接器中顯示預留位置檢視畫面。為了簡化本程式碼研究室的工作,讓我們先傳遞enablePlaceholders = false
,藉此停用預留位置。- 可定義如何建立
PagingSource
的函式。在本範例中,我們會建立ArticlePagingSource
,因此需要透過函式來告知分頁程式庫該如何執行此操作。
現在我們可以來修改 ArticleRepository
了!
更新 ArticleRepository
- 刪除
articlesStream
欄位。 - 新增名為
articlePagingSource()
的方法,用於傳回剛剛建立的ArticlePagingSource
。
class ArticleRepository {
fun articlePagingSource() = ArticlePagingSource()
}
清除 ArticleRepository
分頁程式庫為我們處理了許多事情:
- 處理記憶體內的快取。
- 在使用者滑動到清單末端時提出資料要求。
這表示除了 articlePagingSource()
外,可以移除 ArticleRepository
中的所有其他項目。您的 ArticleRepository
檔案現在看起來應像這樣:
package com.example.android.codelabs.paging.data
import androidx.paging.PagingSource
class ArticleRepository {
fun articlePagingSource() = ArticlePagingSource()
}
現在 ArticleViewModel
中應會出現編譯錯誤。我們來看看其中需要進行哪些變更!
7. 在 ViewModel 中要求及快取 PagingData
在處理編譯錯誤之前,我們來檢查一下 ViewModel
。
class ArticleViewModel(...) : ViewModel() {
val items: StateFlow<List<Article>> = ...
}
為了在 ViewModel
中整合分頁程式庫,我們會將 items
的傳回類型從 StateFlow<List<Article>>
變更為 Flow<PagingData<Article>>
。為此,請先在檔案頂端新增名為 ITEMS_PER_PAGE
的私有常數:
private const val ITEMS_PER_PAGE = 50
class ArticleViewModel {
...
}
接著,我們將 items
更新為 Pager
執行個體的輸出結果,方法是將以下兩個參數傳遞給 Pager
:
pageSize
為ITEMS_PER_PAGE
且預留位置已停用的PagingConfig
PagingSourceFactory
,用於提供剛剛建立的ArticlePagingSource
的執行個體。
class ArticleViewModel(...) : ViewModel() {
val items: Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
pagingSourceFactory = { repository.articlePagingSource() }
)
.flow
...
}
接著,為維持經過設定或瀏覽變更後的分頁狀態,我們會使用 cachedIn()
方法,將 androidx.lifecycle.viewModelScope
傳遞給它。
完成上述變更後,ViewModel
看起來會像這樣:
package com.example.android.codelabs.paging.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.android.codelabs.paging.data.Article
import com.example.android.codelabs.paging.data.ArticleRepository
import com.example.android.codelabs.paging.data.ITEMS_PER_PAGE
import kotlinx.coroutines.flow.Flow
private const val ITEMS_PER_PAGE = 50
class ArticleViewModel(
private val repository: ArticleRepository,
) : ViewModel() {
val items: Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
pagingSourceFactory = { repository.articlePagingSource() }
)
.flow
.cachedIn(viewModelScope)
}
關於 PagingData
還有一點要注意,它屬於獨立類型,包含可變動的更新資料串流,而這些資料會顯示在 RecyclerView
中。每次發出的 PagingData
均完全獨立,但如果幕後的 PagingSource
因基礎資料集的變動而失效,則可能會針對單一查詢發出多個 PagingData
執行個體。因此,公開 PagingData
的 Flows
時,應與其他 Flows
分開進行。
大功告成!ViewModel
現在可以支援分頁功能了!
8. 將轉接器與 PagingData 搭配使用
如要將 PagingData
繫結至 RecyclerView
,請使用 PagingDataAdapter
。每當載入 PagingData
內容時,PagingDataAdapter
都會收到通知,接著向 RecyclerView
發出更新信號。
更新 ArticleAdapter
以使用 PagingData
資料流:
- 目前,
ArticleAdapter
已實作ListAdapter
,請改為實作PagingDataAdapter
。類別主體的其餘部分則維持不變:
import androidx.paging.PagingDataAdapter
...
class ArticleAdapter : PagingDataAdapter<Article, RepoViewHolder>(ARTICLE_DIFF_CALLBACK) {
// body is unchanged
}
到現在為止我們已進行了許多變更,現在只要再執行一個步驟就能開始執行應用程式了,這個步驟就是連結使用者介面!
9. 在 UI 中使用 PagingData
在目前的實作項目中,有一個名為 binding.setupScrollListener()
的方法,可在符合某些條件時呼叫 ViewModel
,以載入更多資料。由於分頁程式庫會自動執行上述所有操作,因此我們可以刪除此方法及相關用例。
接著,因為 ArticleAdapter
不再是 ListAdapter
,而是 PagingDataAdapter
,因此我們進行兩項微幅更動:
- 將
Flow
上的終端機運算子從ViewModel
切換為collectLatest
(而非collect
)。 - 使用
submitData()
(而非submitList()
) 將變更內容告知ArticleAdapter
。
我們會在 pagingData
Flow
上使用 collectLatest
,這樣當新的 pagingData
執行個體發射時,系統會針對先前的 pagingData
發射項目取消收集作業。
完成這些變更後,Activity
看起來應該會像這樣:
import kotlinx.coroutines.flow.collectLatest
class ArticleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityArticlesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val viewModel by viewModels<ArticleViewModel>(
factoryProducer = { Injection.provideViewModelFactory(owner = this) }
)
val items = viewModel.items
val articleAdapter = ArticleAdapter()
binding.bindAdapter(articleAdapter = articleAdapter)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
items.collectLatest {
articleAdapter.submitData(it)
}
}
}
}
}
private fun ActivityArticlesBinding.bindAdapter(
articleAdapter: ArticleAdapter
) {
list.adapter = articleAdapter
list.layoutManager = LinearLayoutManager(list.context)
val decoration = DividerItemDecoration(list.context, DividerItemDecoration.VERTICAL)
list.addItemDecoration(decoration)
}
應用程式現在應可編譯並執行。您已成功將應用程式遷移至分頁程式庫!
10. 在 UI 中顯示載入狀態
當分頁程式庫擷取更多要在使用者介面中顯示的項目時,最佳做法就是告知使用者有更多資料正在傳送中。幸運的是,分頁程式庫方便您透過 CombinedLoadStates
類型存取其載入狀態。
CombinedLoadStates
執行個體會針對分頁程式庫中所有會載入資料的元件描述載入狀態。在本範例中,我們只關注 ArticlePagingSource
的 LoadState
,因此主要處理 CombinedLoadStates.source
欄位中 LoadStates
的類型。您可以透過 PagingDataAdapter.loadStateFlow
經由 PagingDataAdapter
存取 CombinedLoadStates
。
CombinedLoadStates.source
是 LoadStates
類型,包含三種不同 LoadState
的欄位:
LoadStates.append
:用於在使用者目前位置之後擷取的項目LoadState
。LoadStates.prepend
:用於在使用者目前位置之前擷取的項目LoadState
。LoadStates.refresh
:用於初始載入的LoadState
。
每項 LoadState
都可以設為下列其中一種狀態:
LoadState.Loading
:正在載入項目。LoadState.NotLoading
:未載入項目。LoadState.Error
:載入時發生錯誤。
在我們的範例中,由於 ArticlePagingSource
不包含錯誤情況,因此我們只關心 LoadState
是否為 LoadState.Loading
。
首先,我們要在使用者介面頂端和底端新增進度列,以便表示任一方向的擷取作業載入狀態。
在 activity_articles.xml
中新增兩條 LinearProgressIndicator
列,如下所示:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.ArticleActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/prepend_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/append_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
接著,我們透過從 PagingDataAdapter
收集 LoadStatesFlow
來回應 CombinedLoadState
。請收集 ArticleActivity.kt
中的狀態:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
articleAdapter.loadStateFlow.collect {
binding.prependProgress.isVisible = it.source.prepend is Loading
binding.appendProgress.isVisible = it.source.append is Loading
}
}
}
lifecycleScope.launch {
...
}
最後,我們會在 ArticlePagingSource
中稍微延遲,以模擬負載:
private const val LOAD_DELAY_MILLIS = 3_000L
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val start = params.key ?: STARTING_KEY
val range = startKey.until(startKey + params.loadSize)
if (start != STARTING_KEY) delay(LOAD_DELAY_MILLIS)
return ...
}
讓應用程式再次執行,並捲動至清單底部。當分頁程式庫擷取更多項目時,您應該會看到底端的進度列,而作業完成後,此進度列就會消失!
11. 總結
現在快速回顧一下本課程介紹的內容,我們...:
- ...大致瞭解了分頁程序及其重要性。
- ...透過建立
Pager
、定義PagingSource
及發出PagingData
,為應用程式新增了分頁。 - ...使用
cachedIn
運算子在ViewModel
中快取PagingData
。 - ...透過
PagingDataAdapter
在使用者介面中使用PagingData
。 - ...透過
PagingDataAdapter.loadStateFlow
回應CombinedLoadStates
。
大功告成!如要瞭解更多進階分頁概念,請參閱「進階分頁」程式碼研究室!