即学即用Android Jetpack - Paging

作者: 九心_ | 来源:发表于2019-07-01 00:27 被阅读30次

    前言

    即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第五篇。

    我相信几乎所有的Android开发者都会遇到在RecyclerView加载大量数据的情况,如果是在数据库请求,需要消耗数据库资源并且需要花费较多的时间,同样的,如果是发送网络请求,则需要消耗带宽和更多的时间,无论处于哪一种情形,对于用户的体验都是糟糕的。在这两种情形中,如果采用分段加载则缩短了时间,给用户带来了良好的体验,目前,对于加载大量数据的处理方法有两种:

    1. 借助刷新控件实现用户手动请求数据。
    2. 数据到达边界自动请求加载。

    谷歌架构组件Android Jetpack也实现了自己的分页库Paging,以下是我使用Paging实现的效果:

    效果

    Demo使用语言:Kotlin
    Demo地址:Jetpack

    目录

    目录

    一、介绍

    友情提示
    官方文档:Paging
    谷歌实验室:官方教程
    官方Demo:网络方式数据库方式

    我们在前言中已经简要的介绍了Paging,让我们看看谷歌官方如何介绍:

    The Paging Library helps you load and display small chunks of data at a time. Loading partial data on demand reduces usage of network bandwidth and system resources.

    可以看到,官方的介绍就是我们上面提及的内容,我们再来看看Paging是如何运作的:

    Paging架构
    当然,我需要做更具体的介绍,至于ViewModelLiveData,可以翻阅我的前几期博客,关键元素如下:
    名称 作用
    PagedList 一个可以以分页形式异步加载数据的容器,可以跟RecyclerView很好的结合
    DataSourceDataSource.Factory 数据源,DataSource将数据转变成PagedListDataSource.Factory则用来创建DataSource
    LivePagedListBuilder 用来生成LiveData<PagedList>,需要DataSource.Factory参数
    BoundaryCallback 数据到达边界的回调
    PagedListAdapter 一种RecyclerView的适配器

    1. 优点

    网上的分页解决方法挺多的,与他们相比,Paging有什么优点呢?

    • RxJava 2以及Android Jetpack的支持,如LiveDataRoom
    • 自定义分页策略。
    • 异步处理数据。
    • 结合RecyclerView等

    二、实战

    因为本文是Android Jetpack系列文章,所以主要介绍配合LiveData使用,对于RxJava的配合使用,本文会一笔带过。

    第一步 添加依赖

    ext.pagingVersion = '2.1.0-alpha01'
    dependencies {
        // ... 省略
        // paging
        implementation "androidx.paging:paging-runtime:$pagingVersion"
    }
    

    第二步 创建数据源

    1. 非Room数据库

    如果没有使用Room数据库,我们需要自定义实现DataSource,通常实现DataSource有三种方式,分别继承三种抽象类,它们分别是:

    名称 使用场景
    PageKeyedDataSource<Key, Value> 分页请求数据的场景
    ItemKeyedDataSource<Key, Value> 以表的某个列为key,加载其后的N个数据(个人理解以某个字段进行排序,然后分段加载数据)
    PositionalDataSource<T> 当数据源总数特定,根据指定位置请求数据的场景

    这里我们以PageKeyedDataSource<Key, Value>为例,虽然这里的数据库使用的是Room,但我们查询数据以返回List<Shoe>代表着通常数据库的使用方式:

    // 因为代表着不同方式,所以不需要看Dao层
    class ShoeRepository private constructor(private val shoeDao: ShoeDao) {
    
        /**
         * 通过id的范围寻找鞋子
         */
        fun getPageShoes(startIndex:Long,endIndex:Long):List<Shoe> = shoeDao.findShoesByIndexRange(startIndex,endIndex)
    
       //... 省略
    }
    
    /**
     * 自定义PageKeyedDataSource
     * 演示Page库的时候使用
     */
    class CustomPageDataSource(private val shoeRepository: ShoeRepository) : PageKeyedDataSource<Int, Shoe>() {
    
        private val TAG: String by lazy {
            this::class.java.simpleName
        }
    
        // 第一次加载的时候调用
        override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Shoe>) {
            val startIndex = 0L
            val endIndex: Long = 0L + params.requestedLoadSize
            val shoes = shoeRepository.getPageShoes(startIndex, endIndex)
    
            callback.onResult(shoes, null, 2)
        }
    
        // 每次分页加载的时候调用
        override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Shoe>) {
            Log.e(TAG, "startPage:${params.key},size:${params.requestedLoadSize}")
    
            val startPage = params.key
            val startIndex = ((startPage - 1) * BaseConstant.SINGLE_PAGE_SIZE).toLong() + 1
            val endIndex = startIndex + params.requestedLoadSize - 1
            val shoes = shoeRepository.getPageShoes(startIndex, endIndex)
    
            callback.onResult(shoes, params.key + 1)
        }
    
        override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Shoe>) {
           // ... 省略 类似loadAfter
        }
    }
    

    DataSource创建好了,再创建一个DataSource.Factory,这个比较简单,返回上面创建的CustomPageDataSource实例:

    /**
     * 构建CustomPageDataSource的工厂
     */
    class CustomPageDataSourceFactory(val shoeRepository: ShoeRepository):DataSource.Factory<Int,Shoe>() {
        override fun create(): DataSource<Int, Shoe> {
            return CustomPageDataSource(shoeRepository)
        }
    }
    
    2. Room数据库

    如果是使用Room与Paging结合的方式呢?直接在RoomDao层中这样使用:

    /**
     * 鞋子的方法
     */
    @Dao
    interface ShoeDao {
       //... 省略
    
        // 配合LiveData 返回所有的鞋子
        @Query("SELECT * FROM shoe")
        fun getAllShoesLD(): DataSource.Factory<Int, Shoe>
    }
    

    不止简单了一个档次~

    第三步 构建LiveData<PagedList>

    想要获得LiveData<PagedList>则需要先创建LivePagedListBuilderLivePagedListBuilder有设分页数量和配置参数两种构造方法,设置分页数量比较简单,直接查看Api就可以使用,我们看看如何配置参数使用:

    class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {
        // 鞋子集合的观察类
        val shoes: LiveData<PagedList<Shoe>> = LivePagedListBuilder<Int, Shoe>(
            CustomPageDataSourceFactory(shoeRepository) // DataSourceFactory
            , PagedList.Config.Builder()
                .setPageSize(10) // 分页加载的数量
                .setEnablePlaceholders(false) // 当item为null是否使用PlaceHolder展示
                .setInitialLoadSizeHint(10) // 预加载的数量
                .build())
            .build()
    }
    

    第四步 创建PagedListAdapter

    PagedListAdapter就是特殊的RecyclerViewRecyclerAdapter,跟RecyclerAdapter一样,需要继承并实现其方法,这里使用了Data Binding

    /**
     * 鞋子的适配器 配合Data Binding使用
     */
    class ShoeAdapter constructor(val context: Context) :
        PagedListAdapter<Shoe, ShoeAdapter.ViewHolder>(ShoeDiffCallback()) {
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            return ViewHolder(
                RecyclerItemShoeBinding.inflate(
                    LayoutInflater.from(parent.context)
                    , parent
                    , false
                )
            )
        }
    
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            val shoe = getItem(position)
            holder.apply {
                bind(onCreateListener(shoe!!.id), shoe)
                itemView.tag = shoe
            }
        }
    
        /**
         * Holder的点击事件
         */
        private fun onCreateListener(id: Long): View.OnClickListener {
            return View.OnClickListener {
                val intent = Intent(context, DetailActivity::class.java)
                intent.putExtra(BaseConstant.DETAIL_SHOE_ID, id)
                context.startActivity(intent)
            }
        }
    
    
        class ViewHolder(private val binding: RecyclerItemShoeBinding) : RecyclerView.ViewHolder(binding.root) {
    
            fun bind(listener: View.OnClickListener, item: Shoe) {
                binding.apply {
                    this.listener = listener
                    this.shoe = item
                    executePendingBindings()
                }
            }
        }
    }
    

    上面出现的ShoeDiffCallback:

    class ShoeDiffCallback: DiffUtil.ItemCallback<Shoe>() {
        override fun areItemsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
            return oldItem.id == newItem.id
        }
    
        override fun areContentsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
            return oldItem == newItem
        }
    }
    

    布局文件只有一个ImageView,不再赘述。

    第五步 监听数据

    同样使用了Data BindingFragmentShoe的布局仅仅只用了一个RecyclerView,比较简单,也不再赘述。

    /**
     * 鞋子页面
     */
    class ShoeFragment : Fragment() {
    
        // ... 省略
    
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            val binding: FragmentShoeBinding = FragmentShoeBinding.inflate(inflater, container, false)
            context ?: return binding.root
            val adapter = ShoeAdapter(context!!)
            binding.recycler.adapter = adapter
            onSubscribeUi(adapter)
            return binding.root
        }
    
        /**
         * 鞋子数据更新的通知
         */
        private fun onSubscribeUi(adapter: ShoeAdapter) {
            viewModel.shoes.observe(viewLifecycleOwner, Observer {
                if (it != null) {
                    adapter.submitList(it)
                }
            })
        }
    }
    

    这样,我们的程序就可以分段加载数据了,感兴趣的可以打一下日志:

    2019-06-30 17:15:00.564 32051-32117/com.joe.jetpackdemo E/CustomPageDataSource: startPage:2,size:10
    2019-06-30 17:15:02.836 32051-32112/com.joe.jetpackdemo E/CustomPageDataSource: startPage:3,size:10
    2019-06-30 17:15:13.705 32051-32113/com.joe.jetpackdemo E/CustomPageDataSource: startPage:4,size:10
    2019-06-30 17:15:15.869 32051-32116/com.joe.jetpackdemo E/CustomPageDataSource: startPage:5,size:10
    2019-06-30 17:15:19.986 32051-32117/com.joe.jetpackdemo E/CustomPageDataSource: startPage:6,size:10
    2019-06-30 17:15:22.102 32051-32112/com.joe.jetpackdemo E/CustomPageDataSource: startPage:7,size:10
    

    三、更多

    RxJava 2如此强大,怎么能少了对RxJava 2的支持呢?对于上述的代码,我们只要将数据观测的LiveData修改成RxJava 2就行了,因为RoomRxJava 2也提供了支持,并且RxJava 2LiveData做的本质工作是相同的,这里不写代码了,感兴趣的稍微查看一下官方文档就行了。

    四、总结

    总结

    以上内容是本篇博客的全部,本人知识水平有限,难免有误,欢迎指正。
    Over~

    参考内容:

    《Android Jetpack之Paging初探》
    《官方文档》

    🚀如果觉得本文不错,可以查看Android Jetpack系列的其他文章:

    第一篇:《即学即用Android Jetpack - Navigation》
    第二篇:《即学即用Android Jetpack - Data Binding》
    第三篇:《即学即用Android Jetpack - ViewModel & LiveData》
    第四篇:《即学即用Android Jetpack - Room》

    相关文章

      网友评论

        本文标题:即学即用Android Jetpack - Paging

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