14

整合 Android Paging Library: Part 2

 3 years ago
source link: https://enginebai.com/2019/06/17/android-paging-part2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

整合 Android Paging Library: Part 2

上一篇 Part 1 講解 Paging 單純使用 Remote 當作資料來源後,我們這篇要來講解另一種常見的資料架構:Remote + Local,我們會先從 Remote 拉資料到 Local 端,然後 App 統一使用 Local 當作資料來源,讓我們來看看是如何實作這樣的資料架構。 

Remote + Local 資料架構

首先我們展示如何拉 Remote 資料到 Local 後,再讓 View 統一用 Local 資料顯示,這邊先不含 Paging 整合,只看一般資料如何從 Remote → Local → View 這樣的流程顯示出來,熟悉這流程後,再來看 Paging Library 是如何支援這樣的資料架構。 Remote 就是我們一般常用的 API,而 Local 我們選用 Room 當作資料庫,採用 MVVM + RxJava 這樣的 Reactive 架構,如果你不熟悉 Android Room 資料庫,可以參考我之前寫的 Room 入門介紹

我們沿用 Part 1. 的顯示貼文動態牆作為講解範例,我們會拉訊息回來存到資料庫,然後讓 View 訂閱資料庫的資料。

架構設計

我們會採用 MVVM 當作 App 架構,Room 本身也支援 Paging,所以我們會採用 Paging + RxJava + Room 來實作,架構和資料流如下圖:

藍線呈現資料流訂閱的狀態,View 訂閱 ViewModel,ViewModel 訂閱 Model,Model 觸發 Remote DataSource 去載入資料回來存到 Local DataSource,最後紅線呈現在資料流訂閱 onNext(Post) 方法被觸發得到新資料:

blog_-android-paging-library-part-2_architecture.png

首先是我們的 Entity class,用來儲存 API 以及資料庫的資料類別:

@Entity data class Post( @PrimaryKey @SerializedName("id") val id: String, @SerializedName("sender") @ColumnInfo val sender: String?, @SerializedName("caption") @ColumnInfo val caption: String?, @SerializedName("media") @ColumnInfo val media: String?, @SerializedName("timestamp") @ColumnInfo val timestamp: Long? )

再來是 ApiService 和 Dao:

interface PostApiService { @GET("/feed") fun getFeed(): Single<Response<List<Post>>> }

@Dao interface PostDao { @Insert(onConflict = OnConflictStrategy.IGNORE) fun insert(post: Post): Long

@Update(onConflict = OnConflictStrategy.REPLACE) fun update(post: Post)

@Transaction fun upsert(post: Post) { if (-1L == insert(post)) update(post) }

@Query("SELECT * FROM `post`") fun getPostList(): Observable<List<Post>> }

最後是 Repository,裡面要實作從 Remote 拉資料回來後寫入 Local 的邏輯,這邊我們提供兩個方法,一個是 getFeeds(),這個方法回傳 Observeable 可以讓其他人訂閱資料流,另一個是 fetchFeeds() 這個會去拉 Remote 資料然後寫入 Local,這個方法是回傳 Completable,完成資料更新後就 complete,因為當資料寫到 Local的時候,getFeeds() 就會自動發出資料變更的通知。(Room 幫我們實作了發出資料變更的通知)

interface PostRepository { fun fetchFeeds(): Completable fun getFeeds(): Observable<List<Post>> }

class PostRepositoryImpl : PostRepository {

private val remoteDataSource: PostApiService by inject() private val localDataSource: PostDao by inject()

override fun fetchFeeds(): Completable { return remoteDataSource.fetchFeeds() .flatMapObservable { if (it.isSuccessful) Observable.fromIterable(it.body()) else throw HttpException(it) }.concatMapCompletable { Completable.fromAction { localDataSource.upsert(it) } } }

override fun getFeeds(): Observable<List<Post>> { return localDataSource.getPostList() } }

原理介紹

上述的流程熟悉後,換套用 Paging 就會很快上手,我們會向資料庫要資料,當資料庫有資料的時候,可以直接回傳顯示,當所有資料都已經回傳、沒新的資料可以顯示的時候,就需要向 Remote 拉新分頁的資料回來,寫入資料庫,再讓資料庫回傳給介面顯示。 這就是 Paging 實作 Remote + Local 資料來源的原理。

blog_-android-paging-library-part-2_dataflow.png

那我們要如何實作這樣的流程呢?Paging有一個關鍵元件可以達成: PagedList.BoundaryCallback,這個抽象類別可以實作兩個方法:

  • onZeroItemsLoaded(): 當 Local 完全沒有任何資料時會觸發,這邊通常都是要實作向 Remote 拉第一個分頁資料寫回 Local 的邏輯。
  •  onItemAtEndLoaded():當 Local 的資料都回傳完了,已經沒有更多資料可以顯示的時候會觸發,這邊通常都是實作向 Remote 拉下一頁資料寫回 Local 的邏輯。

介紹完這元件後,讓我們來實作這 Callback 然後整合到我們的上面 Remote → Local → View 實作內。

實作

class PostBoundaryCallback : PagedList.BoundaryCallback<Post>(), KoinComponent {

private val remoteDataSource: PostApiService by inject() private val localDataSource: PostDao by inject() private val httpClient: OkHttpClient by inject() private val gson: Gson by inject()

private var nextPageUrl: String? = null

override fun onZeroItemsLoaded() { super.onZeroItemsLoaded() val response = remoteDataSource.getFeed().execute() if (response.isSuccessful) { nextPageUrl = parseNextPageUrl(response.headers()) val postList: List<Post> = response.body() upsertPostList(postList) } }

override fun onItemAtEndLoaded(itemAtEnd: MessageModel) { super.onItemAtEndLoaded(itemAtEnd) nextPageUrl?.run { val response = httpClient.newCall( Request.Builder() .url(this) .build() ).execute() if (response.isSuccessful) { nextPageUrl = parseNextPageUrl(response.headers()) val listType = object : TypeToken<List<Post>>() {}.type val postList: List<Post> = gson.fromJson(response.body()?.string(), listType) upsertPostList(postList) } } } private fun upsertPostList(postList: List<Post>) { postList.forEach { post -> localDataSource.upsert(post) } } }

再來,要把這個 BoundaryCallback 整合到我們的現有程式內,DAO 和 Repository 都要做相對應的改變。

DAO 要把列表查詢的方法 getPostList() 回傳值改為 DataSource.Factory<String, Post>,Room 底層的實作就已經支援分頁,所以可以直接回傳 DataSource.Factory。

@Dao interface PostDao { @Insert(onConflict = OnConflictStrategy.IGNORE) fun insert(post: Post): Long

@Update(onConflict = OnConflictStrategy.REPLACE) fun update(post: Post)

@Transaction fun upsert(post: Post) { if (-1L == insert(post)) update(post) }

@Query("SELECT * FROM `post`") fun getPostList(): DataSource.Factory<Int, Post> }

Repository 要把 DataSource 換成 Local 的資料來源 ( getPostList(): DataSource.Factory<Int, Post>,然後在 RxPagedListBuilder()裡面把 BoundaryCallback 加入。

interface PostRepository { fun getFeeds(): Observable<PagedList<Post>> }

class PostRepositoryImpl : PostRepository {

private val remoteDataSource: PostApiService by inject() private val localDataSource: PostDao by inject()

private val postBoundaryCallback: postBoundaryCallback by inject()

override fun getFeeds(): Observable<PagedList<Post>> { val dataSource = localDataSource.getPostList() val pagedListConfig = PagedList.Config.Builder() .setPageSize(10) .setPrefetchDistance(4) .build() return RxPagedListBuilder(dataSource, pagedListConfig) .setBoundaryCallback(postBoundaryCallback) .buildObservable() } }

其他 ViewModel 和 View 的部分皆不需要更動,可以直接使用 Remote + Local 的 Paging 資料來源。我們的方法觸發順序和資料流的流向如下:

blog_-android-paging-library-part-2_boundarycallback.png

總結

當我們實作 Paging 資料架構從 Remote 轉換為 Remote + Local 的時候,我們只需要實作 BoundaryCallback、新增 Room DAO 回傳 DataSource.Factory 的方法作為 Local DataSource 的主要資料來源即可,而如果你是採用 MVVM(推薦採用這架構),則 View 和 ViewModel 原本實作的 Paging 邏輯則不需要做任何改動。

5 thoughts on “整合 Android Paging Library: Part 2”

Add yours

  1. 3a1488e4ae641e1a2df315ac2a1d17d1?s=80&d=identicon&r=GKevin

    您好,請問要怎麼做下拉更新?我呼叫了 DataSource.invalidate() 資料也不會變更。

    1. 嗨,Kevin,你的 DataSource 是 Remote only 還是 Remote + Local?? 如果是前者,應該呼叫 `DataSource.invalidate()` 就可以才對,如果是後者,則是要清除 Local DB 的資料後才會觸發 BoundaryCallback 去重新拉資料。

  2. 16748f982064953ea6f4bc34efbaa888?s=80&d=identicon&r=Gmark

    您好, 請問一下 如果call api failed的話, onBoundarycallback 就不會再次呼叫了,就算一直scrolling up/down, 請問該怎麼辦呢(network+db)

    1. 你自己有實作 retry 嗎?如果沒有的話,可以加入自動重試或者 UI 顯示重試按鈕,讓使用者可以自己重試。

  3. You saved my day!!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK