JetPack 之 Paging3.0 简单上手指南!

作者: 初壹十五a | 来源:发表于2020-07-01 22:00 被阅读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<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层

    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有详细的讲解,希望可以帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,可以分享给身边好友一起学习

    如果你有需要的话,可以 点这领取

    相关文章

      网友评论

        本文标题:JetPack 之 Paging3.0 简单上手指南!

        本文链接:https://www.haomeiwen.com/subject/ejkkqktx.html