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會將所有載入到記憶體中的項目保留在itemsStateFlow中。當資料集日趨龐大時,這會是首要考量,因為這樣會影響效能。- 當清單中的一或多篇文章出現變動,我們就必須更新,但隨著文章清單越來越大,更新的成本就越來越高。
Paging 程式庫能協助解決所有這些問題,同時提供一致的 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且預留位置已停用的PagingConfigPagingSourceFactory,用於提供剛剛建立的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。
大功告成!如要瞭解更多進階分頁概念,請參閱「進階分頁」程式碼研究室!