28

JetPack之Paging3.0

 3 years ago
source link: https://blog.csdn.net/mingyunxiaohai/article/details/106974926
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

之前写过一篇 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的设置,比如官网上的下图

rY3Yfee.png!web

第一层 数据仓库层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最终返回一个异步流包裹的PagingData Flow<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()

刷新和重试

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的简单使用到此完成 效果如下:

AvUfyeq.gif

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK