前幾天 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。算法
經過這篇文章你將學習到如下內容:數據庫
在分析以前咱們先來了解一下本文實戰項目中用到的技術:編程
使用 Koin 做爲依賴注入,能夠看我以前寫的篇文章:[譯][2.4K Star] 放棄 Dagger 擁抱 Koin。後端
使用 Composing builds 做爲依賴庫的版本管理,能夠看我以前寫篇文章:再見吧 buildSrc, 擁抱 Composing builds 提高 Android 編譯速度。設計模式
JDataBinding 是我基於 DataBinding 封裝的庫,能夠看我以前寫篇文章:項目中封裝 Kotlin + Android Databinding。數組
數據映射(Data Mapper): 將數據源的實體,轉換爲上層用到的 model,在項目中起到了很大重要,我看了不少項目的,這個概念不多被說起到,看國外的大牛的寫的文章時,它們說起到了這個概念,後面會對它詳細的分析。
項目中用到了一些 Kotlin 技巧,能夠查看我另一篇文章:爲數很少的人知道的 Kotlin 技巧以及 原理解析。
還有 Paging 三、Room、Anko、Repository 設計模式、MVVM 架構等等。
Paging 是一個分頁庫,它能夠幫助您從本地存儲或經過網絡加載顯示數據。這種方法使你的 App 更有效地使用網絡帶寬和系統資源。
Paging3 是使用 Kotlin 協程徹底重寫的庫,經歷了從 Paging1x 到 Paging2x 在到如今的 Paging3,深入領悟到 Paging3 比 Paging1 和 Paging2 真的方便了不少。
Google 推薦使用 Paging 做爲 App 架構的一部分,它能夠很方便的和 Jetpack 組件集成,Paging3 包含了如下功能:
Google 推薦咱們使用 Paging3 時,在應用程序的三層中操做,以及它們如何協同工做加載和顯示分頁數據,以下圖所示:
可是我我的認爲應該在增長一層 Data Mapper (下面會有詳細的介紹),以下圖所示:
數據映射(Data Mapper)將數據源的實體,轉換爲上層用到的 model,每每會被咱們忽略掉,可是在項目中起到了很大重要,我看了不少項目的,這個概念不多被說起到,我只在國外的大牛的寫的文章中,它們說起到了這個概念。關於數據映射(Data Mapper) 後面會單獨寫一篇文章,配合 Demo 去驗證,這裏只是簡單說起一下。
在一個快速開發的項目中,爲了越快完成第一個版本交付,下意識的將數據源和 UI 綁定到一塊兒,當業務逐漸增多,數據源變化了,上層也要一塊兒變化,致使後期的重構工做量很大,核心的緣由耦合性太強了。
使用數據映射(Data Mapper)優勢以下:
在 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 層主要用到了 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 中的主要到了 Paging3 的組件 PagingDataAdapter,PagingDataAdapter 是一個處理分頁數據的可回收視圖適配器,您可使用 AsyncPagingDataDiffer 組件來構建本身的自定義適配器,本文中用到是 PagingDataAdapter。
在 App 模塊中的 build.gradle 文件中添加如下代碼:
dependencies {
def paging_version = "3.0.0-alpha01"
implementation "androidx.paging:paging-runtime:$paging_version"
}
複製代碼
接下來我將按照上面說的每層去實現,首先咱們先來看一下項目的結構。
@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>>
。
經過 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。
經過 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())
複製代碼
方法二
在 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 加載本地數據到這裏就結束了,咱們將在下一篇文章講解如何加載網絡數據,最後上一個效果圖。
這篇文章主要介紹瞭如下內容:
Paging3 是什麼以及它的優勢
Paging 是一個分頁庫,它能夠幫助您從本地存儲或經過網絡加載和顯示數據。這種方法使你的 App 更有效地使用網絡帶寬和系統資源,而 Paging3 是使用 Kotlin 協程徹底重寫的庫:
Paging3 的架構以及類的職能源碼分析
數據映射(Data Mapper)
數據映射(Data Mapper)將數據源的實體,轉換爲上層用到的 model,每每會被咱們忽略掉的,可是在項目中起到了很大重要,使用 數據映射(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 源碼,能夠關注我 GitHub 上的 Android10-Source-Analysis,文章都會同步到這個倉庫。
目前正在整理和翻譯一系列精選國外的技術文章,不只僅是翻譯,不少優秀的英文技術文章提供了很好思路和方法,每篇文章都會有譯者思考部分,對原文的更加深刻的解讀,能夠關注我 GitHub 上的 Technical-Article-Translation,文章都會同步到這個倉庫。