深入探索 Paging 3.0: 分頁載入來自網路和資料庫的資料 | MAD Skills

深入探索 Paging 3.0: 分頁載入來自網路和資料庫的資料 | MAD Skills

歡迎回到 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> { … val pagingSourceFactory = { database。reposDao()。reposByName(dbQuery) } @OptIn(ExperimentalPagingApi::class) return Pager( config = PagingConfig( pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = false ), pagingSourceFactory = pagingSourceFactory, remoteMediator = …, )。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): MediatorResult { val page = when (loadType) { LoadType。REFRESH -> … LoadType。PREPEND -> … LoadType。APPEND -> … } val apiQuery = query + IN_QUALIFIER try { val apiResponse = service。searchRepos(apiQuery, page, state。config。pageSize) val repos = apiResponse。items val endOfPaginationReached = repos。isEmpty() repoDatabase。withTransaction { … repoDatabase。reposDao()。insertAll(repos) } return MediatorResult。Success(endOfPaginationReached = endOfPaginationReached) } catch (exception: IOException) { return MediatorResult。Error(exception) } catch (exception: HttpException) { return MediatorResult。Error(exception) } }

由於

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。

感謝您的閱讀,下一篇文章將是本系列的最後一篇,敬請期待。