即学即用Android Jetpack - Paging

前言

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

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

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

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

效果

语言:Kotlin
Demo地址:https://github.com/mCyp/Hoo

目录

目录

一、介绍

友情提示
官方文档: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 - WorkManger》
第七篇:《即学即用Android Jetpack - Startup》
第八篇:《即学即用Android Jetpack - Paging 3》
项目总结篇:《学习Android Jetpack? 实战和教程这里全都有!》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容