Android官方架構元件Paging:分頁庫的設計美學
前言
本文已授權 微信公眾號 玉剛說 (@任玉剛)獨家釋出。
我是一個崇尚 開源 的Android開發者,在過去的一段時間裡,我研究了Github上的一些優秀的開源庫,這些庫原始碼中那些 天馬行空 的 設計 和 思想 令我沉醉其中。
在我職業生涯的 伊始,我沒有接觸過 技術大牛, 但是 閱讀原始碼 可以讓我零距離碰撞 全球行業 內 最頂尖工程師們 的思想,我漸漸愛上了 原始碼閱讀。
在感嘆這些 棒極了 的設計方式時,我也嘗試去 模仿 他們的程式碼風格。後來朋友問我程式碼中為什麼有這麼多 設計模式 時,我才發現,單例 ,代理 ,工廠 ,裝飾,Builder ,甚至更多,當初這些書上怎麼也捋不清的設計模式,現在的我正在潛移默化使用它們,這不是 誇張
今年年初,我嘗試開源了一個 靈活可高度定製 的Android圖片選擇框架 。這個庫獲得了部分認可,當然意見和建議也接踵而來,我很快認識到了自己目前能力的不足—— 通過 組合 的方式 將多個優秀的庫封裝在一起 ,並不是就意味著真正擁有了 組織架構 的能力,而自己對於架構的掌握能力,目前還有很多不足之處。
我意識到自己的不足,於是我積極尋找 更多優秀的架構,試圖通過 原始碼 學習更多API之外的一些東西:程式設計思想 和 架構設計 。
很快,我找到了一個很優秀的庫,Paging—— 它同樣做到了 業務層與UI層之間 的隔離,並且,它的設計更為 優秀
Paging Libray
在不久前的Google 2018 I/O大會上,Google正式推出了AndroidJetpack ——這是一套元件、工具和指導,可以幫助開發者構建出色的 Android 應用,AndroidJetpack 隆重推出了一個新的分頁元件:Paging Library。
我嘗試研究了Paging Library,並分享給大家,本文的目標是闡述:
- 1.瞭解並如何使用 Paging
- 2.知道 Paging 中每個類的 職責,並瞭解掌握其 原理
- 3.站在設計者的角度,徹底搞懂 Paging 的 設計思想
本文不是 Paging API 使用程式碼的展示,但通過本文 徹底搞懂
它的原理之後,API的使用也只是 順手拈來。
它是什麼,怎麼用?
一句話概述: Paging 可以使開發者更輕鬆在 RecyclerView 中 分頁載入資料。
1.官方文件
官方文件 永遠是最接近 正確 和 核心理念 的參考資料 —— 在不久之後,本文可能會因為框架本身API的迭代更新而 毫無意義,但官方文件不會,即使在最惡劣的風暴中,它依然是最可靠的 指明燈:
其次,一個好的Demo能夠起到重要的啟發作用, 這裡我推薦這個Sample:
因為剛剛釋出的原因,目前Paging的中文教程 比較匱乏,許多資料的查閱可能需要開發者 自備梯子。
2.分頁效果
在使用之前,我們需要搞明白的是,目前Android裝置中比較主流的兩種 分頁模式,用我的語言概述,大概是:
- 傳統的 上拉載入更多 分頁效果
- 無限滾動 分頁效果
從語義上來講,我的描述有點不太直觀,不瞭解的讀者估計會很迷糊。
舉個例子,傳統的 上拉載入更多 分頁效果,應該類似 淘寶APP 這種,滑到底部,再上拉顯示footer,才會載入資料:
而無限滾動 分頁效果,應該像是 京東APP 這樣,如果我們慢慢滑動,當滑動了一定量的資料(這個閾值一般是資料總數的某個百分比)時,會自動請求載入下一頁的資料,如果我們繼續滑動,到達一定量的資料時,它會繼續載入下一頁資料,直到載入完所有資料——在使用者看來,就好像是一次就加載出所有商品一樣:
很明顯,無限滾動 分頁效果帶來的使用者體驗更好,不僅是京東,包括 知乎 等其它APP,所採用的分頁載入方式都是 無限滾動 的模式,而 Paging 也正是以無限滾動 的分頁模式而設計的庫。
3.Sample展示
我寫了一個Paging的sample,它最終的效果是這樣:
專案結構圖如下,這可以幫你儘快瞭解sample的結構:
我把這個sample的原始碼託管在了我的github上,你可以通過 點我檢視原始碼 。
4.使用Paging
現在你已經對 Paging 的功能有了一定的瞭解,我們可以開始嘗試使用它了。
請注意,本小節旨在簡單闡述Paging入門使用,讀者不應該困惑於Kotlin語法或者Room庫的使用——你只要能看懂基本流程就好了。
因此,我 更建議 讀者 點選進入github,並將Sample程式碼拉下來閱讀,僅僅是閱讀—— 相比Kotlin語法和Room的API使用,理解程式碼的流程 更為重要。
① 在Module下的build.gradle中新增以下依賴:
def room_version = "1.1.0"
def paging_version = "1.0.0"
def lifecycle_version = "1.1.1"
//Paging的依賴
implementation "android.arch.paging:runtime:$paging_version"
//Paging對RxJava2的原生支援
implementation "android.arch.paging:rxjava2:1.0.0-rc1"
//我在專案中使用了Room,這是Room的相關依賴
implementation "android.arch.persistence.room:runtime:$room_version"
kapt "android.arch.persistence.room:compiler:$room_version"
implementation "android.arch.persistence.room:rxjava2:$room_version"
implementation "android.arch.lifecycle:extensions:$lifecycle_version"
② 建立資料來源
我們要展示在list中的資料,主要以 網路請求 和 本地持久化儲存 的方式獲取,本文為了保證簡單,資料來源通過 Room資料庫中 獲得。
建立Student實體類:
@Entity
data class Student(@PrimaryKey(autoGenerate = true) val id: Int,
val name: String)
建立Dao:
@Dao
interface StudentDao {
@Query("SELECT * FROM Student ORDER BY name COLLATE NOCASE ASC")
fun getAllStudent(): DataSource.Factory<Int, Student>
}
建立資料庫:
@Database(entities = arrayOf(Student::class), version = 1)
abstract class StudentDb : RoomDatabase() {
abstract fun studentDao(): StudentDao
companion object {
private var instance: StudentDb? = null
@Synchronized
fun get(context: Context): StudentDb {
if (instance == null) {
instance = Room.databaseBuilder(context.applicationContext,
StudentDb::class.java, "StudentDatabase")
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
ioThread {
get(context).studentDao().insert(
CHEESE_DATA.map { Student(id = 0, name = it) })
}
}
}).build()
}
return instance!!
}
}
}
private val CHEESE_DATA = arrayListOf(
"Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
"Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
"Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese",
"Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell",
"Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc",
"Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss",
"Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon",
"Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase",
"Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese",
"Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy",
"Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille",
"Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore",
"Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)"
)
③ 建立Adapter和ViewHolder
這一步就很簡單了,就像往常一樣,我們建立一個item的layout佈局檔案(已省略,就是一個TextView用於顯示Student的name),同時建立對應的ViewHolder:
class StudentViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.student_item, parent, false)) {
private val nameView = itemView.findViewById<TextView>(R.id.name)
var student: Student? = null
fun bindTo(student: Student?) {
this.student = student
nameView.text = student?.name
}
}
我們的Adapter需要繼承PagedListAdapter類:
class StudentAdapter : PagedListAdapter<Student, StudentViewHolder>(diffCallback) {
override fun onBindViewHolder(holder: StudentViewHolder, position: Int) {
holder.bindTo(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentViewHolder =
StudentViewHolder(parent)
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<Student>() {
override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean =
oldItem == newItem
}
}
}
④ 展示在介面上
我們建立一個ViewModel,它用於承載 與UI無關 業務程式碼:
class MainViewModel(app: Application) : AndroidViewModel(app) {
val dao = StudentDb.get(app).studentDao()
val allStudents = LivePagedListBuilder(dao.getAllStudent(), PagedList.Config.Builder()
.setPageSize(PAGE_SIZE) //配置分頁載入的數量
.setEnablePlaceholders(ENABLE_PLACEHOLDERS) //配置是否啟動PlaceHolders
.setInitialLoadSizeHint(PAGE_SIZE) //初始化載入的數量
.build()).build()
companion object {
private const val PAGE_SIZE = 15
private const val ENABLE_PLACEHOLDERS = false
}
}
最終,在Activity中,每當觀察到資料來源中 資料的變化,我們就把最新的資料交給Adapter去 展示:
class MainActivity : AppCompatActivity() {
private val viewModel by lazy(LazyThreadSafetyMode.NONE) {
ViewModelProviders.of(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = MainViewModel(application) as T
}).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = StudentAdapter()
recyclerView.adapter = adapter
// 將資料的變化反映到UI上
viewModel.allStudents.observe(this, Observer { adapter.submitList(it) })
}
}
到這裡,Paging 最基本的使用就已經講解完畢了。您可以通過執行預覽和示例 基本一致 的效果,如果有疑問,可以點我檢視原始碼 。
從入門到放棄?
閱讀到這裡,我相信不少朋友會有這樣一個想法—— 這個庫看起來感覺好麻煩,我為什麼要用它呢?
我曾經寫過一篇標題很浮誇的部落格:—— 文中我提出了一種使用DataBinding 不需要哪怕一行Java程式碼就能實現列表/多型別列表的方式,但是最後我也提到了,這只是一種思路,這種簡單的方式背後,可能會隱藏著 嚴重耦合 的情況—— “一行程式碼實現XXX” 的庫屢見不鮮,它們看上去很 簡單 ,但是真正做到 靈活,鬆耦合 的庫寥寥無幾,我認為這種方式是有缺陷的。
因此,簡單並不意味著設計思想的優秀,“看起來很麻煩” 也不能成為否認 Paging 的理由,本文不會去闡述 Paging 在實際專案中應該怎麼用,且不說程式碼正確性與否,這種做法本身就會固定一個人的思維。但如果你理解了 Paging本身原理 的話,相信掌握其用法 也就不在話下了。
Paging原理詳解
先上一張圖
這是官方提供的非常棒的原理示意圖,簡單概括一下:
- DataSource: 資料來源,資料的改變會驅動列表的更新,因此,資料來源是很重要的
- PageList: 核心類,它從資料來源取出資料,同時,它負責控制 第一次預設載入多少資料,之後每一次載入多少資料,如何載入等等,並將資料的變更反映到UI上。
- PagedListAdapter: 介面卡,RecyclerView的介面卡,通過分析資料是否發生了改變,負責處理UI展示的邏輯(增加/刪除/替換等)。
1.建立資料來源
我們思考一個問題,將資料作為列表展示在介面上,我們首先需要什麼。
資料來源,是的,在Paging中,它被抽象為 DataSource , 其獲取需要依靠 DataSource 的內部工廠類 DataSource.Factory ,通過create()方法就可以獲得DataSource 的例項:
public abstract static class Factory<Key, Value> {
public abstract DataSource<Key, Value> create();
}
資料來源一般有兩種選擇,遠端伺服器請求 或者 讀取本地持久化資料——這些並不重要,本文我們以Room資料庫為例:
@Dao
interface StudentDao {
@Query("SELECT * FROM Student ORDER BY name COLLATE NOCASE ASC")
fun getAllStudent(): DataSource.Factory<Int, Student>
}
Paging可以獲得 Room的原生支援,因此作為示例非常合適,當然我們更多獲取 資料來源 是通過 API網路請求,其實現方式可以參考官方Sample,本文不贅述。
現在我們建立好了StudentDao,接下來就是展示UI了,在那之前,我們需要配置好PageList。
2.配置PageList
上文我說到了PageList的作用:
- 1.從資料來源取出資料
- 2.負責控制 第一次預設載入多少資料,之後每一次載入多少資料,如何載入 等等
- 3.將資料的變更反映到UI上。
我們仔細想想,這是有必要配置的,因此我們需要初始化PageList:
val allStudents = LivePagedListBuilder(dao.getAllStudent(), PagedList.Config.Builder()
.setPageSize(15) //配置分頁載入的數量
.setEnablePlaceholders(false) //配置是否啟動PlaceHolders
.setInitialLoadSizeHint(30) //初始化載入的數量
.build()).build()
我們按照上面分的三個職責來講:
- 從資料來源取出資料
很顯然,這對應的是 dao.getAllStudent() ,通過資料庫取得最新資料,如果是網路請求,也應該對應API的請求方法,返回值應該是DataSource.Factory型別。
- 進行相關配置
PageList提供了 PagedList.Config 類供我們進行例項化配置,其提供了4個可選配置:
public static final class Builder {
// 省略其他Builder內部方法
private int mPageSize = -1; //每次載入多少資料
private int mPrefetchDistance = -1; //距底部還有幾條資料時,載入下一頁資料
private int mInitialLoadSizeHint = -1; //第一次載入多少資料
private boolean mEnablePlaceholders = true; //是否啟用佔位符,若為true,則視為固定數量的item
}
- 將變更反映到UI上
這個指的是 LivePagedListBuilder,而不是 PagedList.Config.Builder,它可以設定 獲取資料來源的執行緒 和 邊界Callback,但是一般來講可以不用配置,大家瞭解一下即可。
經過以上操作,我們的PageList設定好了,接下來就可以配置UI相關了。
3.配置Adapter
就像我們平時配置 RecyclerView 差不多,我們配置了ViewHolder和RecyclerView.Adapter,略微不同的是,我們需要繼承PagedListAdapter:
class StudentAdapter : PagedListAdapter<Student, StudentViewHolder>(diffCallback) {
//省略 onBindViewHolder() && onCreateViewHolder()
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<Student>() {
override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean =
oldItem == newItem
}
}
}
當然我們還需要傳一個 DifffUtil.ItemCallback 的例項,這裡需要對資料來源返回的資料進行了比較處理, 它的意義是——我需要知道怎麼樣的比較,才意味著資料來源的變化,並根據變化再進行的UI重新整理操作。
ViewHolder的程式碼正常實現即可,不再贅述。
4.監聽資料來源的變更,並響應在UI上
這個就很簡單了,我們在Activity中宣告即可:
val adapter = StudentAdapter()
recyclerView.adapter = adapter
viewModel.allStudents.observe(this, Observer { adapter.submitList(it) })
這樣,每當資料來源發生改變,Adapter就會自動將 新的資料 動態反映在UI上。
分頁庫的設計美學
現在,我簡單瞭解了它的原理,但是還不是很夠—— 正如我前言所說的,從別人的 程式碼設計和思想 中 取長補短,化為己用 ,這才是我的目的。
讓我們回到最初的問題:
看起來很麻煩,那麼我為什麼用這個庫?
我會有這種想法,我為什麼不能把所有功能都封裝到一個 RecyclerView的Adapter裡面呢,它包含 下拉重新整理,上拉載入分頁 等等功能。
原因很簡單,因為這樣做會將 業務層程式碼 和 UI層 混在一起造 耦合 ,最直接就導致了 難以通過程式碼進行單元測試。
UI層 和 業務層 程式碼的隔離是優秀的設計,這樣更便於 測試 ,我們可以從Google官方文件的目錄結構中看到這一點:
接下來,我會嘗試站在設計者的角度,嘗試去理解 Paging 如此設計的原因。
1.PagedListAdapter:基於RecyclerView的封裝
將分頁資料作為List展示在介面上,RecyclerView 是首選,那麼實現一個對應的 PagedListAdapter 當然是不錯的選擇。
Google對 PagedListAdapter 的職責定義的很簡單,僅僅是一個被代理的物件而已,所有相關的資料處理邏輯都委託給了 AsyncPagedListDiffer:
public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
mDiffer.mListener = mListener;
}
public void submitList(PagedList<T> pagedList) {
mDiffer.submitList(pagedList);
}
protected T getItem(int position) {
return mDiffer.getItem(position);
}
@Override
public int getItemCount() {
return mDiffer.getItemCount();
}
public PagedList<T> getCurrentList() {
return mDiffer.getCurrentList();
}
}
當資料來源發生改變時,實際上會通知 AsyncPagedListDiffer 的 submitList() 方法通知其內部儲存的 PagedList 更新並反映在UI上:
//實際上內部儲存了要展示在UI上的資料來源PagedList<T>
public class AsyncPagedListDiffer<T> {
//省略大量程式碼
private PagedList<T> mPagedList;
private PagedList<T> mSnapshot;
}
篇幅所限,我們不討論資料是如何展示的(答案很簡單,就是通過RecyclerView.Adapter的notifyItemChange()相關方法),我們有一個問題更需要去關注:
Paging 未滑到底部便開始載入資料的邏輯 在哪裡?
如果你認真思考,你應該能想到正確的答案,在 getItem() 方法中執行。
public T getItem(int index) {
//省略部分程式碼
mPagedList.loadAround(index); //如果需要,請求更多資料
return mPagedList.get(index); //返回Item資料
}
每當RecyclerView要展示一個新的Item時,理所應當的會通過 getItem() 方法獲取相應的資料,既然如此,為何不在返回最新資料之前,判斷當前的資料來源是否需要 載入下一頁資料 呢?
2.抽象類PagedList: 設計模式的組合美學
我們來看抽象類PagedList.loadAround(index)方法:
public void loadAround(int index) {
mLastLoad = index + getPositionOffset();
loadAroundInternal(index);
mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
tryDispatchBoundaryCallbacks(true);
}
//這個方法是抽象的
abstract void loadAroundInternal(int index);
需要再次重複的是,即使是PagedList,也有很多種不同的 資料分頁策略:
這些不同的 PagedList 在處理分頁邏輯上,可能有不同的邏輯,那麼,作為設計者,應該做到的是,把異同的邏輯抽象出來交給子類實現(即loadAroundInternal方法),而把公共的處理邏輯暴漏出來,並向上轉型交給Adapter(實際上是 AsyncPagedListDiffer)去執行分頁載入的API,也就是loadAround方法。
好處顯而易見,對於Adapter來說,它只需要知道,在我需要請求分頁資料時,呼叫PagedList的loadAround方法即可,至於 是PagedList的哪個子類,內部執行什麼樣的分頁邏輯,Adapter並不關心。
這些PagedList的不同策略的邏輯,是在PagedList.create()方法中進行的處理:
private static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
@Nullable K key) {
if (dataSource.isContiguous() || !config.enablePlaceholders) {
//省略其他程式碼
//返回ContiguousPagedList
return new ContiguousPagedList<>(contigDataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
key,
lastLoad);
} else {
//返回TiledPagedList
return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
notifyExecutor,
fetchExecutor,
boundaryCallback,
config,
(key != null) ? (Integer) key : 0);
}
}
PagedList是一個抽象類,實際上它的作用是 通過Builder例項化PagedList真正的物件:
通過Builder.build()呼叫create()方法,決定例項化哪個PagedList的子類:
public PagedList<Value> build() {
return PagedList.create(
mDataSource,
mNotifyExecutor,
mFetchExecutor,
mBoundaryCallback,
mConfig,
mInitialKey);
}
Builder模式是非常耳熟能詳的設計模式,它的好處是作為API的門面,便於開發者更簡單上手並進行對應的配置。
不同的PagedList對應不同的DataSource,比如:
class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Callback {
ContiguousPagedList(
//請注意這行,ContiguousPagedList內部需要ContiguousDataSource
@NonNull ContiguousDataSource<K, V> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<V> boundaryCallback,
@NonNull Config config,
final @Nullable K key,
int lastLoad) {
//.....
}
abstract class ContiguousDataSource<Key, Value> extends DataSource<Key, Value> {
//......
}
class TiledPagedList<T> extends PagedList<T> implements PagedStorage.Callback {
TiledPagedList(
//請注意這行,TiledPagedList內部需要PositionalDataSource
@NonNull PositionalDataSource<T> dataSource,
@NonNull Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
int position) {
//......
}
}
public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
//......
}
回到create()方法中,我們看到dataSource此時也僅僅是介面型別的宣告:
private static <K, T> PagedList<T> create(
//其實這時候dataSource只是作為DataSource型別的宣告
@NonNull DataSource<K, T> dataSource,
@NonNull Executor notifyExecutor,
@NonNull Executor fetchExecutor,
@Nullable BoundaryCallback<T> boundaryCallback,
@NonNull Config config,
@Nullable K key) {
}
實際上,create方法的作用是,通過將不同的DataSource,作為依賴例項化對應的PagedList,除此之外,還有對DataSource的對應處理,或者Wrapper(再次包裝,詳情請參考原始碼的create方法,篇幅所限本文不再敘述)。
這個過程中,通過Builder,將 多種資料來源(DataSource),多種分頁策略(PagedList) 互相進行組合,並 向上轉型 交給 介面卡(Adapter) ,然後Adapter將對應的功能 委託 給了 代理類的AsyncPagedListDiffer 處理——這之間通過數種設計模式的組合,最終展現給開發者的是一個 簡單且清晰 的API介面去呼叫,其設計的精妙程度,遠非筆者這種普通的開發者所能企及。
3.更多
實際上,筆者上文所敘述的內容只是 Paging 的冰山一角,其原始碼中,還有很多很值得學習的優秀思想,本文無法一一列舉,比如 執行緒的切換(載入分頁資料應該在io執行緒,而反映在介面上時則應該在ui執行緒),再比如庫 對多種響應式資料型別的支援(LiveData,RxJava),這些實用的功能實現,都通過 Paging 優秀的設計,將其複雜的實現封裝了起來,而將簡單的API暴露給開發者呼叫,有興趣的朋友可以去研究一下。
小結&&吐槽
本文的釋出也算是 歷經坎坷—— 初稿完成時間為6月初,但拖了一個月才發出來,原因就是本文寫好之後,本來準備第二天發表,結果第二天早上一起來,發現 這篇花費了3天晚上總結的文章,竟然沒有能成功儲存!
我自認為我內心還算比較強大,但還是差點崩潰,寫過部落格的同學都知道,這樣一篇6k+的部落格,所需要耗費很多心血才能完成。
我重新整理了思路寫了一遍——收穫是有的,再一次翻閱原始碼的同時,我又加深了對Paging設計思想的理解,這個過程很痛苦,但是也值得。
當然,即使如此,筆者水平有限,也難免文中內容有理解錯誤之處,也希望能有朋友不吝賜教,共同討論一起進步。