Google 最近更新了幾個 Jetpack 新成員 Hilt、Paging 三、App Startup 等等。java
在以前的文章裏面分別分析 App Startup 實踐以及原理 和 Paging3 加載本地數據(一)實踐以及原理,若是沒有看過能夠點擊下方地址前去查看:git
今天這邊文章主要來分析 Paging3 加載網絡數據及其原理,利用週末的時間參考 Google 文檔實現了 Paging3 期間也遇到一些坑,會在文中詳細分析,代碼已經上傳到了 GitHub:Paging3SimpleWithNetWorkgithub
經過這篇文章你將學習到如下內容:面試
在項目 Paging3SimpleWithNetWork 中用到了 Coil(Kotlin 圖片加載庫)、Databinding(數據綁定)、Anko(主要用來替換替代 XML 使用的方式)、Koin(Kotlin 依賴注入庫)、JDatabinding(基於 Databinding 封裝的組件)、Data Mapper(數據映射)、使用 Composing builds 做爲依賴庫的版本管理、Repository 設計模式、MVVM 架構等等,關於這裏一些技術以前沒有了解過,能夠點擊下面鏈接前往查看。算法
Paging 是一個分頁庫,它能夠幫助您從本地存儲或經過網絡加載顯示數據。這種方法使你的 App 更有效地使用網絡帶寬和系統資源。數據庫
Google 推薦使用 Paging 做爲 App 架構的一部分,它能夠很方便的和 Jetpack 組件集成,Paging3 包含了如下功能:編程
在 Paging3 以前提供了 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 這三個類,在這三個類中進行數據獲取的操做。設計模式
https://api.github.com/users?since=0?per_page=30
當 since = 0 時獲取第一頁數據,當前頁面最後一條數據的 ID 是 46。https://api.github.com/users?since=46?per_page=30
。在 Paging3 以後 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 合併爲一個 PagingSource,全部舊 API 加載方法被合併到 PagingSource 中的單個 load() 方法中。api
abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
複製代碼
這是一個掛起函數,實現這個方法來觸發異步加載,具體實現見下文,另外在 Paging3 中還有如下變化數組
Google 推薦咱們使用 Paging3 時,在應用程序的三層中操做,以及它們如何協同工做加載和顯示分頁數據,以下圖所示:
咱們接下來按照 Google 推薦的方式開始實現,只須要四步便可實現 Paging3 加載網絡數據,文中只貼出核心代碼,具體實現能夠看 GitHub 上的 Paging3SimpleWithNetWork 項目。
這裏選擇使用的是 GitHub API
interface GitHubService {
@GET("users")
suspend fun getGithubAccount(@Query("since") id: Int, @Query("per_page") perPage: Int):
List<GithubAccountModel>
companion object {
fun create(): GitHubService {
val client = OkHttpClient.Builder()
.build()
val retrofit = Retrofit.Builder()
.client(client)
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(GitHubService::class.java)
}
}
}
複製代碼
注意: 這裏須要在 getGithubAccount 方法前添加 suspend 關鍵字,不然調用的時候,會拋出如下異常。
Unable to create call adapter for XXXXX
複製代碼
class GitHubItemPagingSource(
private val api: GitHubService
) : PagingSource<Int, GithubAccountModel>(), AnkoLogger {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GithubAccountModel> {
return try {
// key 至關於 id
val key = params.key ?: 0
// 獲取網絡數據
val items = api.getGithubAccount(key, params.loadSize)
// 請求失敗或者出現異常,會跳轉到 case 語句返回 LoadResult.Error(e)
// 請求成功,構造一個 LoadResult.Page 返回
LoadResult.Page(
data = items, // 返回獲取到的數據
prevKey = null, // 上一頁,設置爲空就沒有上一頁的效果,這須要注意的是,若是是第一頁須要返回 null,不然會出現屢次請求
nextKey = items.lastOrNull()?.id// 下一頁,設置爲空就沒有加載更多效果,若是後面沒有更多數據設置爲空,即滑動到最後不會在加載數據
)
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(e)
}
}
}
複製代碼
PagingSource 是一個抽象類,主要用來向 Paging 提供源數據,須要重寫 load 方法,在這個方法進行網絡請求的處理。須要注意的是 LoadResult.Page 裏面的兩個參數 prevKey 和 nextKey,這裏有個坑。
load 方法的參數 LoadParams,它是一個密封類,裏面有三個內部類 Refresh、Append、Prepend。
類名 | 做用 |
---|---|
Refresh | 在初始化刷新的使用 |
Append | 在加載更多的時候使用 |
Prepend | 在當前列表頭部添加數據的時候使用 |
class GitHubRepositoryImpl(
val pageConfig: PagingConfig,
val gitHubApi: GitHubService,
val mapper2Person: Mapper<GithubAccountModel, GitHubAccount>
) : Repository {
override fun postOfData(id: Int): Flow<PagingData<GitHubAccount>> {
return Pager(pageConfig) {
// 加載數據庫的數據
GitHubItemPagingSource(gitHubApi, 0)
}.flow.map { pagingData ->
// 數據映射,數據源 GithubAccountModel ——> 上層用到的 GitHubAccount
pagingData.map { mapper2Person.map(it) }
}
}
}
複製代碼
在 postOfData 方法中構建了一個 Pager, 其構造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory,其中 initialKey、remoteMediator 是可選的,pageConfig 和 pagingSourceFactory 必填的。
pagingSourceFactory 是一個 lambda 表達式,在 Kotlin 中能夠直接用花括號表示,在花括號內,執行執行網絡請求 GitHubItemPagingSource(gitHubApi, 0)
。
最後調用 flow 返回 Flow<PagingData<Value>>
,而後經過 Flow 的 map 方法將數據源 GithubAccountModel 轉換成上層用到的 GithubAccount。
關於 flow 在上一篇 Jetpack 成員 Paging3 實踐以及源碼分析(一) 已經分析過了.
在 ViewModel 接受數據,並傳遞給 Adapter.
val gitHubLiveData: LiveData<PagingData<GitHubAccount>> =
repository.postOfData(0).asLiveData()
複製代碼
LiveData 有三種使用方式,這裏演示的是其中一種,其他的在以前的文章 Jetpack 成員 Paging3 實踐以及源碼分析(一) 已經分析過了。
mMainViewModel.gitHubLiveData.observe(this, Observer { data ->
mAdapter.submitData(lifecycle, data)
})
複製代碼
到這裏請求網絡數據並顯示的在 UI 上就結束了,最後咱們來分析一下 Paging3 內置的錯誤處理支持,包括刷新和重試等功能。
Paging3 提供了內置的錯誤處理支持,包括刷新和重試等功能,說到這裏 Google 對於 Paging3 的設計相比於以前的設計真的好,基本上進行網絡請求地方用 RecyclerView 去展現數據,都須要用到刷新、重試、錯誤處理等等功能。
1. 錯誤處理
Paging3 的組件 PagingDataAdapter,PagingDataAdapter 是一個處理分頁數據的可回收視圖適配器,PagingDataAdapter 提供了三個方法,以下圖所示:
方法名 | 做用 |
---|---|
withLoadStateFooter | 添加列表底部(相似於加載更多) |
withLoadStateHeader | 添加列表的頭部 |
withLoadStateHeaderAndFooter | 添加頭部和底部 |
Paging3 提供了 LoadStateAdapter 用於實現列表底部和頭部樣式,只須要繼承 LoadStateAdapter 作對應的網絡狀態處理便可,例如這裏實現的 FooterAdapter 加載更多樣式。
class FooterAdapter(val adapter: GitHubAdapter) : LoadStateAdapter<NetworkStateItemViewHolder>() {
override fun onBindViewHolder(holder: NetworkStateItemViewHolder, loadState: LoadState) {
holder.bindData(loadState, 0)
}
override fun onCreateViewHolder(
parent: ViewGroup,
loadState: LoadState
): NetworkStateItemViewHolder {
val view = inflateView(parent, R.layout.recycie_item_network_state)
return NetworkStateItemViewHolder(view) { adapter.retry() }
}
private fun inflateView(viewGroup: ViewGroup, @LayoutRes viewType: Int): View {
val layoutInflater = LayoutInflater.from(viewGroup.context)
return layoutInflater.inflate(viewType, viewGroup, false)
}
}
class NetworkStateItemViewHolder(view: View, private val retryCallback: () -> Unit) :
DataBindingViewHolder<LoadState>(view) {
val mBinding: RecycieItemNetworkStateBinding by viewHolderBinding(view)
override fun bindData(data: LoadState, position: Int) {
mBinding.apply {
// 正在加載,顯示進度條
progressBar.isVisible = data is LoadState.Loading
// 加載失敗,顯示並點擊重試按鈕
retryButton.isVisible = data is LoadState.Error
retryButton.setOnClickListener { retryCallback() }
// 加載失敗顯示錯誤緣由
errorMsg.isVisible = !(data as? LoadState.Error)?.error?.message.isNullOrBlank()
errorMsg.text = (data as? LoadState.Error)?.error?.message
executePendingBindings()
}
}
}
複製代碼
在上面分別處理了,正在加載、加載失敗並提供重試按鈕等等狀態。
2. Paging3 同時提供了刷新、重試等等方法,以下圖所示:
3. Paging3 還幫我處理了若是出現屢次網絡請求,只會處理最後一次請求,例如因爲網絡慢,用戶頻繁的刷新數據等等
剛纔分析過 PagingDataAdapter 是一個處理分頁數據的可回收視圖適配器,而且還提供了兩個監聽數據狀態的方法。
這兩個方法的區別是:
CombinedLoadStates 是一個數據類,裏面有三個成員變量 refresh、prepend 和 append。
val refresh: LoadState = (mediator ?: source).refresh
val prepend: LoadState = (mediator ?: source).prepend
val append: LoadState = (mediator ?: source).append
複製代碼
變量 | 做用 |
---|---|
refresh | 在初始化刷新的使用 |
prepend | 在加載更多的時候使用 |
append | 在當前列表頭部添加數據的時候使用 |
refresh、prepend 和 append 都是 LoadState 的對象,LoadState 也是一個密封類,每個 refresh、prepend 和 append 都對應着三種狀態。
變量 | 做用 |
---|---|
Error | 表示加載失敗 |
Loading | 表示正在加載 |
NotLoading | 表示當前未加載 |
到這裏不得不佩服 Google 什麼都替咱們想好了,這裏須要結合本身的項目實際狀況,去定製不一樣的狀態處理。
到這裏 Paging3 算是完結了,最後貼一下本文案例 Paging3SimpleWithNetWork 已經上傳到 GitHub,最後祝你們週末愉快呀。
計劃創建一個最全、最新的 AndroidX Jetpack 相關組件的實戰項目 以及 相關組件原理分析文章,正在逐漸增長 Jetpack 新成員,倉庫持續更新,能夠前去查看:AndroidX-Jetpack-Practice, 若是這個倉庫對你有幫助,請幫我點個贊,我會陸續完成更多 Jetpack 新成員的項目實踐。
致力於分享一系列 Android 系統源碼、逆向分析、算法、翻譯、Jetpack 源碼相關的文章,正在努力寫出更好的文章,若是這篇文章對你有幫助給個 star,一塊兒來學習,期待與你一塊兒成長。
因爲 LeetCode 的題庫龐大,每一個分類都能篩選出數百道題,因爲每一個人的精力有限,不可能刷完全部題目,所以我按照經典類型題目去分類、和題目的難易程度去排序。
每道題目都會用 Java 和 kotlin 去實現,而且每道題目都有解題思路,若是你同我同樣喜歡算法、LeetCode,能夠關注我 GitHub 上的 LeetCode 題解:Leetcode-Solutions-with-Java-And-Kotlin,一塊兒來學習,期待與你一塊兒成長。
正在寫一系列的 Android 10 源碼分析的文章,瞭解系統源碼,不只有助於分析問題,在面試過程當中,對咱們也是很是有幫助的,若是你同我同樣喜歡研究 Android 源碼,能夠關注我 GitHub 上的 Android10-Source-Analysis,文章都會同步到這個倉庫。
目前正在整理和翻譯一系列精選國外的技術文章,不只僅是翻譯,不少優秀的英文技術文章提供了很好思路和方法,每篇文章都會有譯者思考部分,對原文的更加深刻的解讀,能夠關注我 GitHub 上的 Technical-Article-Translation,文章都會同步到這個倉庫。