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

本文已受權「玉剛說」微信公衆號獨家發佈android

概述

PagingGoogle在2018年I/O大會上推出的適用於Android原生開發的分頁庫,隨着愈來愈多的開發者着手使用Paging,愈來愈多的問題暴露出來,最直接的一個問題是:git

如何管理列表額外的狀態?github

這樣的需求隨處可見,好比 側滑刪除爲評論點贊 等等:數據庫

本文將闡述:如何管理Paging分頁列表的 狀態,爲什麼這樣設計,以及設計的過程。編程

列表的狀態問題

和市面上其它熱門的分頁庫相比,Paging最大的亮點在於其 將列表分頁加載的邏輯做爲回調函數封裝入 DataSource,開發者在配置完成後,無需經過代碼手動控制分頁的加載,列表會 自動加載 下一頁數據並展現。緩存

這種便利意味着開發者不須要本身持有 數據源 ,大多數時候這使得開發流程更加便利,但總有偶然,好比這樣一個界面:服務器

這種需求家常便飯,其本質是,列表自己展現服務端返回的列表數據以外,還須要 本地控制額外的狀態微信

什麼叫 額外的狀態 ? 咱們先用簡單的一張圖展現沒有額外狀態的情形,這時,列表的全部UI元素都從服務端獲取:網絡

如今咱們將上文Gif中的點贊效果也經過一張圖表示:架構

讀者可能還未認識到兩種業務場景之間的差別性:對於列表的初始化來說,全部UI元素都被服務端返回的數據渲染,每條評論是否已經被點贊,服務端都經過Comment進行了描述。

須要注意的是,在某一刻,用戶發現某個評論很是有趣,所以他選擇對該評論進行了點讚的操做。

在業務代碼中,咱們須要向服務端POST一個點讚的請求,服務端返回了一個200的成功碼,但問題來了,接下來咱們 如何讓列表中的那條評論狀態發生變化(即點讚的icon由灰色變成綠色高亮,已告知用戶點同意功)?

這就引起了文章最開始的那個問題,當列表的狀態發生了變動,如何管理並更新列表?

方案1:再次刷新請求接口

最簡單的方案是再次請求API,每當列表狀態發生了變動,從新拉取評論列表,服務端返回的最新數據中,該評論天然已經被點讚了(即列表正確進行了更新)。

讀者應該清楚,該方案實際並不可行,緣由有二:

  • 成本過高:某些操做對於用戶來講,應該是很是 輕量級 的(好比點贊),他們甚至但願這些操做可以 當即被響應 在UI上 ,而請求API並刷新列表這一個過程過重了,即便不考慮服務器的負擔,對於用戶來講,UI的刷新須要數秒的等待也是很是糟糕的體驗。
  • 不符合邏輯:咱們更須要注意的是,Paging是一個分頁列表,而刷新請求行爲對於分頁列表來講,是一個不符合產品預期的行爲(好比,個人點贊操做是針對第5頁的某個評論執行的,產品的設計不可能容許每次點贊都重置爲列表的第一頁數據,這意味着極度糟糕的用戶體驗)。

如今咱們理解了 每當列表狀態發生了變動就刷新接口 並不是良策,由於這種經過 遠程從新拉取數據源 更新UI的方式成本過高了。

方案2:額外維護一個狀態的列表

大概思路是在內存中爲RecyclerView維護一個額外的List,用於一一映射對應positionItem狀態:

class CommentPagedAdapter(
  private val likedList: ArrayList<Boolean>
)
複製代碼

經過在內存中維護這樣一個List,的確能夠實現需求,但讀者須要認識到的是,Paging分頁庫自己最大的優勢即是 隨着列表的滾動自動加載分頁數據,每次分頁的行爲開發者並不須要手動配置,並經過調用相似notifyItemRangeInserted()的方法更新UI。

很顯然,每當分頁數據獲取後,開發者依然須要手動維護這個額外狀態的List——方案2和選擇使用Paging的初衷背道而馳,所以它並不是最優先考慮的方案。

庫自己設計的問題?

如今問題是,既不能經過 服務端 做爲數據源,也不能在 內存中 額外維護一個狀態的列表, 讀者不免會質疑Paging庫自己設計的問題。

我該如何控制列表額外的狀態(包括修改、增長或者刪除)?

事實上該問題已經在Github的這個 issue 中進行了討論,Google的工程師的回覆是:

從技術的角度而言 ,咱們能夠建立一個容許部分更改數據源的API,但以後咱們須要記錄這些改動並在主線程上從新傳遞給列表。這種方法的問題在於,若是你有一個已中止的RecyclerView(也就是後堆棧),它將不會(也不該該)接收任何更新,所以PagedList將保留這個可能很長的數據列表並從新應用於主線程上的每一個觀察者。

這使問題變得很是複雜,這就是咱們使用單個列表的緣由。

顯然,Paging考慮到了更多,和市面上 什麼都能作 的框架相比,它 勇於收緊開發者API的調用權限,在開發者們發揮更多奇思妙想以前,將其牢牢束縛到了可控制的範圍以內,這也是筆者很是推崇Paging的緣由之一。

那麼咱們該如何處理咱們的業務?此時引入一個新的角色彷佛是一個不錯的選擇,那就是 持久層(即緩存)。

經過架構解決業務問題

綜上所述,對於分頁列表的狀態管理問題,須要作到的是:

  • 1.將一個單獨的List交給Paging去進行分頁加載並渲染(不該在內存中手動維護一個額外狀態的列表);
  • 2.不該該每次都經過重的操做刷新數據源(好比網絡請求刷新接口)。

所以,咱們須要一個 中間件 進行業務的調度——在須要刷新整個數據源的時候(好比用戶的下拉刷新操做),從服務端拉取數據;在不須要繁重的操做時(好比用戶針對某個評論進行點贊),僅僅須要針對單個數據源進行簡單的修改。

這已經不僅僅是業務業務的問題,而且涉及到了項目自己的架構,接下來, 持久層 (即本地緩存)閃亮登場。

1.用持久層做爲惟一的數據源

Android平臺的數據庫框架有不少種,本文以官方的架構組件Room爲例。

爲何要爲項目的架構額外添加一個持久層?事實上,隨着項目體系的日益龐大,數據庫是終究須要添加進入項目中的,所以,在設計項目的架構以前,提早將數據庫的框架配置進來是一個不錯的選擇——未雨綢繆總不是壞事。

以列表的渲染爲例,讓咱們來看看項目以前的結構:

回到本文,對於Paging來說,咱們並沒有法直接獲取數據源,所以對於列表狀態的管理,咱們須要額外的角色幫助,那就是本地的持久化緩存。

讓咱們看看添加了持久層以後的結構:

添加了緩存以後,每當咱們嘗試初始化一個分頁列表,框架會從服務器拉取數據,以後數據被存儲到了Room中。

請注意!Paging原生提供了對Room數據庫框架的支持,所以它老是能夠第一時間響應到數據庫中數據的變化,並自動渲染在UI上

如今,咱們將 請求服務器API數據的渲染 二者經過持久層進行了隔離,對於RecyclerView來講,持久層是惟一的數據源,即:

列表只反應了數據庫的變動。

如今列表的顯示和服務端的請求已經 徹底無關 了,讀者也許會有這樣的疑問——這樣作的好處是什麼?

2.列表狀態的管理

如今咱們回到文中最初的問題,如何管理列表的狀態?

對於一個擁有複雜狀態的分頁列表,不管是 服務端 做爲數據源,仍是在 內存中 額外維護一個狀態列表,都不是很好的選擇;而如今咱們加入了Room,並做爲列表惟一的數據源,局勢發生了怎樣微妙的變化呢?

讓咱們來看看加入了持久層以後,下拉刷新的邏輯發生了怎樣的變化:

  • 1.下拉刷新意味着咱們須要重置數據,所以咱們手動清除了數據庫內對應表中的數據;
  • 2.當表中數據被清空時,Paging會自動響應到數據的變化,由於沒有了數據,因此Paging會自動向服務器請求數據;
  • 3.數據返回後,會再次將數據存儲到數據庫中;
  • 4.這時Paging會再次響應到數據庫的變化,並將最新的數據渲染到UI上。

看起來邏輯複雜了不少,實際上讀者須要明確的是,步驟二、三、4都是咱們做爲開發者在初始化Paging時就配置好的,所以若是用戶須要刷新頁面,只須要進行第一步的操做便可,即相似這樣的一行代碼:

// 刷新操做,僅需清除表內的列表數據
fun swipeRefresh() {
  // 運行一個事務
  db.runInTransaction {
      // 清除列表數據
      db.getDao().clearDataList()
  }
}
複製代碼

如今咱們將整個流程中,Paging自動執行的步驟用紫色標記出來:

瞧,除了咱們手動執行的邏輯,全部流程都交給了Paging響應式 地執行。

咱們老是下意識認爲複雜的業務邏輯用過程式的編碼更容易實現,Paging用事實證實了並不是如此——若是說項目中的某個頁面追加了下拉刷新的需求,過程式的編碼也許會花費更多的時間,而且代碼也許會更分散、囉嗦且易出錯。

3.更靈活、且可高度擴展

接下來分析的是,對分頁列表點贊這種相對 輕量級的行爲 又該如何處理?

答案呼之欲出, 咱們依然用熟悉的流程圖表示代碼的執行步驟:

即便是複雜的狀態,在這種模式下也再也不是難題:首先,咱們將數據庫對應表中對應評論的isLike(是否被點贊)設置爲true

// 1.對本地的評論數據點贊
fun likeCommentLocal(comment: Comment) {
  // 更新評論
  comment.isLike = true
  // 將評論更新到數據庫中
  db.runInTransaction {
     db.getDao().updateLikeComment(o)
  }
}
複製代碼

與此同時,咱們也向服務器請求接口,告知評論被用戶點贊:

// 2.對評論點贊
fun likeCommentRemote(commentId: String) {
  service.likeComment(commentId)
  // ....
}
複製代碼

當數據庫中數據發生了變動,Paging仍然會響應到數據的更新,並第一時間更新了UI,同時咱們也向服務器發起了請求,一個完整的 點贊 操做相關的業務代碼實現完畢。

有了持久層做爲中間件,代碼組織的靈活性大大提高,同時也具有了更高的擴展性。列表狀態的管理再也不是問題,諸如 點贊下拉刷新側滑刪除 等等等等,均可以經過對持久層的數據源進行修改,paging老是能夠第一時間自動響應到變動並更新UI。

也正如Room官方文檔第一句話所說的,對於Paging分頁列表(對app也同樣)複雜的狀態的展現和管理,開發者應該 將緩存做爲列表的惟一真實的數據源

This cache, which serves as your app's single source of truth.

代碼示例?

如讀者所看到的,本文儘可能避免展現大篇幅的業務代碼,緣由有二:

  • 1.這會破壞文章總體思路的完整性,沒有人喜歡閱讀大篇幅、連續的代碼片斷;
  • 2.實際開發中,項目的業務不一樣、架構選型不一樣,代碼的實現方式也不盡相同,所以業務級別的代碼展現沒有意義。

好比,對於持久層框架的選型,RoomGreenDaoDBFlow都是很是優秀的框架,對於業務代碼的實現,RxJavaLiveData、協程都是優秀的實現方案...

本文的目的是闡述筆者遇到問題的解決步驟和思路,讀者瞭解總體的方案以後,能夠根據實際項目進行技術選型。

固然,若是有相關的疑惑,歡迎參考下面兩個項目的具體實現,這是筆者基於上文的Paging+Room組件,實現了一個簡單的Github的客戶端,本文不細述。

1.MVVM架構的Sample: github.com/qingmei2/MV…

2.MVI架構的Sample:github.com/qingmei2/MV…

系列文章

爭取打造 Android Jetpack 講解的最好的博客系列

Android Jetpack 實戰篇


關於我

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

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

相關文章
相關標籤/搜索