作者: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有詳細的講解,希望可以幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,可以分享給身邊好友一起學習
如果你有需要的話,可以 點這領取