反思|Android 列表分頁組件Paging的設計與實現:系統概述

前言

本文將對Paging分頁組件的設計和實現進行一個系統總體的概述,強烈建議 讀者將本文做爲學習Paging 閱讀優先級最高的文章,全部其它的Paging中文博客閱讀優先級都應該靠後。git

本文篇幅 較長,總體結構思惟導圖以下:github

1、起源

手機應用中,列表是常見的界面構成元素,而對於Android開發者而言,RecyclerView是實現列表的不二選擇。數據庫

在正式討論Paging和列表分頁功能以前,咱們首先看看對於一個普通的列表,開發者如何經過代碼對其進行建模:api

如圖所示,針對這樣一個簡單 聯繫人界面 的建模,咱們引出3個重要的層級:緩存

1.服務端組件、數據庫、內存

爲何說 服務端組件數據庫 以及 內存 是很是重要的三個層級呢?服務器

首先,開發者爲當前頁面建立了一個ViewModel,並經過成員變量在 內存 中持有了一組聯繫人數據,由於ViewModel組件的緣由,即便頁面配置發生了改變(好比屏幕的旋轉),數據依然會被保留下來。網絡

數據庫 的做用則保證了App即便在離線環境下,用戶依然能夠看到必定的內容——顯然對於上圖中的頁面(聯繫人列表)而言,本地緩存是很是有意義的。數據結構

對於絕大多數列表而言,服務端 每每意味着是數據源,每當用戶執行刷新操做,App都應當嘗試向服務端請求最新的數據,並將最新的數據存入 數據庫,並隨之展現在UI上。架構

一般狀況下,這三個層級並不是同時都是必要的,讀者需正確理解三者各自不一樣的使用場景。app

如今,藉助於 服務端組件數據庫 以及 內存,開發者將數據展現在RecyclerView上,這彷佛已是正解了。

2.問題在哪?

到目前爲止,問題尚未徹底暴露出來。

咱們忽視了一個很是現實的問題,那就是 數據是動態的 ——這意味着,每當數據發生了更新(好比用戶進行了下拉刷新操做),開發者都須要將最新的數據響應在UI上。

這意味着,當某個用戶的聯繫人列表中有10000個條目時,每次數據的更新,都會對全部的數據進行重建——從而致使 性能很是低下,用戶看到的只是屏幕中的幾條聯繫人信息,爲此要從新建立10000個條目?用戶顯然沒法接受。

所以,分頁組件的設計勢在必行。

3.整理需求

3.一、簡單易用

上文咱們談到,UI響應數據的變動,這種狀況下,使用 觀察者模式 是一個不錯的主意,好比LiveDataRxJava甚至自定義一個接口等等,開發者僅須要觀察每次數據庫中數據的變動,並進行UI的更新:

class MyViewModel : ViewModel() {
  val users: LiveData<List<User>>
}
複製代碼

新的組件咱們也但願能擁有一樣的便利,好比使用LiveData或者RxJava,並進行訂閱處理數據的更新—— 簡單易用

3.二、處理更多層級

咱們但願新的組件可以處理多層,咱們但願列表展現 服務器 返回的數據、 或者 數據庫 中的數據,並將其放入UI中。

3.三、性能

新的組件必須保證足夠的快,不作任何不必的行爲,爲了保證效率,繁重的操做不要直接放在UI線程中處理。

3.四、感知生命週期

若是可能,新的組件須要可以對生命週期進行感知,就像LiveData同樣,若是頁面並不在屏幕的可視範圍內,組件不該該工做。

3.五、足夠靈活

足夠的靈活性很是重要——每一個項目都有不一樣的業務,這意味着不一樣的API、不一樣的數據結構,新的組件必須保證可以應對全部的業務場景。

這一點並不是必須,可是對於設計者來講難度不小,這意味着須要將不一樣的業務中的共同點抽象出來,並保證這些設計適用在任何場景中。

定義好了需求,在正式開始設計Paging以前,首先咱們先來回顧一下,普通的列表如何實現數據的動態更新的。

4.普通列表的實現方式

咱們依然經過 聯繫人列表 做爲示例,來描述普通列表 如何響應數據的動態更新

首先,咱們須要定義一個Dao,這裏咱們使用了Room組件用於 數據庫 中聯繫人的查詢:

@Dao
interface UserDao {
  @Query("SELECT * FROM user")
  fun queryUsers(): LiveData<List<User>>
}
複製代碼

這裏咱們返回的是一個LiveData,正如咱們前文所言,構建一個可觀察的對象顯然會讓數據的處理更加容易。

接下來咱們定義好ViewModelActivity:

class MyViewModel(val dao: UserDao) : ViewModel() {
  // 1.定義好可觀察的LiveData
  val users: LiveData<List<User>> = dao.queryUsers()
}

class MyActivity : Activity {
  val myViewModel: MyViewModel
  val adapter: ListAdapter

  fun onCreate(bundle: Bundle?) {
    // 2.在Activity中對LiveData進行訂閱
    myViewModel.users.observe(this) {
      // 3.每當數據更新,計算新舊數據集的差別,對列表進行更新
      adapter.submitList(it)
    }
  }    
}
複製代碼

這裏咱們使用到了ListAdapter,它是官方基於RecyclerView.AdapterAsyncListDiffer封裝類,其內建立了AsyncListDiffer的示例,以便在後臺線程中使用DiffUtil計算新舊數據集的差別,從而節省Item更新的性能。

本文默認讀者對ListAdapter必定了解,若是不是很熟悉,請參考DiffUtilAsyncListDifferListAdapter等相關知識點的文章。

此外,咱們還須要在ListAdapter中聲明DiffUtil.ItemCallback,對數據集的差別計算的邏輯進行補充:

class MyAdapter(): ListAdapter<User, UserViewHolder>(
  object: DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User)
        = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: User, newItem: User)
        = oldItem == newItem   
  }
) {
  // ...
}
複製代碼

That's all, 接下來咱們開始思考,新的分頁組件應該是什麼樣的。

2、分頁組件簡介

1.核心類:PagedList

上文提到,一個普通的RecyclerView展現的是一個列表的數據,好比List<User>,但在列表分頁的需求中,List<User>明顯就不太夠用了。

爲此,Google設計出了一個新的角色PagedList,顧名思義,該角色的意義就是 分頁列表數據的容器

既然有了List,爲何須要額外設計這樣一個PagedList的數據結構?本質緣由在於加載分頁數據的操做是異步的 ,所以定義PagedList的第二個做用是 對分頁數據的異步加載 ,這個咱們後文再提。

如今,咱們的ViewModel如今能夠定義成這樣,由於PagedList也做爲列表數據的容器(就像List<User>同樣):

class MyViewModel : ViewModel() {
  // before
  // val users: LiveData<List<User>> = dao.queryUsers()

  // after
  val users: LiveData<PagedList<User>> = dao.queryUsers()
}
複製代碼

ViewModel中,開發者能夠輕易經過對users進行訂閱以響應分頁數據的更新,這個LiveData的可觀察者是經過Room組件建立的,咱們來看一下咱們的dao:

@Dao
interface UserDao {
  // 注意,這裏 LiveData<List<User>> 改爲了 LiveData<PagedList<User>> 
  @Query("SELECT * FROM user")
  fun queryUsers(): LiveData<PagedList<User>>  
}
複製代碼

乍得一看彷佛理所固然,但實際需求中有一個問題,這裏的定義是模糊不清的——對於分頁數據而言,不一樣的業務場景,所須要的相關配置是不一樣的。那麼什麼是分頁相關配置呢?

最直接的一點是每頁數據的加載數量PageSize,不一樣的項目都會自行規定每頁數據量的大小,一頁請求15個數據仍是20個數據?顯然咱們目前的代碼沒法進行配置,這是不合理的。

2.數據源: DataSource及其工廠

回答這個問題以前,咱們還須要定義一個角色,用來爲PagedList容器提供分頁數據,那就是數據源DataSource

什麼是DataSource呢?它不該該是 數據庫數據 或者 服務端數據, 而應該是 數據庫數據 或者 服務端數據 的一個快照(Snapshot)。

每當Paging被告知須要更多數據:「Hi,我須要第45-60個的數據!」——數據源DataSource就會將當前Snapshot對應索引的數據交給PagedList

可是咱們須要構建一個新的PagedList的時候——好比數據已經失效,DataSource中舊的數據沒有意義了,所以DataSource也須要被重置。

在代碼中,這意味着新的DataSource對象被建立,所以,咱們須要提供的不是DataSource,而是提供DataSource的工廠。

爲何要提供DataSource.Factory而不是一個DataSource? 複用這個DataSource不能夠嗎,固然能夠,可是將DataSource設置爲immutable(不可變)會避免更多的未知因素。

從新整理思路,咱們如何定義Dao中接口的返回值呢?

@Dao
interface UserDao {
  // Int 表明按照數據的位置(position)獲取數據
  // User 表明數據的類型
  @Query("SELECT * FROM user")
  fun queryUsers(): DataSource.Factory<Int, User>
}
複製代碼

返回的是一個數據源的提供者DataSource.Factory,頁面初始化時,會經過工廠方法建立一個新的DataSource,這以後對應會建立一個新的PagedList,每當PagedList想要獲取下一頁的數據,數據源都會根據請求索引進行數據的提供。

當數據失效時,DataSource.Factory會再次建立一個新的DataSource,其內部包含了最新的數據快照(本案例中表明着數據庫中的最新數據),隨後建立一個新的PagedList,並從DataSource中取最新的數據進行展現——固然,這以後的分頁流程都是相同的,無需再次複述。

筆者繪製了一幅圖用於描述三者之間的關係,讀者可參考上述文字和圖片加以理解:

3.串聯二者:PagedListBuilder

迴歸第一小節的那個問題,分頁相關業務如何進行配置?咱們雖然介紹了爲PagedList提供數據的DataSource,但這個問題彷佛仍是沒有獲得解決。

此外,如今Dao中接口的返回值已是DataSource.Factory,而ViewModel中的成員被觀察者則是LiveData<PagedList<User>>類型,如何 將數據源的工廠和LiveData<PagedList>進行串聯

所以咱們還須要定義一個新的角色PagedListBuilder,開發者將 數據源工廠相關配置 統一交給PagedListBuilder,便可生成對應的LiveData<PagedList<User>>:

class MyViewModel(val dao: UserDao) : ViewModel() {
  val users: LiveData<PagedList<User>>

  init {
    // 1.建立DataSource.Factory
    val factory: DataSource.Factory = dao.queryUsers()

    // 2.經過LivePagedListBuilder配置工廠和pageSize, 對users進行實例化
    users = LivePagedListBuilder(factory, 30).build()
  }
}
複製代碼

如代碼所示,咱們在ViewModel中先經過dao獲取了DataSource.Factory,工廠建立數據源DataSource,後者爲PagedList提供列表所須要的數據;此外,另一個Int類型的參數則制定了每頁數據加載的數量,這裏咱們指定每頁數據數量爲30。

咱們成功建立了一個LiveData<PagedList<User>>的可觀察者對象,接下來的步驟讀者得心應手,只不過咱們這裏使用的是PagedListAdapter

class MyActivity : Activity {
  val myViewModel: MyViewModel
  // 1.這裏咱們使用PagedListAdapter
  val adapter: PagedListAdapter

  fun onCreate(bundle: Bundle?) {
    // 2.在Activity中對LiveData進行訂閱
    myViewModel.users.observe(this) {
      // 3.每當數據更新,計算新舊數據集的差別,對列表進行更新
      adapter.submitList(it)
    }
  }    
}
複製代碼

PagedListAdapter內部的實現和普通列表ListAdapter的代碼幾乎徹底相同:

// 幾乎徹底相同的代碼,只有繼承的父類不一樣
class MyAdapter(): PagedListAdapter<User, UserViewHolder>(
  object: DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User)
        = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: User, newItem: User)
        = oldItem == newItem   
  }
) {
  // ...
}
複製代碼

準確的來講,二者內部的實現還有微弱的區別,前者ListAdaptergetItem()函數的返回值是User,然後者PagedListAdapter返回值應該是User?(Nullable),其緣由咱們會在下面的Placeholder部分進行描述。

4.更多可選配置:PagedList.Config

目前的介紹中,分頁的功能彷佛已經實現完畢,但這些在現實開發中每每不夠,產品業務還有更多細節性的需求。

在上一小節中,咱們經過LivePagedListBuilderLiveData<PagedList<User>>進行建立,這其中第二個參數是 分頁組件的配置,表明了每頁加載的數量(PageSize) :

// before
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, 30).build()
複製代碼

讀者應該理解,分頁組件的配置 自己就是抽象的,PageSize並不能徹底表明它,所以,設計者額外定義了更復雜的數據結構PagedList.Config,以描述更細節化的配置參數:

// after
val config = PagedList.Config.Builder()
      .setPageSize(15)              // 分頁加載的數量
      .setInitialLoadSizeHint(30)   // 初次加載的數量
      .setPrefetchDistance(10)      // 預取數據的距離
      .setEnablePlaceholders(false) // 是否啓用佔位符
      .build()

// API發生了改變
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()
複製代碼

對複雜業務配置的API設計來講,建造者模式 顯然是不錯的選擇。

接下來咱們簡單瞭解一下,這些可選的配置分別表明了什麼。

4.1.分頁數量:PageSize

最易理解的配置,分頁請求數據時,開發者老是須要定義每頁加載數據的數量。

4.2.初始加載數量:InitialLoadSizeHint

定義首次加載時要加載的Item數量。

此值一般大於PageSize,所以在初始化列表時,該配置可使得加載的數據保證屏幕能夠小範圍的滾動。

若是未設置,則默認爲PageSize的三倍。

4.3.預取距離:PrefetchDistance

顧名思義,該參數配置定義了列表當距離加載邊緣多遠時進行分頁的請求,默認大小爲PageSize——即距離底部還有一頁數據時,開啓下一頁的數據加載。

若該參數配置爲0,則表示除非明確要求,不然不會加載任何數據,一般不建議這樣作,由於這將致使用戶在滾動屏幕時看到佔位符或列表的末尾。

4.4.是否啓用佔位符:PlaceholderEnabled

該配置項須要傳入一個boolean值以決定列表是否開啓placeholder(佔位符),那麼什麼是placeholder呢?

咱們先來看未開啓佔位符的狀況:

如圖所示,沒有開啓佔位符的狀況下,列表展現的是當前全部的數據,請讀者重點觀察圖片右側的滾動條,當滾動到列表底部,成功加載下一頁數據後,滾動條會從長變短,這意味着,新的條目成功實裝到了列表中。一言以蔽之,未開啓佔位符的列表,條目的數量和PagedList中數據數量是一致的。

接下來咱們看一下開啓了佔位符的狀況:

如圖所示,開啓了佔位符的列表,條目的數量和DataSource中數據的總量是一致的。 這並不意味着列表從DataSource一次加載了大量的數據並進行渲染,全部業務依然交給Paging進行分頁處理。

當用戶滑動到了底部還沒有加載的數據時,開發者會看到還未渲染的條目,這是理所固然的,PagedList的分頁數據加載是異步的,這時對於Item的來講,要渲染的數據爲null,所以開發者須要配置佔位符,當數據未加載完畢時,UI如何進行渲染——這也正是爲什麼上文說到,對於PagedListAdapter來講,getItem()函數的返回值是可空的User?,而不是User

隨着PagedList下一頁數據的異步加載完畢,伴隨着RecyclerView的原生動畫,新的數據會被從新覆蓋渲染到placeholder對應的條目上,就像gif圖展現的同樣。

4.5.關於Placeholder

這裏我專門開一個小節談談關於placeholder,由於這個機制和咱們傳統的分頁業務彷佛有所不一樣,但Google的工程師們認爲在某些業務場景下,該配置確實頗有用。

開啓了佔位符,用戶老是能夠快速的滑動列表,由於列表「持有」了整個數據集,所以不會像未開啓佔位符時,滑動到底部而被迫暫停滾動,直到新的數據的加載完畢才能繼續瀏覽。順暢的操做總比指望以外的阻礙要好得多

此外,開啓了佔位符意味着用戶與 加載指示器 完全告別,相似一個 正在加載更多... 的提示標語或者一個簡陋的ProgressBar效果然的會提高用戶體驗嗎?也許答案是否認的,相比之下,用戶應該更喜歡一個灰色的佔位符,並等待它被新的數據渲染。

但缺點也隨之而來,首先,佔位符的條目高度應該和正確的條目高度一致,在某些需求中,這也許並不符合,這將致使漸進性的動畫效果並不會那麼好。

其次,對於開發者而言,開啓佔位符意味着須要對ViewHolder進行額外的代碼處理,數據爲null或者不爲null?兩種狀況下的條目渲染邏輯都須要被添加。

最後,這是一個限制性的條件,您的DataSource數據源內部的數據數量必須是肯定的,好比經過Room從本地獲取聯繫人列表;而當數據經過網絡請求獲取的話,這時數據的數量是不肯定的,不開啓Placeholder反而更好。

5.更多觀察者類型的配置

在本文的示例中,咱們創建了一個LiveData<PagedList<User>>的可觀察者對象供用戶響應數據的更新,實際上組件的設計應該面向提供對更多優秀異步庫的支持,好比RxJava

所以,和LivePagedListBuilder同樣,設計者還提供了RxPagedListBuilder,經過DataSource數據源和PagedList.Config以構建一個對應的Observable:

// LiveData support
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()

// RxJava support
val users: Observable<PagedList<User>> = RxPagedListBuilder(factory, config).buildObservable()
複製代碼

3、工做流程原理概述

Paging幕後是如何工做的?

接下來,筆者將針對Paging分頁組件的工做流程進行系統性的描述,探討Paging如何實現異步分頁數據的加載和響應 的。

爲了便於理解,筆者將整個流程拆分爲三個步驟,併爲每一個步驟繪製對應的一張流程圖,這三個步驟分別是:

  • 1.初次建立流程
  • 2.UI渲染和分頁加載流程
  • 3.刷新數據源流程

1.初次建立流程

如圖所示,咱們定義了ViewModelRepositoryRepository內部實現了App的數據加載的邏輯,而其左側的ViewModel則負責與UI組件的通訊。

Repository負責爲ViewModel中的LiveData<PagedList<User>>進行建立,所以,開發者須要建立對應的PagedList.Config分頁配置對象和DataSource.Factory數據源的工廠,並經過調用LivePagedListBuilder相關的API建立出一個LiveData<PagedList<User>>

LiveData一旦被訂閱,Paging將會嘗試建立一個PagedList,同時,數據源的工廠DataSource.Factory也會建立一個DataSource,並交給PagedList持有該DataSource

這時候PagedList已經被成功的建立了,可是此時的PagedList內部只持有了一個DataSource,卻並無持有任何數據,這意味着觀察者角色的UI層即將接收到一個空數據的PagedList

這沒有任何意義,所以咱們更但願PagedList第一次傳遞到UI層級的同時,已經持有了初始的列表數據(即InitialLoadSizeHint);所以,Paging嘗試在後臺線程中經過DataSourcePagedList內部的數據列表進行初始化。

如今,PagedList第一次建立完畢,並持有屬於本身的DataSource和初始的列表數據,經過LiveData這個管道,即將向UI層邁出屬於本身的第一個腳印。

2.UI渲染和分頁加載流程

經過內部線程的切換,PagedList從後臺線程切換到了UI線程,經過LiveData抵達了UI層級,也就是咱們一般說的Activity或者Fragment中。

讀者應該有印象,在上文的示例代碼中,Activity觀察到PagedList後,會經過PagedListAdapter.submitList()函數將PagedList進行注入。PagedListAdapter第一次接收到PagedList後,就會對UI進行渲染。

當用戶嘗試對屏幕中的列表進行滾動時,咱們接收到了須要加載更多數據的信號,這時,PagedList在內部主動觸發數據的加載,數據源提供了更多的數據,PagedList接收到以後將會主動觸發RecyclerView的更新,用戶經過RecyclerView原生動畫觀察到了更多的列表Item

3.刷新數據源流程

當數據發生了更新,Paging幕後又作了哪些工做呢?

正如前文所說,數據是動態的, 假設用戶經過操做添加了一個聯繫人,這時數據庫中的數據集發生了更新。

所以,這時屏幕中RecyclerView對應的PagedListDataSource已經沒有失效了,由於DataSource中的數據是以前數據庫中數據的快照,數據庫內部進行了更新,PagedList從舊的DataSource中再取數據毫無心義。

所以,Paging組件接收到了數據失效的信號,這意味着生產者須要從新構建一個PagedList,所以DataSource.Factory再次提供新版本的數據源DataSource V2——其內部持有了最新數據的快照。

在建立新的PagedList的時候,針對PagedList內部的初始化須要慎重考慮,由於初始化的數據須要根據用戶當前屏幕中所在的位置(position)進行加載。

經過LiveDataUI層級再次觀察到了新的PagedList,並再次經過submitList()函數注入到PagedListAdapter中。

和初次的數據渲染不一樣,這一次咱們使用到了PagedListAdapter內部的AsyncPagedListDiffer對兩個數據集進行差別性計算——這避免了notifyDataSetChanged()的濫用,同時,差別性計算的任務被切換到了後臺線程中執行,一旦計算出差別性結果,新的PagedList會替換舊的PagedList,並對列表進行 增量更新

4、DataSource數據源簡介

Paging分頁組件的設計中,DataSource是一個很是重要的模塊。顧名思義,DataSource<Key, Value>中的Key對應數據加載的條件,Value對應數據集的實際類型, 針對不一樣場景,Paging的設計者提供了三種不一樣類型的DataSource抽象類:

  • PositionalDataSource<T>
  • ItemKeyedDataSource<Key, Value>
  • PageKeyedDataSource<Key, Value>

接下來咱們分別對其進行簡單的介紹。

本章節涉及的知識點很是重要,但不做爲本文的重點,筆者將在該系列的下一篇文章中針對DataSource的設計與實現進行更細節的探究,歡迎關注。

1.PositionalDataSource

PositionalDataSource<T>是最簡單的DataSource類型,顧名思義,其經過數據所處當前數據集快照的位置(position)提供數據。

PositionalDataSource<T>適用於 目標數據總數固定,經過特定的位置加載數據,這裏KeyInteger類型的位置信息,而且被內置固定在了PositionalDataSource<T>類中,T即數據的類型。

最容易理解的例子就是本文的聯繫人列表,其全部的數據都來自本地的數據庫,這意味着,數據的總數是固定的,咱們老是能夠根據當前條目的position映射到DataSource中對應的一個數據。

PositionalDataSource<T>也正是Room幕後實現的功能,使用Room爲何能夠避免DataSource的配置,經過dao中的接口就能返回一個DataSource.Factory

來看Room組件配置的dao對應編譯期生成的源碼:

// 1.Room自動生成了 DataSource.Factory
@Override
 public DataSource.Factory<Integer, Student> getAllStudent() {
   // 2.工廠函數提供了PositionalDataSource
   return new DataSource.Factory<Integer, Student>() {
     @Override
     public PositionalDataSource<Student> create() {
       return new PositionalDataSource<Student>(__db, _statement, false , "Student") {
         // ...
       };
     }
   };
 }
複製代碼

2.ItemKeyedDataSource

ItemKeyedDataSource<Key, Value>適用於目標數據的加載依賴特定條目的信息,好比須要根據第N項的信息加載第N+1項的數據,傳參中須要傳入第N項的某些信息時。

一樣拿聯繫人列表舉例,另外的一種分頁加載方式是經過上一個聯繫人的name做爲Key請求新一頁的數據,由於聯繫人name字母排序的緣由,DataSource很容易針對一個name檢索並提供接下來新一頁的聯繫人數據——好比根據Alice找到下一個用戶BobA -> B)。

3.PageKeyedDataSource

更多的網絡請求API中,服務器返回的數據中都會包含一個String類型相似nextPage的字段,以表示當前頁數據的下一頁數據的接口(好比GithubAPI),這種分頁數據加載的方式正是PageKeyedDataSource<Key, Value>的拿手好戲。

這是平常開發中用到最多的DataSource類型,和ItemKeyedDataSource<Key, Value>不一樣的是,前者的數據檢索關係是單個數據與單個數據之間的,後者則是每一頁數據和每一頁數據之間的。

一樣拿聯繫人列表舉例,這種分頁加載方式是按照頁碼進行數據加載的,好比一次請求15條數據,服務器返回數據列表的同時會返回下一頁數據的url(或者頁碼),藉助該參數請求下一頁數據成功後,服務器又回返回下下一頁的url,以此類推。

總的來講,DataSource針對不一樣種數據分頁的加載策略提供了不一樣種的抽象類以方便開發者調用,不少狀況下,一樣的業務使用不一樣的DataSource都可以實現,開發者按需取用便可。

5、最佳實踐

如今讀者對多種不一樣的數據源DataSource有了簡單的瞭解,先拋開 分頁列表 的業務不談,咱們思考另一個問題:

當列表的數據經過多個層級 網絡請求Network) 和 本地緩存Database)進行加載該怎麼處理?

回答這個問題,須要先思考另一個問題:

Network+Database的解決方案有哪些優點?

1.優點

讀者認真思考可得,Network+Database的解決方案優勢以下:

  • 1.很是優秀的離線模式支持,即便用戶設備並無連接網絡,本地緩存依然能夠帶來很是不錯的使用體驗;
  • 2.數據的快速恢復,若是異常致使App的終止,本地緩存能夠對頁面數據進行快速恢復,大幅減小流量的損失,以及加載的時間。
  • 3.二者的配合的效果老是相得益彰。

看起來Network+Database是一個很是不錯的數據加載方案,那麼爲何大多數場景並無使用本地緩存呢?

主要緣由是開發成本——本地緩存的搭建老是須要額外的代碼,不只如此,更重要的緣由是,數據交互的複雜性也會致使額外的開發成本

2.複雜的交互模型

爲何說Network+Database會致使 數據交互的複雜性

讓咱們回到本文的 聯繫人列表 的示例中,這個示例中,全部聯繫人數據都來自 本地緩存,所以讀者能夠很輕易的構建出該功能的總體結構:

如圖所示,ViewModel中的數據老是由Database提供,若是把數據源從Database換成Network,數據交互的模型也並無什麼區別—— 數據源老是單一的

那麼,當數據的來源不惟一時——即Network+Database的數據加載方案中會有哪些問題呢?

咱們來看看常規的實現方案的數據模型:

如圖所示,ViewModel嘗試加載數據時,老是會先進行網絡判斷,若網絡未鏈接,則展現本地緩存,不然請求網絡,而且在網絡請求成功時,將數據保存本地。

乍得一看,這種方案彷佛並無什麼問題,實際上卻有兩個很是大的弊端:

2.1 業務並不是這麼簡單

首先,經過一個boolean類型的值就能表明網絡鏈接的狀態嗎?顯而易見,答案是否認的。

實際上,在某些業務場景下,服務器的鏈接狀態能夠是更爲複雜的,好比接收到了部分的數據包?好比某些狀況下網絡請求錯誤,這時候是否須要從新展現本地緩存?

若涉及到網絡請求的重試則更復雜,成功展現網絡數據,再次失敗展現緩存——業務愈來愈複雜,咱們甚至會逐漸沉浸其中沒法自拔,最終醒悟,這種數據的交互模型徹底不夠用了

2.2 無用的本地緩存

另一個很明顯的弊端則是,當網絡鏈接狀態良好的時候,用戶看到的數據老是服務器返回的數據。

這種狀況下,請求的數據再次存入本地緩存彷佛毫無心義,由於網絡環境的通暢,Database中的緩存歷來未做爲數據源被展現過。

3.使用單一數據源

使用 單一數據源 (single source of truth)的好處不言而喻,正如上文所闡述的,多個數據源 反而會將業務邏輯變得愈來愈複雜,所以,咱們設計出這樣的模型:

ViewModel若是響應Database中的數據變動,且Database做爲惟一的數據來源?

其思路是:ViewModel只從Database中取得數據,當Database中數據不夠時,則向Server請求網絡數據,請求成功,數據存入DatabaseViewModel觀察到Database中數據的變動,並更新到UI中。

這彷佛沒法知足上文中的需求?讀者認真思考可知,實際上是沒問題的,當網絡鏈接發生故障時,這時向服務端請求數據失敗,並不會更新Database,所以UI展現的正是指望的本地緩存。

ViewModel僅僅響應Database中數據的變動,這種使用 單一數據源 的方式讓複雜的業務邏輯簡化了不少。

4.分頁列表的最佳實踐

如今咱們理解了 單一數據源 的好處,該方案在分頁組件中也一樣適用,咱們惟一須要實現的是,如何主動觸發服務端數據的請求?

這是固然的,由於Database中依賴網絡請求成功以後的數據存儲更新,不然列表所展現的永遠是Database中不變的數據——別忘了,ViewModelServer之間並無任何關係。

針對Database中的數據更新,簡單的方式是 直接進行網絡請求,這種方式使用很是廣泛,好比,列表須要下拉刷新,這時主動請求網絡,網絡請求成功後將數據存入數據庫便可,這時ViewModel響應到數據庫中的更新,並將最新的數據更新在UI上。

另一種方式則和Paging分頁組件自己有關,當列表滾動到指定位置,須要對下一頁數據進行加載時,如何向網絡拉取最新數據?

Paging爲此提供了BoundaryCallback類用於配置分頁列表自動請求分頁數據的回調函數,其做用是,當數據庫中最後一項數據被加載時,則會調用其onItemAtEndLoaded函數:

class MyBoundaryCallback(
    val database : MyLocalCache
    val apiService: ApiService
) : PagedList.BoundaryCallback<User>() {

    override fun onItemAtEndLoaded(itemAtEnd: User) {
      // 請求網絡數據,並更新到數據庫中
      requestAndAppendData(apiService, database, itemAtEnd)
    }
}
複製代碼

BoundaryCallback類爲Paging經過Network+Database進行分頁加載的功能完成了最後一塊拼圖,如今,分頁列表全部數據都來源於本地緩存,而且複雜的業務實現起來也足夠靈活。

5.更多優點

經過Network+Database進行Paging分頁加載還有更多好處,好比更輕易管理分頁列表 額外的狀態

不只僅是分頁列表,這種方案使得全部列表的 狀態管理 的更加容易,筆者爲此撰寫了另一篇文章去闡述它,篇幅所限,本文不進行展開,有興趣的讀者能夠閱讀。

Android官方架構組件Paging-Ex:列表狀態的響應式管理

6、總結

本文對Paging進行了系統性的概述,最後,Paging究竟是一個什麼樣的分頁庫?

首先,它支持NetworkDatabase或者二者,經過Paging,你能夠輕鬆獲取分頁數據,並直接更新在RecyclerView中。

其次,Paging使用了很是優秀的 觀察者模式 ,其簡單的API的內部封裝了複雜的分頁邏輯。

第三,Paging靈活的配置和強大的支持——不一樣DataSource的數據加載方式、不一樣的響應式庫的支持(LiveDataRxJava)等等,Paging老是可以勝任分頁數據加載的需求。


更多 & 參考

再次重申,強烈建議 讀者將本文做爲學習Paging 閱讀優先級最高的文章,全部其它的Paging中文博客閱讀優先級都應該靠後。

——是由於本文的篇幅較長嗎?(1w字的確...)不止如此,本文嘗試對Paging的總體結構進行拆分,筆者認爲,只要對總體結構有足夠的理解,一切API的調用都垂手可得。但若是直接上手寫代碼的話,反而容易形成 只見樹木,不見森林 之感,上手效率反而下降。

此外,本文附帶一些學習資料,供讀者參考:

1.參考視頻

本文的大綱來源於 Google I/O '18中對Paging的一個視頻分享,講的很是精彩,本文絕大多數內容和靈感也是由此而來,強烈建議讀者觀看。

2.參考文章

其實也就是筆者去年寫的幾篇關於Paging的文章:


關於我

Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 Github

若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章
相關標籤/搜索