Android Paging庫使用詳解(小結)
Android分頁包能夠更輕易地在RecyclerView里面緩慢且優(yōu)雅地加載數(shù)據(jù).
許多應用從數(shù)據(jù)源消耗數(shù)據(jù), 數(shù)據(jù)源里面有大量的數(shù)據(jù), 但是一次卻只展示一小部分.
分頁包幫助應用觀測和展示大量數(shù)據(jù)的合理數(shù)目的子集. 這個功能有如下幾個優(yōu)勢:
- 數(shù)據(jù)請求消耗更少的網絡帶寬和系統(tǒng)資源.
- 即使在數(shù)據(jù)更新期間, 應用依然對用戶輸入響應迅速.
添加分頁依賴
按照如下代碼添加依賴:
dependencies { def paging_version = "1.0.0" implementation "android.arch.paging:runtime:$paging_version" // alternatively - without Android dependencies for testing testImplementation "android.arch.paging:common:$paging_version" // optional - RxJava support, currently in release candidate implementation "android.arch.paging:rxjava2:1.0.0-rc1" }
備注: 分頁包幫助開發(fā)者在UI的列表容器中順暢地展示數(shù)據(jù), 而不管是使用設備內部的數(shù)據(jù)庫還是從應用后端拉取數(shù)據(jù).
庫架構
分頁庫的核心構件是PagedList類, 它是一個集合, 用于異步加載應用數(shù)據(jù)塊或者數(shù)據(jù)頁. 該類在應用的其它架構之間充當中介.
Data
每一個PagedList實例從DataSource中加載最新的應用數(shù)據(jù). 數(shù)據(jù)從應用后端或者數(shù)據(jù)庫流入PagedList對象. 分頁包支持多樣的應用架構, 包括脫機數(shù)據(jù)庫和與后臺服務器通訊的數(shù)據(jù)庫.
UI
PagedList類通過PagedListAdapter加載數(shù)據(jù)項到RecyclerView里面. 在加載數(shù)據(jù)的時候, 這些類協(xié)同工作, 拉取數(shù)據(jù)并展示內容, 包括預取看不見的內容并在內容改變時加載動畫.
支持不同的數(shù)據(jù)架構
分頁包支持應用架構, 包括應用拉取數(shù)據(jù)的地方是從后臺服務器, 還是本機數(shù)據(jù)庫, 還是兩者的結合.
只有網絡
要展示后臺數(shù)據(jù), 需要使用Retrofit的同步版本, 加載信息到自定義的DataSource對象中.
備注: 分頁包的DataSource對象并沒有提供任何錯誤處理機制, 因為不同的應用需要用不同的方式處理和展示UI錯誤. 如果錯誤發(fā)生了, 順從結果的回調, 然后稍后重試.
只有數(shù)據(jù)庫
要設置RecyclerView觀測本地存儲, 偏向于使用Room持久化庫. 用這種方式, 無論任何時候數(shù)據(jù)庫數(shù)據(jù)插入或者修改, 這些改變會自動地在負責展示這些數(shù)據(jù)的RecyclerView展示出來.
網絡+數(shù)據(jù)庫
在開始觀測數(shù)據(jù)庫之后, 你能夠通過使用PagedList.BoundaryCallback來監(jiān)聽數(shù)據(jù)庫什么時候過期. 之后, 你可能從網絡拉取更多的數(shù)據(jù), 并把它們插入到數(shù)據(jù)庫中. 如果UI正在展示數(shù)據(jù)庫, 以上就是你所需要做的全部.
下面的代碼片斷展示了BoundaryCallback的使用實例:
class ConcertViewModel { fun search(query: String): ConcertSearchResult { val boundaryCallback = ConcertBoundaryCallback(query, myService, myCache) // Error-handling not shown in this snippet. val networkErrors = boundaryCallback.networkErrors } } class ConcertBoundaryCallback( private val query: String, private val service: MyService, private val cache: MyLocalCache ) : PagedList.BoundaryCallback<Concert>() { override fun onZeroItemsLoaded() { requestAndSaveData(query) } override fun onItemAtEndLoaded(itemAtEnd: Concert) { requestAndSaveData(query) } }
處理網絡錯誤
在使用網絡拉取或者分頁的數(shù)據(jù), 而這些數(shù)據(jù)正在使用分頁包展示的時候, 不總是把網絡分為要么"可用"要么"不可能"是很重要的, 因為許多連接是間歇性或者成片的:
- 特定的服務器可能不能響應網絡請求;
- 設備可能聯(lián)接了慢的或者弱的網絡;
應用應該檢查每一個請求是否成功, 并且在網絡不可用的情形下, 盡可能快地恢復. 比如, 你可以為用戶提供一個"重試"按鈕, 如果數(shù)據(jù)沒有刷新成功的話. 如果在數(shù)據(jù)分頁期間發(fā)生錯誤, 最好自動地重新分頁請求.
更新已有應用
如果應用已經從網絡或者數(shù)據(jù)庫消費數(shù)據(jù), 很大可能可以直接升級到分頁庫提供的功能.
自定義分頁解決方案
如果你使用了自定義功能加載數(shù)據(jù)源中的小的數(shù)據(jù)集, 你可以使用PagedList類取代這個邏輯. PagedList類實例提供了內建的連接, 到通用的數(shù)據(jù)源. 這些實例也提供了在應用中引用的RecyclerView的適配器.
使用列表而非分頁加載的數(shù)據(jù)
如果你使用內存里的列表作為UI適配器的后備數(shù)據(jù)結構, 考慮使用PagedList類觀測數(shù)據(jù)更新, 如果列表中數(shù)據(jù)項變得很多的話. PagedList實例既可以使用LiveData<PagedList>也可以使用Observable<List>對UI傳遞數(shù)據(jù)更新, 同時最小化了加載時間和內存使用. 然而, 應用中使用PagedList對象代替List并不要求對UI結構和數(shù)據(jù)更新邏輯作任何改變.
使用CursorAdapter將數(shù)據(jù)cursor與列表視圖聯(lián)系起來
應用也許會使用CursorAdapter將數(shù)據(jù)從Cursor跟ListView連接起來. 在這種情況下, 通常需要從ListView遷移到RecyclerView, 然后使用Room或者PositionalDataSource構件代替Cursor, 當然, 這主要依據(jù)于Cursor實例能否訪問SQLite數(shù)據(jù)庫.
在一些情況下, 比如使用Spinner實例的時候, 你僅僅提供了Adapter本身. 然后一個庫使用了加載進adapter中的數(shù)據(jù), 并展示了數(shù)據(jù). 在這些情況下, 把adapter數(shù)據(jù)類型轉化為LiveData<PagedList>, 之后在嘗試使用將這些數(shù)據(jù)項在UI中填充起來之前, 將這個列表在ArrayAdapter對象中包裹起來.
使用AsyncListUtil異步加載內容
如果你在使用AsyncListUtil對象異步地加載和展示分組信息的話, 分頁包將會使得加載數(shù)據(jù)更加方便:
- 數(shù)據(jù)并不需要定位. 分頁包讓你直接從后臺使用網絡提供的鍵加載數(shù)據(jù).
- 數(shù)據(jù)量太大. 使用分頁包可以將數(shù)據(jù)加載分頁直到沒有任何數(shù)據(jù)留下.
- 更方便地觀測數(shù)據(jù). 分頁包能夠展示應用在可觀測數(shù)據(jù)結構中持有的ViewModel.
數(shù)據(jù)庫例子
使用LiveData觀測分頁數(shù)據(jù)
下面的示例代碼展示了所有一起工作的碎片. 當演唱會事件在數(shù)據(jù)庫中添加, 刪除或者修改的修改的時候, RecyclerView中的內容自動且高效地更新:
@Dao interface ConcertDao { // The Integer type parameter tells Room to use a PositionalDataSource // object, with position-based loading under the hood. @Query("SELECT * FROM user ORDER BY concert DESC") fun concertsByDate(): DataSource.Factory<Int, Concert> } class MyViewModel(concertDao: ConcertDao) : ViewModel() { val concertList: LiveData<PagedList<Concert>> = LivePagedListBuilder( concertDao.concertsByDate(), /* page size */ 20 ).build() } class MyActivity : AppCompatActivity() { public override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) val viewModel = ViewModelProviders.of(this) .get(MyViewModel::class.java!!) val recyclerView = findViewById(R.id.concert_list) val adapter = ConcertAdapter() viewModel.concertList.observe(this, { pagedList -> adapter.submitList(pagedList) }) recyclerView.setAdapter(adapter) } } class ConcertAdapter() : PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) { fun onBindViewHolder(holder: ConcertViewHolder, position: Int) { val concert = getItem(position) if (concert != null) { holder.bindTo(concert) } else { // Null defines a placeholder item - PagedListAdapter automatically // invalidates this row when the actual object is loaded from the // database. holder.clear() } } companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Concert>() { // Concert details may have changed if reloaded from the database, // but ID is fixed. override fun areItemsTheSame(oldConcert: Concert, newConcert: Concert): Boolean = oldConcert.id == newConcert.id override fun areContentsTheSame(oldConcert: Concert, newConcert: Concert): Boolean = oldConcert == newConcert } } }
使用RxJava2觀測分頁數(shù)據(jù)
如果你偏愛使用RxJava2而非LiveData, 那么你可以創(chuàng)建Observable或者Flowable對象:
class MyViewModel(concertDao: ConcertDao) : ViewModel() { val concertList: Flowable<PagedList<Concert>> = RxPagedListBuilder( concertDao.concertsByDate(), /* page size */ 50 ).buildFlowable(BackpressureStrategy.LATEST) }
之后你可以按照如下代碼開始和停止觀測數(shù)據(jù):
class MyActivity : AppCompatActivity() { private lateinit var adapter: ConcertAdapter<Concert> private lateinit var viewModel: MyViewModel private val disposable = CompositeDisposable() public override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) val recyclerView = findViewById(R.id.concert_list) viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java!!) adapter = ConcertAdapter() recyclerView.setAdapter(adapter) } override fun onStart() { super.onStart() disposable.add(viewModel.concertList.subscribe({ flowableList -> adapter.submitList(flowableList) })) } override fun onStop() { super.onStop() disposable.clear() } }
基于RxJava2解決方案的ConcertDao和ConcertAdapter代碼, 和基于LiveData解決方案的代碼是一樣的.
UI構件及其出發(fā)點
將UI和視圖模型聯(lián)接起來
你可以按照如下方式, 將LiveData<PagedList>實例跟PagedListAdapter聯(lián)系起來:
private val adapter = ConcertPagedListAdapter() private lateinit var viewModel: ConcertViewModel override fun onCreate(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this) .get(ConcertViewModel::class.java) viewModel.concerts.observe(this, adapter::submitList) }
當數(shù)據(jù)源提供一個新PagedList實例的時候, activity會將這些對象改善給adapter. PagedListAdapter實現(xiàn), 定義了更新如何計算, 自動地處理分頁和列表不同. 由此, 你的ViewHolder只需要綁定到特定的提供項:
class ConcertPagedListAdapter() : PagedListAdapter<Concert, ConcertViewHolder>( object : DiffUtil.ItemCallback<Concert>() { // The ID property identifies when items are the same. override fun areItemsTheSame(oldItem: Concert, newItem: Concert) = oldItem.id = newItem.id // Use the "==" operator (or Object.equals() in Java-based code) to know // when an item's content changes. Implement equals(), or write custom // data comparison logic here. override fun areContentsTheSame(oldItem: Concert, newItem: Concert) = oldItem.name == newItem.name && oldItem.date == newItem.date } ) { override fun onBindViewHolder(holder: ConcertViewHolder, position: Int) { val concert: Concert? = getItem(position) // Note that "concert" is a placeholder if it's null holder.bind(concert) } }
PagedListAdapter使用PagedList.Callback對象處理分頁加載事件. 當用戶滑動時, PagedListAdapter調用PagedList.loadAround()方法將從DataSource中拉聚攏數(shù)據(jù)項提示提供給基本的PagedList.
備注: PageList是內容不可變的. 這意味著, 盡管新內容能夠被加載到PagedList實例中, 但已加載項一旦加載完成便不能發(fā)生改變. 由此, 如果PagedList中的內容發(fā)生改變, PagedListAdapter對象將會接收到一個包含已更新信息的全新的PagedList.
實現(xiàn)diffing回調
先前的代碼展示了areContentsTheSame()的手動實現(xiàn), 它比較了對象的相關的域. 你也可以使用Java中的Object.equals()方法或者Kotlin中的==操作符. 但是要確保要么實現(xiàn)了對象中的equals()方法或者使用了kotlin中的數(shù)據(jù)對象.
使用不同的adapter類型進行diffing
如果你選擇不從PagedListAdapter繼承--比如你在使用一個提供了自己的adapter的庫的時候--你依然可以通過直接使用AsyncPagedListDiffer對象使用分頁包adapter的diffing功能.
在UI中提供占位符
在應用完成拉取數(shù)據(jù)之前, 如果你想UI展示一個列表, 你可以向用戶展示占位符列表項. RecyclerView通過將列表項臨時地設置為null來處理這個情況.
備注: 默認情況下, 分頁包開啟了占位符行為.
占位符有如下好處:
- 支持scrollbar. PagedList向PagedListAdapter提供了大量的列表項. 這個信息允許adapter繪制一個表示列表已滿的scrollbar. 當新的頁加載時, scrollbar并不會跳動, 因為列表是并不沒有改變它的size.
- 不需要"正在加載"旋轉指針. 因為列表大小已知, 沒必要提醒用戶有更多的數(shù)據(jù)項正在加載. 占位符本身表達了這個信息.
在添加占位符的支持之前, 請牢記以下先置條件:
- 要求集合中數(shù)據(jù)可數(shù). 來自Room持久化庫的DataSource實例能夠高效地計算數(shù)據(jù)項. 然而, 如果你在用自定義本地存儲方案或者只有網絡的數(shù)據(jù)架構, 想了解數(shù)據(jù)集中有多少數(shù)據(jù)項可能代價很高, 甚至不可能.
- 要求adapter負責未加載數(shù)據(jù)項. 你正在使用的adapter或者展示機制來準備填充列表, 需要處理null列表項. 比如, 當將數(shù)據(jù)綁定到ViewHolder的時候, 你需要提供默認值表示未加載數(shù)據(jù).
- 要求數(shù)據(jù)相同數(shù)量的item view. 如果列表項數(shù)目能夠基于內容發(fā)生改變, 比如, 社交網絡更新, 交叉淡入淡出看起來并不好. 在這種情況下, 強烈推薦禁掉占位符.
數(shù)據(jù)構件及其出發(fā)點
構建可觀測列表
通常情況下, UI代碼觀測LiveData<PagedList>對象(或者, 如果你在使用RxJava2, 是Flowable<PagedList>/Observable<PagedList>對象), 這個對象存在于應用的ViewModel中. 這個可觀測對象形成了應用列表數(shù)據(jù)內容和展示的連接.
要創(chuàng)建這么一個可觀測PagedList對象, 需要將DataSource.Factory實例傳給LivePageListBuilder/RxPagedListBuilder對象. 一個DataSource對象對單個PagedList加載分頁. 這個工廠類為內容更新創(chuàng)建PagedList實例, 比如數(shù)據(jù)庫表驗證, 網絡刷新等. Room持久化庫能夠提供DataSource.Factory, 或者自定義.
如下代碼展示了如何在應用的ViewModel類中使用Room的DataSource.Factory構建能力創(chuàng)建新的LiveData<PagedaList>實例:
ConcertDao.kt:
interface ConcertDao { // The Integer type parameter tells Room to use a PositionalDataSource // object, with position-based loading under the hood. @Query("SELECT * FROM concerts ORDER BY date DESC") public abstract DataSource.Factory<Integer, Concert> concertsByDate() }
ConcertViewModel.kt:
// The Integer type argument corresponds to a PositionalDataSource object. val myConcertDataSource : DataSource.Factory<Integer, Concert> = concertDao.concertsByDate() val myPagedList = LivePagedListBuilder(myConcertDataSource, /* page size */ 20) .build()
定義分頁配置
要想為復雜情形更深入地配置LiveData<PagedList>, 你也可以定義自己的分頁配置. 尤其是, 你可以定義如下屬性:
- 頁大小: 每一頁的數(shù)據(jù)量.
- 預取距離: 給定UI中最后可見項, 超過該項之后多少項, 分頁包要嘗試提前提取數(shù)據(jù). 這個值應該比page size大幾倍.
- 占位符展示: 決定了UI是否會為還沒有完成加載的數(shù)據(jù)項展示占位符.
如果你想要對分布包從數(shù)據(jù)庫加載中設置更多的控件, 要像下面的代碼一樣, 傳遞自定義的Executor對象給LivePagedListBuilder:
EventViewModel.kt:
val myPagingConfig = PagedList.Config.Builder() .setPageSize(50) .setPrefetchDistance(150) .setEnablePlaceholders(true) .build() // The Integer type argument corresponds to a PositionalDataSource object. val myConcertDataSource : DataSource.Factory<Integer, Concert> = concertDao.concertsByDate() val myPagedList = LivePagedListBuilder(myConcertDataSource, myPagingConfig) .setFetchExecutor(myExecutor) .build()
選擇正確的數(shù)據(jù)源類型
連接更最好地處理源數(shù)據(jù)結構的數(shù)據(jù)源很重要:
- 如果加載的頁嵌套了之前/之后頁的key的話, 使用PageKeyDataSource. 比如, 比如你正在從網絡中拉取社交媒體博客, 你也許需要傳遞從一次加載向下一次加載的nextPage token.
- 如果需要使用每N項數(shù)據(jù)項的數(shù)據(jù)拉取每N+1項的話, 使用ItemKeyedDataSource. 比如, 你在為一個討論型應用拉取螺紋評論, 你可能需要傳遞最后一條評論的ID來獲取下一條評論的內容.
- 如果你需要從數(shù)據(jù)商店中的任意位置拉取分頁數(shù)據(jù)的話, 使用PositionalDataSource. 這個類支持請求任意位置開始的數(shù)據(jù)集. 比如, 請求也許返回從位置1200開始的20條數(shù)據(jù).
通知數(shù)據(jù)非法
在使用分頁包時, 在表或者行數(shù)據(jù)變得陳腐時, 取決于數(shù)據(jù)層來通知應用的其它層. 要想這么做的話, 需要從DataSource類中調用invalidate()方法.
備注: UI也可以使用"滑動刷新"模式來觸發(fā)數(shù)據(jù)非法功能.
構建自己的數(shù)據(jù)源
如果你使用了自定義的數(shù)據(jù)解決方案, 或者直接從網絡加載數(shù)據(jù), 你可以實現(xiàn)一個DataSource子類. 下面的代碼展示了數(shù)據(jù)源從給定的concert起始時間切斷:
class ConcertTimeDataSource(private val concertStartTime: Date) : ItemKeyedDataSource<Date, Concert>() { override fun getKey(item: Concert) = item.startTime override fun loadInitial( params: LoadInitialParams<Date>, callback: LoadInitialCallback<Concert>) { val items = fetchItems(concertStartTime, params.requestedLoadSize) callback.onResult(items) } override fun loadAfter( params: LoadParams<Date>, callback: LoadCallback<Concert>) { val items = fetchItemsAfter( date = params.key, limit = params.requestedLoadSize) callback.onResult(items) } }
通過創(chuàng)建真實的DataSource.Factory子類, 你之后能夠加載自定義的數(shù)據(jù)到PagedList對象. 下面的代碼展示了如何創(chuàng)建在之前代碼中定義的自定義數(shù)據(jù)源:
class ConcertTimeDataSourceFactory(private val concertStartTime: Date) : DataSource.Factory<Date, Concert>() { val sourceLiveData = MutableLiveData<ConcertTimeDataSource>() override fun create(): DataSource<Date, Concert> { val source = ConcertTimeDataSource(concertStartTime) sourceLiveData.postValue(source) return source } }
考慮內容更新
當你構建可觀測PagedList對象的時候, 考慮一下內容是如何更新的. 如果你直接從Room數(shù)據(jù)庫中加載數(shù)據(jù), 更新會自動地推送到UI上面.
如果你在使用分頁的網絡API, 通常你會有用戶交互, 比如"滑動刷新", 把它作為信號去驗證當前DataSource非法并請求一個新的. 這個行為出行在下面的代碼中:
class ConcertActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... concertViewModel.refreshState.observe(this, Observer { swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING }) swipeRefreshLayout.setOnRefreshListener { concertViewModel.invalidateDataSource() } } }
提供數(shù)據(jù)表現(xiàn)之間的映射
對于DataSource加載的數(shù)據(jù), 分頁包支持基于數(shù)據(jù)項和基于頁的轉換.
下面的代碼中, concert名和日期的聯(lián)合被映射成包含姓名和日期的字符串:
class ConcertViewModel : ViewModel() { val concertDescriptions : LiveData<PagedList<String>> init { val factory = database.allConcertsFactory() .map { concert -> concert.name + " - " + concert.date } concerts = LivePagedListBuilder(factory, 30).build() } } }
如果在數(shù)據(jù)加載之后, 想要包裹, 轉換或者準備item, 這將非常有用. 因為這個工作是在獲取執(zhí)行器中完成的, 你可以在其中執(zhí)行花銷巨大的工作, 比如, 從硬盤中讀取, 查詢數(shù)據(jù)庫等.
備注: JOIN查詢總是比作為map()一部分的查詢要高效.
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Android 圖片切換器(dp、sp、px) 的單位轉換器
這篇文章主要介紹了Android 圖片切換器(dp、sp、px) 的單位轉換器的相關資料,需要的朋友可以參考下2017-03-03Android Studio無法執(zhí)行Java類的main方法問題及解決方法
這篇文章主要介紹了Android Studio無法執(zhí)行Java main方法的問題,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03Android開發(fā)之Activity管理工具類完整示例
這篇文章主要介紹了Android開發(fā)之Activity管理工具類,集合完整實例形式分析了Android操作Activity創(chuàng)建、添加、獲取、移除等相關操作技巧,需要的朋友可以參考下2018-01-01Android自定義View實現(xiàn)QQ音樂中圓形旋轉碟子
這篇文章主要為大家詳細介紹了Android自定義View實現(xiàn)QQ音樂中圓形旋轉碟子,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-09-09