歡迎回到 MAD Skills 系列之 Paging 3。0!在上一篇文章《獲取資料並繫結到 UI | MAD Skills》中,我們在
ViewModel
中集成了
Pager
,並利用配合
PagingDataAdapter
向 UI 填充資料,我們也添加了載入狀態指示器,並在出現錯誤時重新載入。
這次,我們把難度提升一個檔次。目前為止,我們都是直接透過網路載入資料,而這樣的操作只適用於理想環境。我們有時候可能遇到網路連線緩慢,或者完全斷網的情況。同時,即使網路狀況良好,我們也不會希望自己的應用成為資料黑洞——在導航到每個介面時都拉取資料是一種十分浪費的行為。
解決這一問題的方法便是從
本地快取
載入資料,並且只在必要的時候進行重新整理。對快取資料的更新必須先到達本地快取,再傳播至 ViewModel。這樣一來,本地快取便可成為唯一可信的資料來源。對我們來說十分方便的是 Paging 庫在 Room 庫一些小小的幫助下已經可以應對這種場景。下面就讓我們開始吧!
影片載入中。。。
△ Paging: 顯示資料及其載入狀態
使用 Room 建立 PagingSource
由於我們將要分頁的資料來源會來自本地而不是直接依賴 API,那麼我們要做的第一件事便是更新
PagingSource
。好訊息是,我們要做的工作很少。是因為我前面提到的 “來自 Room 的小小幫助” 嗎?事實上這裡的幫助遠不止於一點: 只需要在 Room 的 DAO 中為
PagingSource
新增宣告,便可透過
DAO
獲取
PagingSource
!
@Daointerface RepoDao { @Query( “SELECT * FROM repos WHERE ” + “name LIKE :queryString” ) fun reposByName(queryString: String): PagingSource
我們現在可以在
GitHubRepository
中更新
Pager
的建構函式來使用新的
PagingSource
了:
fun getSearchResultStream(query: String): Flow
RemoteMediator
目前為止一切順利……不過我們好像忘記了什麼。本地的資料庫要如何填充資料呢?來看看 RemoteMediator,當資料庫中的資料載入完畢時,它負責從網路載入更多資料。讓我們看看它是如何工作的。
瞭解
RemoteMediator
的關鍵在於認識到它是一個回撥。
RemoteMediator
的結果永遠不會展示在 UI 上,因為它只是 Paging 用於通知作為開發者的我們:
PagingSource
的資料已經耗盡。更新資料庫並通知 Paging,這是我們自己的工作。與
PagingSource
類似,
RemoteMediator
有兩個泛型引數: 查詢引數型別和返回值型別。
@OptIn(ExperimentalPagingApi::class)class GithubRemoteMediator( …) : RemoteMediator
讓我們來仔細觀察下
RemoteMediator
中的抽象方法。第一個方法是
initialize()
,它是在所有載入開始前,
RemoteMediator
呼叫的第一個方法,它的返回值為
InitializeAction
。
InitializeAction
可以是
LAUNCH_INITIAL_REFRESH
,也可以是
SKIP_INITIAL_REFRESH
。前者表示在呼叫 load() 方法時攜帶的載入型別為 refresh,後者意味著只有在 UI 明確發起請求時才會使用
RemoteMediator
執行重新整理操作。在我們的用例中,由於倉庫狀態可能更新得頗為頻繁,所以我們返回
LAUNCH_INITIAL_REFRESH
。
override suspend fun initialize(): InitializeAction { return InitializeAction。LAUNCH_INITIAL_REFRESH }
接下來我們來看
load
方法。
load
方法在
loadType
與
PagingState
所定義的邊界處呼叫,載入型別可以是
refresh
、
append
或
prepend
。這一方法負責獲取資料,將其持久化在磁碟上並通知處理結果,其結果可以是
Error
或
Success
。如果結果是 Error,載入狀態將會反映這一結果,並可能重試載入。如果載入成功,需要通知
Pager
是否可以載入更多資料。
override suspend fun load(loadType: LoadType, state: PagingState
由於
load
方法是一個有返回值的掛起函式,所以 UI 可以精確地反映載入完成的狀態。在上一篇文章中,我們簡要介紹了
withLoadStateHeaderAndFooter
擴充套件函式,並瞭解瞭如何使用它來載入頭部和底部。我們可以觀察到,該擴充套件函式的名字中包含了一個型別:
LoadState
。讓我們進一步瞭解這一型別。
LoadState、LoadStates 以及 CombinedLoadStates
由於分頁是一系列非同步事件,所以透過 UI 反映載入資料的當前狀態十分重要。在分頁操作中,
Pager
的載入狀態是透過
CombinedLoadStates
型別表示的。
顧名思義,這個型別是其他表示載入資訊的型別的組合。這些型別包括:
LoadState
是一個完整描述下列載入狀態的密封類:
Loading
NotLoading
Error
LoadStates
是包含以下三種
LoadState
值的資料類:
append
prepend
refresh
通常來講,
prepend
與
append
載入狀態會用於響應額外的資料獲取,而 refresh 載入狀態則用來響應初始載入、重新整理和重試。
由於
Pager
可能會從
PagingSource
或者
RemoteMediator
載入資料,所以
CombinedLoadStates
有兩個
LoadState
欄位。其中名為
source
的欄位用於
PagingSource
,而名為
mediator
的欄位用於
RemoteMediator
。
方便起見,
CombinedLoadStates
與
LoadStates
相似,同樣含有
refresh
、
append
和
prepend
欄位,它們會基於
Paging
的配置和其他語義反映
RemoteMediator
或
PagingSource
的
LoadState
。請務必檢視相關文件以確定這些欄位在不同場景下的行為。
使用這些資訊更新我們的 UI 就像從
PagingAdapter
暴露的
loadStateFlow
中獲取資料一樣簡單。在我們的應用中,我們可以在第一次載入時使用這些資訊顯示一個載入指示器:
lifecycleScope。launch { repoAdapter。loadStateFlow。collect { loadState -> // 在刷新出錯時顯示重試頭部,並且展示之前快取的狀態或者展示預設的 prepend 狀態 header。loadState = loadState。mediator ?。refresh ?。takeIf { it is LoadState。Error && repoAdapter。itemCount > 0 } ?: loadState。prepend val isListEmpty = loadState。refresh is LoadState。NotLoading && repoAdapter。itemCount == 0 // 顯示空列表 emptyList。isVisible = isListEmpty // 無論資料來自本地資料庫還是遠端資料,僅在重新整理成功時顯示列表。 list。isVisible = loadState。source。refresh is LoadState。NotLoading || loadState。mediator?。refresh is LoadState。NotLoading // 在初始載入或重新整理時顯示載入指示器 progressBar。isVisible = loadState。mediator?。refresh is LoadState。Loading // 如果初始載入或重新整理失敗,顯示重試狀態 retryButton。isVisible = loadState。mediator?。refresh is LoadState。Error && repoAdapter。itemCount == 0 }}
我們開始從
Flow
收集資料,並在
Pager
尚未載入且現存列表為空時,使用
CombinedLoadStates。refresh
欄位展示進度條。我們之所以使用
refresh
欄位,是因為我們只希望在第一次啟動應用、或者明確觸發了重新整理時才展示大進度條。我們還可以檢查是否有載入狀態出錯並通知使用者。
回顧
在本文中,我們實現了以下功能:
使用資料庫作為唯一可信資料來源,並對資料進行分頁;
使用 RemoteMediator 填充基於 Room 的 PagingSource;
使用來自 PagingAdapter 的 LoadStateFlow 更新帶有進度條的 UI。
感謝您的閱讀,下一篇文章將是本系列的最後一篇,敬請期待。