Android_Jetpack:Paging元件之BoundaryCallback的使用

博麗芙蘭發表於2020-11-28

Paging元件除了單純地支援網路、資料庫為資料來源外,還支援網路+資料庫的架構方式,這就用到了BoundaryCallback。本文我們會使用PositionalDataSource方式載入資料,來簡化多資料來源應用的複雜度。

BoundaryCallback的使用流程如下:
在這裡插入圖片描述

通過流程圖可知,資料庫是頁面的唯一資料來源:頁面訂閱了資料庫的變化,當資料庫中的資料發生變化時,會直接反映到頁面上。

  • 若資料庫中沒有資料,會通知BoundaryCallback中得到onZeroItemsLoaded方法;若資料庫中有資料,則當使用者滑動到RecyclerView底部時,且資料庫中的資料全部載入完畢時,會通知BoundaryCallback中的OnItemAtEndLoad方法。
  • 當BoundaryCallback中的回撥方法被呼叫時,需要在該方法內開啟工作執行緒請求網路資料。
  • 當網路資料成功載入回來,並不直接展示資料,而是將其寫入資料庫。
  • 由於已經設定好了頁面對資料庫的訂閱,當資料庫有新資料寫入時,會自動更新到頁面。
  • 當需要重新整理資料時,可以通過頁面下拉重新整理功能在下拉過程中情況資料。當資料庫被清空時,由於資料庫發生變化,進而再次觸發步驟1,通知BoundaryCallback重新獲取資料。

接下來將在Android_Jetpack:Paging元件之PageKeyedDataSource的MVVM使用的基礎上,使用BoundaryCallback和Room元件進行展示。

①引入依賴,建立room資料庫和Model類,以及針對Model類實現對應的Dao檔案,以方便對資料的增刪改查。

//按照"最新笑話"定義
data class JokeResponse(val reason:String, val result:Result,@SerializedName("error_code") val errorCode :Int) {
    data class Result(val data:List<Joke>)
    @Entity(tableName = "joke")
    data class Joke(val content:String, val hashId:String,val unixtime:Long,val updatetime:String){
        @PrimaryKey(autoGenerate = true)
        var id:Long = 0
    }
}

@Dao
interface JokeDao {
    @Insert
    fun insertJokes(jokes: List<JokeResponse.Joke>)
    @Query("DELETE FROM joke")
    fun clear()
    @Query("SELECT * FROM joke")
    fun getJokeList(): DataSource.Factory<Int,JokeResponse.Joke>
}

注意getJokeList()方法返回的是一個DataSource.Factory,這樣就可以實現資料庫的訂閱。

②實現BoundaryCallback。

class JokeBoundaryCallback(private val jokeDao: JokeDao):PagedList.BoundaryCallback<JokeResponse.Joke> (){
    private val sharedPreferences: SharedPreferences = context.getSharedPreferences("Joke", Context.MODE_PRIVATE)
    override fun onZeroItemsLoaded() {
        super.onZeroItemsLoaded()
        //載入第一頁資料,資料庫為空時,會回撥該方法
        JokeBoundaryCallbackViewModel.FIRST_PAGE = 1
        searchJokes()
    }

    override fun onItemAtFrontLoaded(itemAtFront: JokeResponse.Joke) {
        super.onItemAtFrontLoaded(itemAtFront)
        //載入第一個資料
        //暫時用不上,什麼都不用做
    }

    override fun onItemAtEndLoaded(itemAtEnd: JokeResponse.Joke) {
        super.onItemAtEndLoaded(itemAtEnd)
        //載入最後一個資料,當使用者滑動到頁面的最下方時且資料庫中的資料已經全部載入完畢,會回撥該方法
        //itemAtEnd是資料庫中最後一條資料
        readPage()
        //介面實測最多20頁,之後都只顯示第20頁的內容,因此20頁以後不載入
        //注意,這只是介面不支援的權宜之計,實際專案中,應該使Joke含有當前所在頁數字段
        if (JokeBoundaryCallbackViewModel.FIRST_PAGE<20) {
            searchJokes()
        }
    }
    //查詢資料
    private fun searchJokes(){
        val job = Job()
        val scope = CoroutineScope(job)
        scope.launch {
            val jokeInfoResponse = RetrofitNetwork.searchJokes(JokeBoundaryCallbackViewModel.FIRST_PAGE,JokeBoundaryCallbackViewModel.PAGE_SIZE)
            Log.d("jokeInfoResponse",jokeInfoResponse.toString())
            if (jokeInfoResponse.errorCode == 0){
                val jokes= jokeInfoResponse.result.data
                insertJokes(jokes)
                JokeBoundaryCallbackViewModel.FIRST_PAGE+=1
                savePage()
            }else{
            }
        }
    }
    //插入資料
    private fun insertJokes(joks:List<JokeResponse.Joke>){
        thread {
            jokeDao.insertJokes(joks)
        }
    }
    //儲存最後頁數
    private fun savePage(){
        val editor:SharedPreferences.Editor =  sharedPreferences.edit()
        editor.putInt("joke_max_page", JokeBoundaryCallbackViewModel.FIRST_PAGE)
        editor.apply()
        editor.commit()
    }
    //讀取最後頁數
    private fun readPage() {
        JokeBoundaryCallbackViewModel.FIRST_PAGE = sharedPreferences.getInt("joke_max_page", 1)
    }
}

在BoundaryCallback中有三個回撥方法,具體功能已經寫在註釋內。需要注意的是,由於onItemAtEndLoaded()方法的引數返回的是資料庫中最後一條資料,所以我們手動儲存了當前的頁碼,以便於下次進入時能夠從沒有寫入資料庫的頁數中獲取,而非從頭開始。
③實現ViewModel。

class JokeBoundaryCallbackViewModel (jokeDao: JokeDao) : ViewModel() {
   companion object{
       var FIRST_PAGE=1
       const val PAGE_SIZE=10
   }
    var jokePagedList: LiveData<PagedList<JokeResponse.Joke>>
    init {
        //Room元件對Paging元件提供原生支援,LivePagedListBuilder建立PagedList時可以直接將Room作為資料來源
        jokePagedList =
                LivePagedListBuilder<Int,JokeResponse.Joke>(jokeDao.getJokeList(), PAGE_SIZE)
                    .setBoundaryCallback(JokeBoundaryCallback(jokeDao))//將PagedList與BoundaryCallback關聯
                        .build()
    }
}

④修改Activity,完成頁面呼叫。

class JokeViewModelFactory (private val jokeDao: JokeDao): ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return JokeBoundaryCallbackViewModel(jokeDao) as T
    }
}

class BoundaryCallbackTestMainActivity : AppCompatActivity() {
    lateinit var jokeDao: JokeDao
    lateinit var viewModel: JokeBoundaryCallbackViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_boundary_callback_test_main)

        jokeDao = AppDatabase.getDatabase(this).jokeDao()
        viewModel = ViewModelProvider(this, JokeViewModelFactory(jokeDao)).get(JokeBoundaryCallbackViewModel::class.java)

        jokeRecyclerView.layoutManager = LinearLayoutManager(this)
        jokeRecyclerView.setHasFixedSize(true)
        jokeRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
        val jokePagedListAdapter = JokePagedListAdapter(this)
        viewModel.jokePagedList.observe(this, Observer { jokes ->
            jokePagedListAdapter.submitList(jokes)
        })
        jokeRecyclerView.adapter = jokePagedListAdapter
    }
}

其餘未展示的部分和之前一樣不變,參照Android_Jetpack:Paging元件之PageKeyedDataSource的MVVM使用即可。

執行一下,檢視LOG:首次進入頁面時獲取資料,上滑獲取下一頁的資料,退出重新進入到之前獲取過資料的部分,直接顯示資料庫內的資料,滑動到資料庫內沒有資料的頁數,獲取新頁數的資料。

⑤新增下拉重新整理功能。
下拉重新整理思路為在下拉的時候,清空資料庫表,由此觸發自動執行BoundaryCallback中onZeroItemsLoaded()方法。

a.在JokeBoundaryCallbackViewModel中新增方法

//重新整理資料
    fun refresh(){
        thread {
            Log.d("refresh","refresh")
            jokeDao.clear()
        }
    }
    

b.在佈局檔案新增下拉重新整理元件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
      android:id="@+id/swipeRefreshLayout"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      <androidx.recyclerview.widget.RecyclerView
          android:id="@+id/jokeRecyclerView"
          android:layout_width="match_parent"
          android:layout_height="match_parent"/>

  </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</LinearLayout>

c.在BoundaryCallbackTestMainActivity中使用下拉重新整理元件

swipeRefreshLayout.setOnRefreshListener {
    viewModel.refresh()
    swipeRefreshLayout.isRefreshing = false
}

執行程式,下拉重新整理,檢視LOG,OK。
在這裡插入圖片描述
在這裡插入圖片描述

相關文章