Jetpack 新成員 Paging3 網絡實踐及原理分析(二)

前言

Google 最近更新了幾個 Jetpack 新成員 Hilt、Paging 三、App Startup 等等。java

在以前的文章裏面分別分析 App Startup 實踐以及原理Paging3 加載本地數據(一)實踐以及原理,若是沒有看過能夠點擊下方地址前去查看:git

今天這邊文章主要來分析 Paging3 加載網絡數據及其原理,利用週末的時間參考 Google 文檔實現了 Paging3 期間也遇到一些坑,會在文中詳細分析,代碼已經上傳到了 GitHub:Paging3SimpleWithNetWorkgithub

經過這篇文章你將學習到如下內容:面試

  • Paging3 是什麼?
  • Paging3 相對以前版本 (Paging一、Paging2) 核心的變化?
  • 關於 Paging 支持的分頁策略?
  • 在項目中如何使用 Paging3 去加載網絡數據?
  • Paging3 網絡異常如何處理?
  • Paging3 如何監聽網絡請求狀態?
  • Paging3 如何進行刷新和重試?

在項目 Paging3SimpleWithNetWork 中用到了 Coil(Kotlin 圖片加載庫)、Databinding(數據綁定)、Anko(主要用來替換替代 XML 使用的方式)、Koin(Kotlin 依賴注入庫)、JDatabinding(基於 Databinding 封裝的組件)、Data Mapper(數據映射)、使用 Composing builds 做爲依賴庫的版本管理、Repository 設計模式、MVVM 架構等等,關於這裏一些技術以前沒有了解過,能夠點擊下面鏈接前往查看。算法

Paging3 是什麼?

Paging 是一個分頁庫,它能夠幫助您從本地存儲或經過網絡加載顯示數據。這種方法使你的 App 更有效地使用網絡帶寬和系統資源。數據庫

Google 推薦使用 Paging 做爲 App 架構的一部分,它能夠很方便的和 Jetpack 組件集成,Paging3 包含了如下功能:編程

  • 在內存中緩存分頁數據,確保您的 App 在使用分頁數據時有效地使用系統資源。
  • 內置刪除重複數據的請求,確保您的 App 有效地使用網絡帶寬和系統資源。
  • 可配置 RecyclerView 的 adapters,當用戶滾動到加載數據的末尾時自動請求數據。
  • 支持 Kotlin 協程和 Flow, 以及 LiveData 和 RxJava。
  • 內置的錯誤處理支持,包括刷新和重試等功能。

Paging3 相對於以前類的職能變化

在 Paging3 以前提供了 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 這三個類,在這三個類中進行數據獲取的操做。設計模式

  • PositionalDataSource:主要用於加載數據有限的數據(加載本地數據庫)
  • ItemKeyedDataSource:主要用來請求網絡數據,它適用於經過當前頁面最後一條數據的 id,做爲下一頁的數據的開始的位置,例如 Github 的 API。
    • 例如地址 https://api.github.com/users?since=0?per_page=30 當 since = 0 時獲取第一頁數據,當前頁面最後一條數據的 ID 是 46。
    • 將 46 做爲開始位置,此時 since = 46,地址變成:https://api.github.com/users?since=46?per_page=30
  • PageKeyedDataSource:也是用來請求網絡數據,它適用於經過頁碼分頁來請求數據。

在 Paging3 以後 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 合併爲一個 PagingSource,全部舊 API 加載方法被合併到 PagingSource 中的單個 load() 方法中。api

abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
複製代碼

這是一個掛起函數,實現這個方法來觸發異步加載,具體實現見下文,另外在 Paging3 中還有如下變化數組

  • LivePagedListBuilder 和 RxPagedListBuilder 合併爲了 Pager。
  • 使用 PagedList.Config 替換 PagingConfig。
  • 使用 RemoteMediator 替換了 PagedList.BoundaryCallback 去加載網絡和本地數據庫的數據。

四步實現 Paging3 加載網絡數據

Google 推薦咱們使用 Paging3 時,在應用程序的三層中操做,以及它們如何協同工做加載和顯示分頁數據,以下圖所示:

咱們接下來按照 Google 推薦的方式開始實現,只須要四步便可實現 Paging3 加載網絡數據,文中只貼出核心代碼,具體實現能夠看 GitHub 上的 Paging3SimpleWithNetWork 項目。

1. 網絡請求部分

這裏選擇使用的是 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
複製代碼

2. 在 Repository 層建立 PagingSource 數據源

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,這裏有個坑

    • prevKey:上一頁,設置爲空就沒有上一頁的效果,這須要注意的是,若是是第一頁須要返回 null,不然會出現屢次請求,我剛開始忽略了,致使首次加載的時候,出現了兩次請求。
    • nextKey:下一頁,設置爲空就沒有加載更多效果,若是後面沒有更多數據設置爲空,即滑動到最後不會在加載數據。
  • load 方法的參數 LoadParams,它是一個密封類,裏面有三個內部類 Refresh、Append、Prepend。

    類名 做用
    Refresh 在初始化刷新的使用
    Append 在加載更多的時候使用
    Prepend 在當前列表頭部添加數據的時候使用

3. 在 Repository 層建立 Pager 和 PagingData

  • Pager:是主要的入口頁面,在其構造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory。
  • PagingData:是分頁數據的容器,它查詢一個 PagingSource 對象並存儲結果。
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 實踐以及源碼分析(一) 已經分析過了.

4. 最後一步,接受數據,並綁定 UI

在 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 內置的錯誤處理支持,包括刷新和重試等功能。

5. 網絡狀態異常的處理

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 同時提供了刷新、重試等等方法,以下圖所示:

  • refresh:經常使用用於下拉更新數據。
  • retry:經常使用於底部更多樣式,當請求網絡失敗的時候,顯示重試按鈕,點擊調用 retry。

3. Paging3 還幫我處理了若是出現屢次網絡請求,只會處理最後一次請求,例如因爲網絡慢,用戶頻繁的刷新數據等等

6. 監聽網路請求狀態

剛纔分析過 PagingDataAdapter 是一個處理分頁數據的可回收視圖適配器,而且還提供了兩個監聽數據狀態的方法。

這兩個方法的區別是:

  • addDataRefreshListener:當一個新的 PagingData 提交併顯示的時候調用。
  • addLoadStateListener:這個方法同 addDataRefreshListener 方法,它們之間的區別是 addLoadStateListener 方法返回了一個 CombinedLoadStates 的對象,如上圖所示。

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 10 源碼分析的文章,瞭解系統源碼,不只有助於分析問題,在面試過程當中,對咱們也是很是有幫助的,若是你同我同樣喜歡研究 Android 源碼,能夠關注我 GitHub 上的 Android10-Source-Analysis,文章都會同步到這個倉庫。

Android 應用系列

精選譯文

目前正在整理和翻譯一系列精選國外的技術文章,不只僅是翻譯,不少優秀的英文技術文章提供了很好思路和方法,每篇文章都會有譯者思考部分,對原文的更加深刻的解讀,能夠關注我 GitHub 上的 Technical-Article-Translation,文章都會同步到這個倉庫。

工具系列

相關文章
相關標籤/搜索