之前写过一篇Paging2.x的是使用和分析,Paging2.x运行起来的效果无限滑动还挺不错的,不过代码写起来有点麻烦,功能也不是太完善,比如下拉刷新的方法都没有提供,我们还得自己去调用DataSource#invalidate()
方法重置数据来实现。最近google出了3.0的测试版,功能更加强大,用起来更简单,现在来开始尝试一把
先看看官网对Paging3.0的功能介绍
开始使用,首先引入依赖库
def paging_version = "3.0.0-alpha02" implementation "androidx.paging:paging-runtime:$paging_version" 复制代码
配置一个RecyclerView,主要需要两个部分一个是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
从官网上来看,google提倡我使用三层架构来完成数据到Adapter的设置,比如官网上的下图
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) } } } 复制代码
LoadResult.Error(e)
,正常强开情况下调用LoadResult.Page
方法来设置从网络或者数据库获取到的数据创建Repository
class ArticleRepository { fun getArticleData() = Pager(PagingConfig(pageSize = 20)){ ArticleDataSource() }.flow } 复制代码
代码虽少不过有两个重要的对象:Pager 和 PagingData
Repository最终返回一个异步流包裹的PagingDataFlow<PagingData<Value>>
,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层其实就是到了我们的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() } } } 复制代码
viewModel.getArticleData()
方法获取LiveData并监听返回数据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的简单使用到此完成 效果如下: