Jetpack 成员 Paging3 数据库实践以及源码分析(一)

简介: Paging 是一个分页库,它可以帮助您从本地存储或通过网络加载显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源。

image.png


前言



前几天 Google 更新了几个 Jetpack 新成员 Hilt、Paging 3、App Startup 等等,在之前的文章里面分了 App Startup 是什么、App Startup 为我们解决了什么问题,如果之前没有看过可以点击下面连接前往查看文章和代码。



今天这边文章主要来分析 Paging3,Paging3 会分为三篇文章,详细的分析其原理,每篇文章都有完整的项目示例。



通过这篇文章你将学习到以下内容:


  • Paging3 是什么?
  • Paging3 在项目中的架构以及类的职能源码分析?
  • 如何在项目中正确使用 Paging3?
  • 数据映射(Data Mapper)是什么?
  • Kotlin Flow 是什么?


在分析之前我们先来了解一下本文实战项目中用到的技术:



Paging3 是什么?



Paging 是一个分页库,它可以帮助您从本地存储或通过网络加载显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源。


Paging3 是使用 Kotlin 协程完全重写的库,经历了从 Paging1x 到 Paging2x 在到现在的 Paging3,深刻领悟到 Paging3 比 Paging1 和 Paging2 真的方便了很多。


Google 推荐使用 Paging 作为 App 架构的一部分,它可以很方便的和 Jetpack 组件集成,Paging3 包含了以下功能:


  • 在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。
  • 内置删除重复数据的请求,确保您的 App 有效地使用网络带宽和系统资源。
  • 可配置 RecyclerView 的 adapters,当用户滚动到加载数据的末尾时自动请求数据。
  • 支持 Kotlin 协程和 Flow, 以及 LiveData 和 RxJava。
  • 内置的错误处理支持,包括刷新和重试等功能。


Paging3 的架构以及类的职能源码分析



Google 推荐我们使用 Paging3 时,在应用程序的三层中操作,以及它们如何协同工作加载和显示分页数据,如下图所示:


image.png


但是我个人认为应该在增加一层 Data Mapper (下面会有详细的介绍),如下图所示:


image.png


数据映射(Data Mapper)将数据源的实体,转换为上层用到的 model,往往会被我们忽略掉,但是在项目中起到了很大重要,我看了很多项目的,这个概念很少被提及到,我只在国外的大牛的写的文章中,它们提及到了这个概念。关于数据映射(Data Mapper) 后面会单独写一篇文章,配合 Demo 去验证,这里只是简单提及一下。


Data Mapper


在一个快速开发的项目中,为了越快完成第一个版本交付,下意识的将数据源和 UI 绑定到一起,当业务逐渐增多,数据源变化了,上层也要一起变化,导致后期的重构工作量很大,核心的原因耦合性太强了。


使用数据映射(Data Mapper)优点如下:


  • 数据源的更改不会影响上层的业务。
  • 糟糕的后端实现不会影响上层的业务 (想象一下,如果你被迫执行2个网络请求,因为后端不能在一个请求中提供你需要的所有信息,你会让这个问题影响你的整个代码吗)。
  • Data Mapper 便于做单元测试,确保不会因为数据源的变化,而影响上层的业务。
  • 在本文案例项目 Paging3Simple 中会用到 Data Mapper 作为数据映射,在代码中有详细的注释。


Repository layer


在 Repository layer 中的主要使用 Paging3 组件中的 PagingSource,每个 PagingSource 对象定义一个数据源以及如何从该数据源查找数据, PagingSource 对象可以从任何一个数据源加载数据,包括网络数据和本地数据。


PagingSource 是一个抽象类,其中有两个重要的方法 load 和 和 getRefreshKey,load 方法如下所示:


abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>


这是一个挂起函数,实现这个方法来触发异步加载,另外一个 getRefreshKey 方法


open fun getRefreshKey(state: PagingState<Key, Value>): Key? = null


该方法只在初始加载成功且加载页面的列表不为空的情况下被调用。


在这一层中还有另外一个 Paging3 的组件 RemoteMediator,RemoteMediator 对象处理来自分层数据源的分页,例如具有本地数据库缓存的网络数据源。


ViewModel layer


在 ViewModel layer 层主要用到了 Paging3 的组件 Pager,Pager 是主要的入口页面,在其构造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory,代码如下所示:


class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
    config: PagingConfig,
    initialKey: Key? = null,
    @OptIn(ExperimentalPagingApi::class)
    remoteMediator: RemoteMediator<Key, Value>? = null,
    pagingSourceFactory: () -> PagingSource<Key, Value>
)


今天这篇文章和项目主要用到了 PagingConfig 和 PagingSource,PagingSource 上面已经说过了,所以我们主要来分一下 PagingConfig。


val pagingConfig = PagingConfig(
    // 每页显示的数据的大小
    pageSize = 60,
    // 开启占位符
    enablePlaceholders = true,
    // 预刷新的距离,距离最后一个 item 多远时加载数据
    prefetchDistance = 3,
    /**
     * 初始化加载数量,默认为 pageSize * 3
     *
     * internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
     * val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER
     */
    initialLoadSize = 60,
    /**
     * 一次应在内存中保存的最大数据
     * 这个数字将会触发,滑动加载更多的数据
     */
    maxSize = 200
)


将 ViewModel 层连接到 UI 层用到了 Paging3 的组件 PagingData,PagingData 对象是分页数据的容器,它查询一个 PagingSource 对象并存储结果。


Google 推荐我们将组件 Pager 放到 ViewModel layer,但是我更喜欢放到 Repository layer,详见下文。


UI layer


在 UI layer 中的主要到了 Paging3 的组件 PagingDataAdapter,PagingDataAdapter 是一个处理分页数据的可回收视图适配器,您可以使用 AsyncPagingDataDiffer 组件来构建自己的自定义适配器,本文中用到是 PagingDataAdapter。


Paging 3 如何在项目中使用



在 App 模块中的 build.gradle 文件中添加以下代码:


dependencies {
  def paging_version = "3.0.0-alpha01"
  implementation "androidx.paging:paging-runtime:$paging_version"
}


接下来我将按照上面说的每层去实现,首先我们先来看一下项目的结构。


image.png


  • bean: 存放上层需要的 model,会和 RecyclerView 的 Adapter 绑定在一起。
  • loca: 存放和本地数据库相关的操作。
  • mapper: 数据映射,主要将数据源的实体 转成上层的 model。
  • repository:主要来处理和数据源相关的操作(本地、网络、内存中缓存等等)。
  • di: 和依赖注入相关。
  • ui:数据的展示。


数据库部分


@Dao
interface PersonDao {
    @Query("SELECT * FROM PersonEntity order by updateTime desc")
    fun queryAllData(): PagingSource<Int, PersonEntity>
    @Insert
    fun insert(personEntity: List<PersonEntity>)
    @Delete
    fun delete(personEntity: PersonEntity)
}


关于 Dao 这里需要解释一下, queryAllData 方法返回了一个 PagingSource,后面会通过 Pager 转换成 flow<PagingData<Value>>


Repository 部分


通过 Koin 注入 RepositoryFactory,通过 RepositoryFactory 管理相关的 Repository,RepositoryFactory 代码如下:


class RepositoryFactory(val appDataBase: AppDataBase) {
    // 传递 PagingConfig 和 Data Mapper 
    fun makeLocalRepository(): Repository =
        PersonRepositoryImpl(appDataBase, pagingConfig,Person2PersonEntityMapper(), PersonEntity2PersonMapper())
    val pagingConfig = PagingConfig(
        // 每页显示的数据的大小
        pageSize = 60,
        // 开启占位符
        enablePlaceholders = true,
        // 预刷新的距离,距离最后一个 item 多远时加载数据
        prefetchDistance = 3,
        /**
         * 初始化加载数量,默认为 pageSize * 3
         *
         * internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
         * val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER
         */
        initialLoadSize = 60,
        /**
         * 一次应在内存中保存的最大数据
         * 这个数字将会触发,滑动加载更多的数据
         */
        maxSize = 200
    )
}


这里主要是生成 PagingConfig 和 Data Mapper 然后传递给 PersonRepositoryImpl,我们来看一下 PersonRepositoryImpl 相关代码。


class PersonRepositoryImpl(
    val db: AppDataBase,
    val pageConfig: PagingConfig,
    val mapper2PersonEntity: Mapper<Person, PersonEntity>,
    val mapper2Person: Mapper<PersonEntity, Person>
) : Repository {
    private val mPersonDao by lazy { db.personDao() }
    override fun postOfData(): Flow<PagingData<Person>> {
        return Pager(pageConfig) {
            // 加载数据库的数据
            mPersonDao.queryAllData()
        }.flow.map { pagingData ->
            // 数据映射,数据库实体 PersonEntity ——>  上层用到的实体 Person
            pagingData.map { mapper2Person.map(it) }
        }
    }
}


Pager 是主要的入口页面,在其构造方法中接受 PagingConfig、pagingSourceFactory。


pagingSourceFactory: () -> PagingSource<Key, Value>


pagingSourceFactory 是一个 lambda 表达式,在 Kotlin 中可以直接用花括号表示,在花括号内,执行加载数据库的数据的请求。


最后调用 flow 返回 Flow<PagingData<Value>>,然后通过 Flow 的 map 将数据库实体 PersonEntity 转换成上层用到的实体 Person。


Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库,也叫做异步流,类似 RxJava 的 Observable,本文主要用到了 Flow 当中的 map 方法进行数据转换,简单实例如下所示:


flow{
    for (i in 1..4) {
        emit(i)
    }
}.map {
    it * it
}


到这里我们在回过去看,项目中 pagingData.map { mapper2Person.map(it) } 这行代码,其中 mapper2Person 是我们自己实现的 Data Mapper,代码如下所示:


class PersonEntity2PersonMapper : Mapper<PersonEntity, Person> {
    override fun map(input: PersonEntity): Person = Person(input.id, input.name, input.updateTime)
}


数据库实体 PersonEntity 转换为 上层用到的实体 Person。


UI 部分


通过 koin 依赖注入 MainViewModel,并传递参数 Repository。


class MainViewModel(val repository: Repository) : ViewModel() {
    // 调用 Flow 的 asLiveData 方法转为 LiveData
    val pageDataLiveData3: LiveData<PagingData<Person>> = repository.postOfData().asLiveData()
}


在 Activity 当中注册 observe,并将数据绑定给 Adapter,如下所示:


mMainViewModel.pageDataLiveData3.observe(this, Observer { data ->
    mAdapter.submitData(lifecycle, data)
})


知识扩充


刚才我们调用了 asLiveData 方法转为 LiveData,其实还有两种方法(作为了解即可)。


方法一


在 LifeCycle 2.2.0 之前使用的方法,使用两个 LiveData,一个是可变的,一个是不可变的,如下所示:


// 私有的 MutableLiveData 可变的,对内访问
private val _pageDataLiveData: MutableLiveData<Flow<PagingData<Person>>>
        by lazy { MutableLiveData<Flow<PagingData<Person>>>() }
// 对外暴露不可变的 LiveData,只能查询
val pageDataLiveData: LiveData<Flow<PagingData<Person>>> = _pageDataLiveData
_pageDataLiveData.postValue(repository.postOfData())


  • 准备一私有的 MutableLiveData,只对内访问。
  • 对外暴露不可变的 LiveData。
  • 将值赋值给 _pageDataLiveData。


方法二


在 LifeCycle 2.2.0 之后,可以用更精简的方法来完成,使用 LiveData 协程构造方法 (coroutine builder)。


val pageDataLiveData2 = liveData {
    emit(repository.postOfData())
}


liveData 协程构造方法提供了一个协程代码块,产生的是一个不可变的 LiveData,emit() 方法则用来更新 LiveData 的数据。


最后添加左右滑动删除功能


调用 recyclerview 封装好的 ItemTouchHelper 实现 左右滑动删除 item 功能。


private fun initSwipeToDelete() {
    /**
     * 位于 [androidx.recyclerview.widget] 包下,已经封装好的控件
     */
    ItemTouchHelper(object : ItemTouchHelper.Callback() {
        override fun getMovementFlags(
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder
        ): Int =
            makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
        override fun onMove(
            recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
            target: RecyclerView.ViewHolder
        ): Boolean = false
        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
            (viewHolder as PersonViewHolder).mBinding.person?.let {
                // 当 item 左滑 或者 右滑 的时候删除 item
                mMainViewModel.remove(it)
            }
        }
    }).attachToRecyclerView(rvList)
}


关于 Paging 加载本地数据到这里就结束了,我们将在下一篇文章讲解如何加载网络数据,最后上一个效果图。


image.png


总结



这篇文章主要介绍了以下内容:


Paging3 是什么以及它的优点


Paging 是一个分页库,它可以帮助您从本地存储或通过网络加载和显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源,而 Paging3 是使用 Kotlin 协程完全重写的库:


  • 在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。
  • 内置删除重复数据的请求,确保您的 App 有效地使用网络带宽和系统资源。
  • 可配置 RecyclerView 的 adapters,当用户滚动到加载数据的末尾时自动请求数据。
  • 支持 Kotlin 协程和 Flow, 以及 LiveData 和 RxJava。
  • 内置的错误处理支持,包括刷新和重试功能。


Paging3 的架构以及类的职能源码分析


  • PagingSource:每个 PagingSource 对象定义一个数据源以及如何从该数据源查找数据。
  • RemoteMediator:RemoteMediator 对象处理来自分层数据源的分页,例如具有本地数据库缓存的网络数据源。
  • Pager:是主要的入口页面,在其构造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory。
  • PagingDataAdapter:是一个处理分页数据的可回收视图适配器,您可以使用 AsyncPagingDataDiffer 组件来构建自己的自定义适配器。


数据映射(Data Mapper)


数据映射(Data Mapper)将数据源的实体,转换为上层用到的 model,往往会被我们忽略掉的,但是在项目中起到了很大重要,使用 数据映射(Data Mapper)优点如下:


  • 数据源的更改不会影响上层的业务。
  • 糟糕的后端实现不会影响上层的业务 (想象一下,如果你被迫执行2个网络请求,因为后端不能在一个请求中提供你需要的所有信息,你会让这个问题影响你的整个代码吗)。
  • Data Mapper 便于做单元测试,确保不会因为数据源的变化,而影响上层的业务。
  • 在本文案例项目 Paging3Simple 中会用到 Data Mapper 作为数据映射。


Kotlin Flow


Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库,也叫做异步流,类似 RxJava 的 Observable,本文主要用到了 flow 当中的 map 方法进行数据转换,如下面的例子所示:


flow{
    for (i in 1..4) {
        emit(i)
    }
}.map {
    it * it
}


到这里我相信应该理解了,项目中 pagingData.map { mapper2Person.map(it) } 这行代码的意思了。


GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice


正在建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请仓库右上角帮我点个赞。


结语



致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,可以关注我,如果这篇文章对你有帮助给个 star,正在努力写出更好的文章,一起来学习,期待与你一起成长。


算法


由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。


  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……


每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。


Android 10 源码系列


正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。



Android 应用系列



精选译文


目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。



工具系列



逆向系列



目录
相关文章
|
20天前
|
弹性计算 安全 关系型数据库
活动实践 | 自建数据库迁移到云数据库
通过阿里云RDS,用户可获得稳定、安全的企业级数据库服务,无需担心数据库管理与维护。该方案使用RDS确保数据库的可靠性、可用性和安全性,结合ECS和DTS服务,实现自建数据库平滑迁移到云端,支持WordPress等应用的快速部署与运行。通过一键部署模板,用户能迅速搭建ECS和RDS实例,完成数据迁移及应用上线,显著提升业务灵活性和效率。
|
4月前
|
存储 负载均衡 安全
高效管理大型数据库:分片与复制的策略与实践
在当今数据驱动的世界中,管理和优化大型数据库系统是每个企业的关键任务。特别是在面对数据量迅速增长的情况下,如何确保系统的高可用性和性能成为重要挑战。本文探讨了两种核心技术——分片(Sharding)和复制(Replication),以及它们在实际应用中的策略与实践。通过对比这两种技术的优缺点,并结合具体案例分析,本文旨在为数据库管理员和开发者提供一套高效管理大型数据库的综合方案。
|
1月前
|
关系型数据库 MySQL Linux
Linux环境下MySQL数据库自动定时备份实践
数据库备份是确保数据安全的重要措施。在Linux环境下,实现MySQL数据库的自动定时备份可以通过多种方式完成。本文将介绍如何使用`cron`定时任务和`mysqldump`工具来实现MySQL数据库的每日自动备份。
101 3
|
1月前
|
NoSQL Cloud Native atlas
探索云原生数据库:MongoDB Atlas 的实践与思考
【10月更文挑战第21天】本文探讨了MongoDB Atlas的核心特性、实践应用及对云原生数据库未来的思考。MongoDB Atlas作为MongoDB的云原生版本,提供全球分布式、完全托管、弹性伸缩和安全合规等优势,支持快速部署、数据全球化、自动化运维和灵活定价。文章还讨论了云原生数据库的未来趋势,如架构灵活性、智能化运维和混合云支持,并分享了实施MongoDB Atlas的最佳实践。
|
2月前
|
NoSQL Cloud Native atlas
探索云原生数据库:MongoDB Atlas 的实践与思考
【10月更文挑战第20天】本文探讨了MongoDB Atlas的核心特性、实践应用及对未来云原生数据库的思考。MongoDB Atlas作为云原生数据库服务,具备全球分布、完全托管、弹性伸缩和安全合规等优势,支持快速部署、数据全球化、自动化运维和灵活定价。文章还讨论了实施MongoDB Atlas的最佳实践和职业心得,展望了云原生数据库的发展趋势。
|
2月前
|
SQL 关系型数据库 MySQL
Go语言项目高效对接SQL数据库:实践技巧与方法
在Go语言项目中,与SQL数据库进行对接是一项基础且重要的任务
90 11
|
2月前
|
SQL 存储 关系型数据库
添加数据到数据库的SQL语句详解与实践技巧
在数据库管理中,添加数据是一个基本操作,它涉及到向表中插入新的记录
|
2月前
|
Rust 前端开发 关系型数据库
Tauri 开发实践 — Tauri 集成本地数据库
本文介绍了在 Tauri 框架中集成本地数据库的几种方案,包括直接绑定 SQLite、使用第三方数据库库和使用 tauri-plugin-sql-api 插件。最终选择了 tauri-plugin-sql-api,因为它集成简单、支持多种数据库类型,并且与 Tauri 框架深度整合,提升了开发效率和安全性。文章详细介绍了如何安装和使用该插件,以及如何编写核心代码实现数据库操作。
241 2
|
2月前
|
SQL 关系型数据库 数据库
SQL数据库:核心原理与应用实践
随着信息技术的飞速发展,数据库管理系统已成为各类组织和企业中不可或缺的核心组件。在众多数据库管理系统中,SQL(结构化查询语言)数据库以其强大的数据管理能力和灵活性,广泛应用于各类业务场景。本文将深入探讨SQL数据库的基本原理、核心特性以及实际应用。一、SQL数据库概述SQL数据库是一种关系型数据库
111 5
|
2月前
|
SQL 开发框架 .NET
ASP连接SQL数据库:从基础到实践
随着互联网技术的快速发展,数据库与应用程序之间的连接成为了软件开发中的一项关键技术。ASP(ActiveServerPages)是一种在服务器端执行的脚本环境,它能够生成动态的网页内容。而SQL数据库则是一种关系型数据库管理系统,广泛应用于各类网站和应用程序的数据存储和管理。本文将详细介绍如何使用A
91 3