JetPack 之 Paging3.0 簡單上手指南!

作者:Chsmy

之前有一篇Paging2.x的使用和分析,Paging2.x運行起來的效果無限滑動還挺不錯的,不過代碼寫起來有點麻煩,功能也不是太完善,比如下拉刷新的方法都沒有提供,我們還得自己去調用DataSource#invalidate()方法重置數據來實現。最近google出了3.0的測試版,功能更加強大,用起來更簡單,現在來開始嘗試一把。

先看看官網對Paging3.0的功能介紹

  • 分頁數據緩存到內存中,保證應用在處理頁面數據的時候,更有效的使用系統資源
  • 同時多個相同的請求只會觸發一個,確保App有效的使用網絡資源和系統資源
  • 可以配置RecyclerView的adapters,讓其滑動到末尾自動發起請求
  • 對Kotlin協程和Flow以及LiveData、RxJava 有很好的支持
  • 內置刷新、重試、錯誤處理等功能

開始使用,首先引入依賴庫

def paging_version = "3.0.0-alpha02"
implementation "androidx.paging:paging-runtime:$paging_version"

配置一個RecyclerView,主要需要兩個部分一個是Adapter,一個是數據。先從Adapter開始

構建Adapter

Adapter的創建跟Paging2.x寫法差不多,不過繼承的類不一樣了,Paging2.x繼承的是PagedListAdapter,在3.0中PagedListAdapter已經沒有了,需要繼承PagingDataAdapter

class ArticleAdapter : PagingDataAdapter<Article,ArticleViewHolder>(POST_COMPARATOR){

    companion object{
        val POST_COMPARATOR = object : DiffUtil.ItemCallback<Article>() {
            override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
                    oldItem == newItem

            override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
                    oldItem.id == newItem.id
        }
    }

    override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
         holder.tvName.text = getItem(position)?.title
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
        return ArticleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item,parent,false))
    }

}
class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
   val tvName: TextView = itemView.findViewById(R.id.tvname)
}

寫法跟寫正常的RecyclerView.Adapter基本一樣,就加了一樣東西,需要在構造方法裏傳入一個DiffUtil.ItemCallback用來確定差量更新的時候的計算規則。

Adapter寫完了,下面就是數據了,我們使用Retrofit和kotlin協程從網絡獲取數據之後將數據設置給Adapter

獲取數據並設置給Adapter

從官網上來看,google提倡我使用三層架構來完成數據到Adapter的設置,比如官網上的下圖

第一層 數據倉庫層Repository

Repository層主要使用PagingSource這個分頁組件來實現,每個PagingSource對象都對應一個數據源,以及該如何從該數據源中查找數據。PagingSource可以從任何單個數據源比如網絡或者數據庫中查找數據。

Repository層還有另一個分頁組件可以使用RemoteMediator,它是一個分層數據源,比如有本地數據庫緩存的網絡數據源。

下面創建我們的PagingSource和Repository

class ArticleDataSource:PagingSource<Int,Article>() {

    /**
     * 實現這個方法來觸發異步加載(例如從數據庫或網絡)。 這是一個suspend掛起函數,可以很方便的使用協程異步加載
     */
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {

        return try {
            val page = params.key?:0
            //獲取網絡數據
            val result = WanRetrofitClient.service.getHomeList(page)
            LoadResult.Page(
                    //需要加載的數據
                    data = result.data.datas,
                    //如果可以往上加載更多就設置該參數,否則不設置
                    prevKey = null,
                    //加載下一頁的key 如果傳null就說明到底了
                    nextKey = if(result.data.curPage==result.data.pageCount) null else page+1
            )
        }catch (e:Exception){
            LoadResult.Error(e)
        }

    }
}
  • 繼承PagingSource,需要兩個泛型,第一個表示下一頁數據的加載方式,比如使用頁碼加載可以傳Int,使用最後一條數據的某個屬性來加載下一頁就傳別的類型比如String等
  • 實現其load方法來觸發異步加載,可以看到它是一個用suspend修飾的掛起函數,可以很方便的使用協程異步加載。
  • 其參數LoadParams中有一個key值,我們可以拿出來用於加載下一頁。
  • 返回值是一個LoadResult,出現異常調用LoadResult.Error(e),正常強開情況下調用LoadResult.Page方法來設置從網絡或者數據庫獲取到的數據
  • prevKey 和 nextKey 分別代表下次向上加載或者向下加載的時候需要提供的加載因子,比如我們通過page的不斷增加來加載每一頁的數據,nextKey就可以傳入下一頁page+1。如果設置爲null的話說明沒有數據了。

創建Repository

class ArticleRepository {

    fun getArticleData() = Pager(PagingConfig(pageSize = 20)){
        ArticleDataSource()
    }.flow

}

代碼雖少不過有兩個重要的對象:Pager 和 PagingData

  • Pager是進入分頁的主要入口,它需要4個參數:PagingConfig、Key、RemoteMediator、PagingSource其中第一個和第四個是必填的。
  • PagingConfig用來配置加載的時候的一些屬性,比如多少條算一頁,距離底部多遠的時候開始加載下一頁,初始加載的條數等等。
  • PagingData 用來存儲每次分頁數據獲取的結果
  • flow是kotlin的異步數據流,點類似 RxJava 的 Observable

第二層ViewModel層

Repository最終返回一個異步流包裹的PagingDataFlow<PagingData>,PagingData存儲了數據結果,最終可以使用它將數據跟UI界面關聯。

ViewModel中一般都使用LiveData來跟UI層交互,Flow的擴展函數可以直接轉換成一個LiveData可觀察對象。

class PagingViewModel:ViewModel() {

    private val repository:ArticleRepository by lazy { ArticleRepository() }
    /**
     * Pager 分頁入口 每個PagingData代表一頁數據 最後調用asLiveData將結果轉化爲一個可監聽的LiveData
     */
    fun getArticleData() = repository.getArticleData().asLiveData()

}

UI層

UI層其實就是到了我們的Activity中,給RecycleView設置Adapter,給Adater設置數據

class PagingActivity : AppCompatActivity() {

    private val viewModel by lazy { ViewModelProvider(this).get(PagingViewModel::class.java) }

    private val adapter: ArticleAdapter by lazy { ArticleAdapter() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)

        val refreshView:SmartRefreshLayout = findViewById(R.id.refreshView)
        val recyclerView :RecyclerView = findViewById(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter.withLoadStateFooter(PostsLoadStateAdapter(adapter))
        //獲取數據並渲染UI
        viewModel.getArticleData().observe(this, Observer {
            lifecycleScope.launchWhenCreated {
                adapter.submitData(it)
            }
        })
        //監聽刷新狀態當刷新完成之後關閉刷新
        lifecycleScope.launchWhenCreated {
            @OptIn(ExperimentalCoroutinesApi::class)
            adapter.loadStateFlow.collectLatest {
                if(it.refresh !is LoadState.Loading){
                    refreshView.finishRefresh()
                }
            }
        }
        refreshView.setOnRefreshListener {
            adapter.refresh()
        }
    }
}
  • 創建出前面寫的Adapter的實例,並設置給RecyclerView
  • 調用viewModel.getArticleData()方法獲取LiveData並監聽返回數據
  • 調用adapter的submitData方法來觸發頁面的渲染。這個方法是一個suspend修飾的掛起方法,所以將它放到一個有生命週期的協程中調用。如果不想放到協程中可以調用另外一個兩個參數的方法adapter.submitData(lifecycle,it)傳入lifecycle就行了

刷新和重試

Paging3.0中調用刷新的方法比Paging2.x中方便多了,直接就提供了刷新的方法,並且還提供了加載數據出錯後的重試方法。

前面的activity代碼中,在下拉刷新控制的下拉監聽中直接調用adapter.refresh()方法就可以完成刷新了,那什麼時候關閉刷新動畫呢,需要調用adapter.loadStateFlow.collectLatest方法來監聽

 lifecycleScope.launchWhenCreated {
            @OptIn(ExperimentalCoroutinesApi::class)
            adapter.loadStateFlow.collectLatest {
                if(it.refresh !is LoadState.Loading){
                    refreshView.finishRefresh()
                }
            }
        }

收集流的狀態,如果是不是Loading狀態的說明加載完成了,可以關閉動畫了。

PagingDataAdapter可以設置頭部和底部的加載進度或者加載出錯時候的佈局,這樣當處於加載中的狀態的時候,可以顯示加載動畫,加載出錯的時候可以顯示出重試的按鈕。用起來也簡單舒服。

需要自定義一個Adapter繼承自LoadStateAdapter,並將這個Adapter設置給最開始adapter就可以了

class PostsLoadStateAdapter(
        private val adapter: ArticleAdapter
) : LoadStateAdapter<NetworkStateItemViewHolder>() {
    override fun onBindViewHolder(holder: NetworkStateItemViewHolder, loadState: LoadState) {
        holder.bindTo(loadState)
    }

    override fun onCreateViewHolder(
            parent: ViewGroup,
            loadState: LoadState
    ): NetworkStateItemViewHolder {
        return NetworkStateItemViewHolder(parent) { adapter.retry() }
    }
}

ViewHolder

class NetworkStateItemViewHolder(
    parent: ViewGroup,
    private val retryCallback: () -> Unit
) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.network_state_item, parent, false)
) {
    private val progressBar = itemView.findViewById<ProgressBar>(R.id.progress_bar)
    private val errorMsg = itemView.findViewById<TextView>(R.id.error_msg)
    private val retry = itemView.findViewById<Button>(R.id.retry_button)
        .also {
            it.setOnClickListener { retryCallback() }
        }

    fun bindTo(loadState: LoadState) {
        progressBar.isVisible = loadState is Loading
        retry.isVisible = loadState is Error
        errorMsg.isVisible = !(loadState as? Error)?.error?.message.isNullOrBlank()
        errorMsg.text = (loadState as? Error)?.error?.message
    }
}

LoadState有三種:NotLoading、Loading、Error,我們可以根據不同的狀態來改變底部或者頂部的佈局樣式

最後在activity中設置,下面添加一個底部佈局

recyclerView.adapter = adapter.withLoadStateFooter(PostsLoadStateAdapter(adapter))

直接調用adapter的相關方法就可以了,總共三個方法,添加底部,添加頭部,兩個都添加。

Paging3.0的簡單使用到此完成 效果如下:

如果你對新技術比較感興趣,歡迎關注本號,後續會推送更詳細的實踐相關文章。

最.最.最.最後,在這裏我也分享自己收錄整理的Android學習PDF,裏面對Jetpack有詳細的講解,希望可以幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,可以分享給身邊好友一起學習

如果你有需要的話,可以 點這領取

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章