Jetpack 成員 Paging3 實踐以及源碼分析(一)

前言

前幾天 Google 更新了幾個 Jetpack 新成員 Hilt、Paging 三、App Startup 等等,在以前的文章裏面分了 App Startup 是什麼、App Startup 爲咱們解決了什麼問題,若是以前沒有看過能夠點擊下面鏈接前往查看文章和代碼。android

今天這邊文章主要來分析 Paging3,並配有完整的項目演示,Paging3 會分爲兩篇文章,去詳細的分析其原理:git

  • 文章一(本文):主要來分析 Paging 3 如何加載本地數據庫,代碼已經上傳到 GitHub,AndroidX-Jetpack-Practice 下面的 Paging3Simplegithub

    GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice面試

  • 文章二:主要來分析 Paging3 如何加載網絡請求,核心主要是針對於不一樣的分頁策略,咱們如何去實現,在 Paging3 以前提供了 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource,Paging3 以後統一爲 PagingSource。算法

經過這篇文章你將學習到如下內容:數據庫

  • Paging3 是什麼?
  • Paging3 在項目中的架構以及類的職能源碼分析?
  • 如何在項目中正確使用 Paging3?
  • 數據映射(Data Mapper)是什麼?
  • Kotlin Flow 是什麼?

在分析以前咱們先來了解一下本文實戰項目中用到的技術:編程

Paging3 是什麼?

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

Paging3 是使用 Kotlin 協程徹底重寫的庫,經歷了從 Paging1x 到 Paging2x 在到如今的 Paging3,深入領悟到 Paging3 比 Paging1 和 Paging2 真的方便了不少。

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

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

Paging3 的架構以及類的職能源碼分析

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

可是我我的認爲應該在增長一層 Data Mapper (下面會有詳細的介紹),以下圖所示:

數據映射(Data Mapper)將數據源的實體,轉換爲上層用到的 model,每每會被咱們忽略掉,可是在項目中起到了很大重要,我看了不少項目的,這個概念不多被說起到,我只在國外的大牛的寫的文章中,它們說起到了這個概念。關於數據映射(Data Mapper) 後面會單獨寫一篇文章,配合 Demo 去驗證,這裏只是簡單說起一下。

Data Mapper

在一個快速開發的項目中,爲了越快完成第一個版本交付,下意識的將數據源和 UI 綁定到一塊兒,當業務逐漸增多,數據源變化了,上層也要一塊兒變化,致使後期的重構工做量很大,核心的緣由耦合性太強了。

使用數據映射(Data Mapper)優勢以下:

  • 數據源的更改不會影響上層的業務。
  • 糟糕的後端實現不會影響上層的業務 (想象一下,若是你被迫執行2個網絡請求,由於後端不能在一個請求中提供你須要的全部信息,你會讓這個問題影響你的整個代碼嗎)。
  • Data Mapper 便於作單元測試,確保不會由於數據源的變化,而影響上層的業務。
  • 在本文案例項目 Paging3Simple 中會用到 Data Mapper 做爲數據映射,在代碼中有詳細的註釋。

Repository layer

在 Repository layer 中的主要使用 Paging3 組件中的 PagingSource,每一個 PagingSource 對象定義一個數據源以及如何從該數據源查找數據, PagingSource 對象能夠從任何一個數據源加載數據,包括網絡數據和本地數據。

PagingSource 是一個抽象類,其中有兩個重要的方法 load 和 和 getRefreshKey,load 方法以下所示:

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

這是一個掛起函數,實現這個方法來觸發異步加載,另一個 getRefreshKey 方法

open fun getRefreshKey(state: PagingState<Key, Value>): Key? = null
複製代碼

該方法只在初始加載成功且加載頁面的列表不爲空的狀況下被調用。

在這一層中還有另一個 Paging3 的組件 RemoteMediator,RemoteMediator 對象處理來自分層數據源的分頁,例如具備本地數據庫緩存的網絡數據源。

ViewModel layer

在 ViewModel layer 層主要用到了 Paging3 的組件 Pager,Pager 是主要的入口頁面,在其構造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory,代碼以下所示:

class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
    config: PagingConfig,
    initialKey: Key? = null,
    @OptIn(ExperimentalPagingApi::class)
    remoteMediator: RemoteMediator<Key, Value>? = null,
    pagingSourceFactory: () -> PagingSource<Key, Value>
)
複製代碼

今天這篇文章和項目主要用到了 PagingConfig 和 PagingSource,PagingSource 上面已經說過了,因此咱們主要來分一下 PagingConfig。

val pagingConfig = PagingConfig(
    // 每頁顯示的數據的大小
    pageSize = 60,

    // 開啓佔位符
    enablePlaceholders = true,

    // 預刷新的距離,距離最後一個 item 多遠時加載數據
    prefetchDistance = 3,

    /**
     * 初始化加載數量,默認爲 pageSize * 3
     *
     * internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
     * val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER
     */
    initialLoadSize = 60,

    /**
     * 一次應在內存中保存的最大數據
     * 這個數字將會觸發,滑動加載更多的數據
     */
    maxSize = 200
)
複製代碼

將 ViewModel 層鏈接到 UI 層用到了 Paging3 的組件 PagingData,PagingData 對象是分頁數據的容器,它查詢一個 PagingSource 對象並存儲結果。

Google 推薦咱們將組件 Pager 放到 ViewModel layer,可是我更喜歡放到 Repository layer,詳見下文。

UI layer

在 UI layer 中的主要到了 Paging3 的組件 PagingDataAdapter,PagingDataAdapter 是一個處理分頁數據的可回收視圖適配器,您可使用 AsyncPagingDataDiffer 組件來構建本身的自定義適配器,本文中用到是 PagingDataAdapter。

Paging 3 如何在項目中使用

在 App 模塊中的 build.gradle 文件中添加如下代碼:

dependencies {
  def paging_version = "3.0.0-alpha01"

  implementation "androidx.paging:paging-runtime:$paging_version"
}
複製代碼

接下來我將按照上面說的每層去實現,首先咱們先來看一下項目的結構。

  • bean: 存放上層須要的 model,會和 RecyclerView 的 Adapter 綁定在一塊兒。
  • loca: 存放和本地數據庫相關的操做。
  • mapper: 數據映射,主要將數據源的實體 轉成上層的 model。
  • repository:主要來處理和數據源相關的操做(本地、網絡、內存中緩存等等)。
  • di: 和依賴注入相關。
  • ui:數據的展現。

數據庫部分

@Dao
interface PersonDao {

    @Query("SELECT * FROM PersonEntity order by updateTime desc")
    fun queryAllData(): PagingSource<Int, PersonEntity>

    @Insert
    fun insert(personEntity: List<PersonEntity>)

    @Delete
    fun delete(personEntity: PersonEntity)
}
複製代碼

關於 Dao 這裏須要解釋一下, queryAllData 方法返回了一個 PagingSource,後面會經過 Pager 轉換成 flow<PagingData<Value>>

Repository 部分

經過 Koin 注入 RepositoryFactory,經過 RepositoryFactory 管理相關的 Repository,RepositoryFactory 代碼以下:

class RepositoryFactory(val appDataBase: AppDataBase) {
    // 傳遞 PagingConfig 和 Data Mapper 
    fun makeLocalRepository(): Repository =
        PersonRepositoryImpl(appDataBase, pagingConfig,Person2PersonEntityMapper(), PersonEntity2PersonMapper())

    val pagingConfig = PagingConfig(
        // 每頁顯示的數據的大小
        pageSize = 60,

        // 開啓佔位符
        enablePlaceholders = true,

        // 預刷新的距離,距離最後一個 item 多遠時加載數據
        prefetchDistance = 3,

        /**
         * 初始化加載數量,默認爲 pageSize * 3
         *
         * internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
         * val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER
         */
        initialLoadSize = 60,

        /**
         * 一次應在內存中保存的最大數據
         * 這個數字將會觸發,滑動加載更多的數據
         */
        maxSize = 200
    )

}
複製代碼

這裏主要是生成 PagingConfig 和 Data Mapper 而後傳遞給 PersonRepositoryImpl,咱們來看一下 PersonRepositoryImpl 相關代碼。

class PersonRepositoryImpl(
    val db: AppDataBase,
    val pageConfig: PagingConfig,
    val mapper2PersonEntity: Mapper<Person, PersonEntity>,
    val mapper2Person: Mapper<PersonEntity, Person>
) : Repository {

    private val mPersonDao by lazy { db.personDao() }

    override fun postOfData(): Flow<PagingData<Person>> {
        return Pager(pageConfig) {
            // 加載數據庫的數據
            mPersonDao.queryAllData()
        }.flow.map { pagingData ->

            // 數據映射,數據庫實體 PersonEntity ——>  上層用到的實體 Person
            pagingData.map { mapper2Person.map(it) }
        }
    }
}
複製代碼

Pager 是主要的入口頁面,在其構造方法中接受 PagingConfig、pagingSourceFactory。

pagingSourceFactory: () -> PagingSource<Key, Value>
複製代碼

pagingSourceFactory 是一個 lambda 表達式,在 Kotlin 中能夠直接用花括號表示,在花括號內,執行加載數據庫的數據的請求。

最後調用 flow 返回 Flow<PagingData<Value>>,而後經過 Flow 的 map 將數據庫實體 PersonEntity 轉換成上層用到的實體 Person。

Flow 庫是在 Kotlin Coroutines 1.3.2 發佈以後新增的庫,也叫作異步流,相似 RxJava 的 Observable,本文主要用到了 Flow 當中的 map 方法進行數據轉換,簡單實例以下所示:

flow{
    for (i in 1..4) {
        emit(i)
    }
}.map {
    it * it
}
複製代碼

到這裏咱們在回過去看,項目中 pagingData.map { mapper2Person.map(it) } 這行代碼,其中 mapper2Person 是咱們本身實現的 Data Mapper,代碼以下所示:

class PersonEntity2PersonMapper : Mapper<PersonEntity, Person> {
    override fun map(input: PersonEntity): Person = Person(input.id, input.name, input.updateTime)
}
複製代碼

數據庫實體 PersonEntity 轉換爲 上層用到的實體 Person。

UI 部分

經過 koin 依賴注入 MainViewModel,並傳遞參數 Repository。

class MainViewModel(val repository: Repository) : ViewModel() {

    // 調用 Flow 的 asLiveData 方法轉爲 LiveData
    val pageDataLiveData3: LiveData<PagingData<Person>> = repository.postOfData().asLiveData()
}
複製代碼

在 Activity 當中註冊 observe,並將數據綁定給 Adapter,以下所示:

mMainViewModel.pageDataLiveData3.observe(this, Observer { data ->
    mAdapter.submitData(lifecycle, data)
})
複製代碼

知識擴充

剛纔咱們調用了 asLiveData 方法轉爲 LiveData,其實還有兩種方法(做爲了解便可)。

方法一

在 LifeCycle 2.2.0 以前使用的方法,使用兩個 LiveData,一個是可變的,一個是不可變的,以下所示:

// 私有的 MutableLiveData 可變的,對內訪問
private val _pageDataLiveData: MutableLiveData<Flow<PagingData<Person>>>
        by lazy { MutableLiveData<Flow<PagingData<Person>>>() }

// 對外暴露不可變的 LiveData,只能查詢
val pageDataLiveData: LiveData<Flow<PagingData<Person>>> = _pageDataLiveData

_pageDataLiveData.postValue(repository.postOfData())
複製代碼
  • 準備一私有的 MutableLiveData,只對內訪問。
  • 對外暴露不可變的 LiveData。
  • 將值賦值給 _pageDataLiveData。

方法二

在 LifeCycle 2.2.0 以後,能夠用更精簡的方法來完成,使用 LiveData 協程構造方法 (coroutine builder)。

val pageDataLiveData2 = liveData {
    emit(repository.postOfData())
}
複製代碼

liveData 協程構造方法提供了一個協程代碼塊,產生的是一個不可變的 LiveData,emit() 方法則用來更新 LiveData 的數據。

最後添加左右滑動刪除功能

調用 recyclerview 封裝好的 ItemTouchHelper 實現 左右滑動刪除 item 功能。

private fun initSwipeToDelete() {

    /**
     * 位於 [androidx.recyclerview.widget] 包下,已經封裝好的控件
     */
    ItemTouchHelper(object : ItemTouchHelper.Callback() {
        override fun getMovementFlags(
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder
        ): Int =
            makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)

        override fun onMove(
            recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
            target: RecyclerView.ViewHolder
        ): Boolean = false

        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
            (viewHolder as PersonViewHolder).mBinding.person?.let {
                // 當 item 左滑 或者 右滑 的時候刪除 item
                mMainViewModel.remove(it)
            }
        }
    }).attachToRecyclerView(rvList)
}
複製代碼

關於 Paging 加載本地數據到這裏就結束了,咱們將在下一篇文章講解如何加載網絡數據,最後上一個效果圖。

wca41-qu1r

總結

這篇文章主要介紹瞭如下內容:

Paging3 是什麼以及它的優勢

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

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

Paging3 的架構以及類的職能源碼分析

  • PagingSource:每一個 PagingSource 對象定義一個數據源以及如何從該數據源查找數據。
  • RemoteMediator:RemoteMediator 對象處理來自分層數據源的分頁,例如具備本地數據庫緩存的網絡數據源。
  • Pager:是主要的入口頁面,在其構造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory。
  • PagingDataAdapter:是一個處理分頁數據的可回收視圖適配器,您可使用 AsyncPagingDataDiffer 組件來構建本身的自定義適配器。

數據映射(Data Mapper)

數據映射(Data Mapper)將數據源的實體,轉換爲上層用到的 model,每每會被咱們忽略掉的,可是在項目中起到了很大重要,使用 數據映射(Data Mapper)優勢以下:

  • 數據源的更改不會影響上層的業務。
  • 糟糕的後端實現不會影響上層的業務 (想象一下,若是你被迫執行2個網絡請求,由於後端不能在一個請求中提供你須要的全部信息,你會讓這個問題影響你的整個代碼嗎)。
  • Data Mapper 便於作單元測試,確保不會由於數據源的變化,而影響上層的業務。
  • 在本文案例項目 Paging3Simple 中會用到 Data Mapper 做爲數據映射。

Kotlin Flow

Flow 庫是在 Kotlin Coroutines 1.3.2 發佈以後新增的庫,也叫作異步流,相似 RxJava 的 Observable,本文主要用到了 flow 當中的 map 方法進行數據轉換,以下面的例子所示:

flow{
    for (i in 1..4) {
        emit(i)
    }
}.map {
    it * it
}
複製代碼

到這裏我相信應該理解了,項目中 pagingData.map { mapper2Person.map(it) } 這行代碼的意思了。

GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

計劃創建一個最全、最新的 AndroidX Jetpack 相關組件的實戰項目 以及 相關組件原理分析文章,正在逐漸增長 Jetpack 新成員,倉庫持續更新,能夠前去查看:AndroidX-Jetpack-Practice

結語

致力於分享一系列 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,文章都會同步到這個倉庫。

工具系列

逆向系列

相關文章
相關標籤/搜索