iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
0
Mobile Development

Android Architecture Components 學習心得筆記系列 第 20

Day 20 Paging (二) NetWork Datasource

今天要來實作使用 Paging 來完成一個簡單的列表顯示,且必須透過網路請求取得數據。

添加依賴

dependencies {
    .
    .
    implementation "androidx.paging:paging-runtime-ktx:2.1.0"
}

先看 API :

https://free-nba.p.rapidapi.com/players?page=1

發出 GET 請求後返回一個 List
其中帶入 ?page=1 來取得第一頁資料,帶入 ?page=2 來取得第二頁資料,依此類推。
這個 page 需要自己計算,回來的 response 不會有相關資訊,所以這邊用 PageKeyedDataSource 來當範例。

interface AllPlayerApi {

    companion object {

        private val retrofit = Retrofit
            .Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl("https://free-nba.p.rapidapi.com/")
            .build()

        val api = retrofit.create(AllPlayerApi::class.java)
    }
    
    @GET("players")
    fun getAllPlayer(@Query("page") page: Int): Call<AllPlayerData>
}

使用之前的 Retrofit 來取得資料,呼叫 getAllPlayer 便可以取得我們想要的數據 Call<AllPlayerData>

data class AllPlayerData(
    @SerializedName("data")
    val dataItemList: List<DataItem>
)

DataSource


class PagingKeyDataSource : PageKeyedDataSource<Int, DataItem>() {

    private var page = 1
    
    val api = AllPlayerApi.api

    val call = api.getAllPlayer(page)

    override fun loadInitial(params: LoadInitialParams<Int>, 
                            callback: LoadInitialCallback<Int, DataItem>) {

        call.enqueue(object : Callback<AllPlayerData> {
        
            override fun onFailure(call: Call<AllPlayerData>?, t: Throwable?) { }

            override fun onResponse(call: Call<AllPlayerData>?, 
                                    response: Response<AllPlayerData>) {

                callback.onResult(response.body()!!.dataItemList!!, null, page)
            }
        })
    }

    override fun loadAfter(params: LoadParams<Int>, 
                           callback: LoadCallback<Int, DataItem>) {

        call.enqueue(object : Callback<AllPlayerData> {
        
            override fun onFailure(call: Call<AllPlayerData>?, t: Throwable?) { }

            override fun onResponse(call: Call<AllPlayerData>?, 
                                    response: Response<AllPlayerData>) {

                callback.onResult(response.body()!!.dataItemList!!, page)

                page += 1
            }
        })
    }

    override fun loadBefore(params: LoadParams<Int>, 
                            callback: LoadCallback<Int, DataItem>) { }
}

PagingKeyDataSource 這個類別繼承 PagingKeyDataSource ,後面帶入 <Int, DataItem>,Int 代表下一頁的 key,也就是 ?page=1 這個參數,後面的 Value 就是要顯示在列表的 Item 物件

這裡需要覆寫三個方法 loadInitialloadBeforeloadAfter
看字面意思應該就知道要幹嘛了
loadInitial 只會在初始化跑一次,在裡面去實作取資料的動作,並在取得資料成功後用 LoadCallback 把資料帶回,並在 callback 回去後把我們自行計算的 page +1。

 callback.onResult(response.body()!!.dataItemList!!, null, page)

使用者往下滑到沒有資料時,會進入 loadAfter,再去取得下一頁的資料,裡面做的事其實跟 loadInitial 是一樣的,如果沒有資料時就在 onFailure 裡面另外處理就可以了。

至於 loadBefore 是向前一個分頁加載數據,沒有要這個功能的話就空著就可以了。

DataSourceFactory

class DataSourceFactory : DataSource.Factory<String, DataItem>() {

    private val sourceLiveData = MutableLiveData<PagingDataSource>()

    override fun create(): DataSource<String, DataItem> {
    
        val source = PagingDataSource()
        sourceLiveData.postValue(source)
        return source
    }
}

定義好 DataSource 後統一藉由 DataSourceFactory 去拿取想要的 DataSource,這樣之後如果要把資料的來源換成本地的資料庫或是其他來源會比較方便。

在 create 裡面把 DataSource 做好初始化,以及資料變化時的動作,因為是使用 MVVM 架構,所以 Data 必須用 MutableLiveData 包起來回傳。

到這邊已經完成最麻煩的資料來源的部分了,接著只要藉由 LiveData 的觀察者模式去觀察這個 DataSource,並在發生變化時通知 adapter !

PagedListAdapter

class PagingAdapter : PagedListAdapter<DataItem, PagingHolder>(DiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingHolder {

        val layoutInflater = LayoutInflater.from(parent.context)
        val view = layoutInflater.inflate(R.layout.item_paging, parent, false)

        return PagingHolder(view)
    }

    override fun onBindViewHolder(holder: PagingHolder, position: Int) {

        val data = getItem(position)

        data?.let {
            holder.setResult(data)
        }
    }

    companion object DiffCallback : DiffUtil.ItemCallback<DataItem>() {

        override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem == newItem
        }

        override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem.id == newItem.id
        }
    }
}

還記得之前練習 DataBinding 使用的 ListAdapter 嗎, PagedListAdapter 和他非常相似。

onBindViewHolder 裡面呼叫 getItem 就可以取得 Paging 幫我們自動計算得到的 Item
,並丟進一個 DiffUtil.ItemCallback 去判斷更換數據的邏輯,提高效能。
這裡的 ViewHolder 就是普通的 ViewHolder,就不貼惹。

ViewModel

class PagingViewModel: ViewModel() {

    private val sourceFactory by lazy {
        DataSourceFactory()
    }

    val pagingDataItems: LiveData<PagedList<DataItem>> by lazy {
        sourceFactory.toLiveData(6, null)
    }
}

初始化 DataSourceFactory 後,Paging 提供了一個與 LiveData 整合的 fun toLiveData,可以把 DataSource 轉成 LiveData
後面填入的是每一次要載入的 pageSize 和 initialLoadKey(不知道要幹嘛,我填 null)

訂閱 LiveData

終於來到最後一哩路

在 Activity

 viewModel.pagingDataItems.observe(this, Observer {

            adapter.submitList(it)
        })

透過 submitList 把資料傳給 adapter,就可以跑跑看了~
之前在處理分頁時的卡頓感不見了,列表瀏覽變得更滑順流暢,且效能更好。

到這邊完成了 Paging 最常見的使用(NetWork Datasource)。

有任何問題或講得不清楚的地方歡迎留言和我討論。

更歡迎留言糾正我任何說錯的地方!

下一篇:Paging (三) (NetWork + Database)Datasource


上一篇
Day 19 Paging (ㄧ) 介紹
下一篇
Day 21 Paging (三) (NetWork + Database) Datasource
系列文
Android Architecture Components 學習心得筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言