概述
目录
- 前言
- 一、使用步骤
- 1. 实体类
- 2.创建Dao
- 3.创建 Database
- 4. 重点 RemoteMediator
- 4.1上代码
- 4.2 内容讲解
- 4.3 initialize()
- 5.ViewModel
- 6.DiffCallback
- 7.开始使用
- 7.1 Fragment:
- 7.2 Adapter
- 7.3 layout
- 8.点赞无效, 有八哥?
- 9.最后 效果图
- 总结
前言
上一篇写了Paging3的基本使用. 这一片 结合Room, 实现 NetWork 和 Db 数据处理. 实现 条目增删改操作.
下面是Paging存在的问题:
Paging 数据源不开放, 无法随意增删改操作; 只能借助 Room;
这就意味着: 从服务器拉下来的数据全缓存. 刷新时数据全清再重新缓存, 查询条件变更时重新缓存
当Room数据发生变化时, 会使内存中 PagingSource 失效。从而重新加载库表中的数据
官方文档:
Room: 官方文档点这里
Paging3: 官方文档点这里
推荐文章
- ListAdapter封装, 告别Adapter代码 (一)- ListAdapter 入门
- ListAdapter封装, 告别Adapter代码 (二)- SimpleAdapter
- ListAdapter封装, 告别Adapter代码 (三)- 头尾,多类型,嵌套,单多选
- paging3-分页数据加载库(入门)
引入库
//ViewModel, livedata, lifecycle
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0"
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
//协程
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
//room
implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
implementation "androidx.room:room-ktx:2.3.0"
//Paging
implementation "androidx.paging:paging-runtime:3.0.0"
提示:以下是本篇文章正文内容,下面案例可供参考
一、使用步骤
1. 实体类
首先实体类需要映射数据库中的表及字段.
@Entity
class RoomEntity(
@Ignore
//状态标记刷新条目方式, 用于ListAdapter; 但在 Paging 中废弃了
override var hasChanged: Boolean= false,
@ColumnInfo
//选中状态, 这里用作是否点赞.
override var hasChecked: Boolean = false)
: BaseCheckedItem {
@PrimaryKey
var id: String = "" //主键
@ColumnInfo
var name: String? = null //变量 name @ColumnInfo 可以省去
@ColumnInfo
var title: String? = null //变量 title
@Ignore
var content: String? = null //某内容; @Ignore 表示不映射为表字段
@Ignore
var index: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RoomEntity
if (hasChecked != other.hasChecked) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var result = hasChecked.hashCode()
result = 31 * result + (name?.hashCode() ?: 0)
return result
}
}
BaseItem
interface BaseItem {
var hasChanged: Boolean
fun getMItemType(): Int = 0
}
interface BaseCheckedItem : BaseItem{
var hasChecked: Boolean // 是否被勾选中
}
2.创建Dao
Room 必备的 Dao类; 这里提供了 5个函数; 看注释就好了.
@Dao
interface RoomDao {
//删除单条数据
@Query("delete from RoomEntity where id = :id ")
suspend fun deleteById(id:String)
//修改单条数据
@Update
suspend fun updRoom(entity: RoomEntity) //修改点赞状态;
//新增数据方式
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(list: MutableList<RoomEntity>)
//配合Paging; 返回 PagingSource
@Query("SELECT * FROM RoomEntity")
fun pagingSource(): PagingSource<Int, RoomEntity>
//清空数据; 当页面刷新时清空数据
@Query("DELETE FROM RoomEntity")
suspend fun clearAll()
}
3.创建 Database
Room 必备的 Database类;
@Database(entities = [RoomEntity::class, RoomTwoEntity::class], version = 8)
abstract class RoomTestDatabase : RoomDatabase() {
abstract fun roomDao(): RoomDao
abstract fun roomTwoDao(): RoomTwoDao
companion object {
private var instance: RoomTestDatabase? = null
fun getInstance(context: Context): RoomTestDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
RoomTestDatabase::class.java,
"Test.db" //数据库名称
)
// .allowMainThreadQueries() //主线程中执行
.fallbackToDestructiveMigration() //数据稳定前, 重建.
// .addMigrations(MIGRATION_1_2) //版本升级
.build()
}
return instance!!
}
}
}
4. 重点 RemoteMediator
官方解释:
RemoteMediator 的主要作用是:在 Pager 耗尽数据或现有数据失效时,从网络加载更多数据。它包含 load() 方法,您必须替换该方法才能定义加载行为。
所以:
这个类要做的, 1.从服务器拉数据存入数据库; 2.刷新时清空数据; 3.请求成功状态.
4.1上代码
上代码, 让我们先把代码敲起来
@ExperimentalPagingApi
class RoomRemoteMediator(private val database: RoomTestDatabase)
: RemoteMediator<Int, RoomEntity>(){
private val userDao = database.roomDao()
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, RoomEntity>
): MediatorResult {
return try {
//获取页码 key
val loadKey = when (loadType) {
//表示 刷新.
LoadType.REFRESH -> null //loadKey 是页码标志, null代表第一页;
LoadType.PREPEND ->
return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
//这里用 NoMoreException 方式显示没有更多;
if(index>=15){
return MediatorResult.Error(NoMoreException())
}
if (lastItem == null) {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
lastItem.index
}
}
// val response = ApiManager.INSTANCE.mApi.getDynamicList()
val data = createListData(loadKey)
database.withTransaction {
if (loadType == LoadType.REFRESH) {
userDao.clearAll()
}
userDao.insertAll(data)
}
//endOfPaginationReached 表示 是否最后一页; 如果用 NoMoreException(没有更多) 方式, 则必定false
MediatorResult.Success(
endOfPaginationReached = false
)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}
}
private var index = 0
private fun createListData(min: Int?) : MutableList<RoomEntity>{
val result = mutableListOf<RoomEntity>()
Log.d("pppppppppppppppppp", "啦数据了当前index=$index")
repeat(10){
// val p = min ?: 0 + it
index++
val p = index
result.add(RoomEntity().apply {
id = "test$p"
name = "小明$p"
title = "干哈呢$p"
index = p
})
}
return result
}
}
createListData() 是模拟数据, 就不用多说了.
4.2 内容讲解
LoadType:
表示加载状态的密封类.
类名 | 意义 |
---|---|
LoadType.REFRESH | 表示刷新, loadKey=null 表示第一页 |
LoadType.PREPEND | 分页开始前会调用一次, 类似于确认是否分页 |
LoadType.APPEND | 数据追加, 此时需要从服务器拉数据 |
MediatorResult:
表示 加载请求结果的密封类.
类名 | 意义 |
---|---|
MediatorResult.Error | 表示加载请求失败,类似于 LoadResult; 可以承载失败异常信息, 可以做 没有更多 |
MediatorResult.Success | 表示加载请求成功. endOfPaginationReached = true 表示仍有未加载数据, 可以加载更多 endOfPaginationReached = false 表示最后一页了, 没有更多数据可供加载 |
整个 Load() 看起来代码不少. 实际上就三小步
第一步: 根据 loadType 判断并获取 加载的 key(可以理解为页码); 并注意额外状态返回即可
第二步: 网络请求拉取数据, 博主这里用的模拟数据
第三步: 将数据持久化存储, 如果是刷新状态 则先清空旧数据
页码获取方式:
官方文档用的 lastItem.id 方式, 意思就是从这个ID后面加载数据. 如果是用页码的就不太合适了. 而且这方式似乎有问题, 在博主的模拟数据中, 第一页last.index 应当是9. 但博主这里总是0
也可以数据库存储. SharePrefences等存储当前页码
如果room仅用作配合Paging. 而不设置数据的有效状态或有效时长时, 则可以直接在 RemoteMediator 中定义变量页码计数;
4.3 initialize()
有的时候,我们刚更新的数据, 此时立刻再更新一遍数据 无异于是对网络资源和设备性能的一种浪费. 此时我们只想用 db 中的数据,而不需要从服务器拉数据.
此时就要重写 initialize() 告诉 RemoteMediator: 数据是否仍然有效;
返回值 | 意义 |
---|---|
InitializeAction.SKIP_INITIAL_REFRESH | 表示数据有效, 无需刷新 |
InitializeAction.LAUNCH_INITIAL_REFRESH | 表示数据已经失效, 需要立即拉取数据替换并刷新 |
至于策略嘛, 例如存储上次拉取的时间等, 需自行根据业务区分.
5.ViewModel
Pager 的构造函数 需要传入 我们自定义的 remoteMediator 对象;
然后我们还增加了: 点赞(指定条目刷新); 删除(指定条目删除) 操作;
class RoomModelTest(application: Application) : AndroidViewModel(application) {
@ExperimentalPagingApi
val flow = Pager(
config = PagingConfig(pageSize = 10, prefetchDistance = 2,initialLoadSize = 10),
remoteMediator = RoomRemoteMediator(RoomTestDatabase.getInstance(application))
) {
RoomTestDatabase.getInstance(application).roomDao().pagingSource()
}.flow
.cachedIn(viewModelScope)
fun praise(info: RoomEntity) {
info.hasChecked = !info.hasChecked //这里有个坑
info.name = "我名变了"
viewModelScope.launch(Dispatchers.IO) {
RoomTestDatabase.getInstance(getApplication()).roomDao().updRoom(info)
}
}
fun del(info: RoomEntity) {
viewModelScope.launch(Dispatchers.IO) {
RoomTestDatabase.getInstance(getApplication()).roomDao().deleteById(info.id)
}
}
}
6.DiffCallback
看过我 ListAdapter 系列 的小伙伴,应该知道. 我曾经用 状态标记方式作为 判断 Item 是否变化的依据; 但是在 Paging+Room 的组合中, 就不能这样用了;
原因:
在Paging中 列表数据的改变, 完全取决于 Room 数据库中存储的数据.
当我们要删除或点赞操作时, 必须要更新数据库指定条目的内容;
而当数据库中数据发生改变时, PagingSource 失效, 原有对象将会重建. 所以 新旧 Item 可能不再是同一实体, 也就是说内存地址不一样了.
OK, 我们上代码
class DiffCallbackPaging: DiffUtil.ItemCallback<RoomEntity>() {
/**
* 比较两个条目对象 是否为同一个Item
*/
override fun areItemsTheSame(oldItem: RoomEntity, newItem: RoomEntity): Boolean {
return oldItem.id == newItem.id
}
/**
* 再确定为同一条目的情况下; 再去比较 item 的内容是否发生变化;
* 原来我们使用 状态标识方式判断; 现在我们要改为 Equals 方式判断;
* @return true: 代表无变化; false: 有变化;
*/
override fun areContentsTheSame(oldItem: RoomEntity, newItem: RoomEntity): Boolean {
// return !oldItem.hasChanged
if(oldItem !== newItem){
Log.d("pppppppppppp", "不同")
}else{
Log.d("pppppppppppp", "相同")
}
return oldItem == newItem
}
}
细心的小伙伴应该能发现, 在 areContentsTheSame 方法中,我打印了一行日志.
博主是想看看, 当一个条目点赞时, 是只有这一条记录的实体失效重建, 还是说整个列表重建
答案是: 一溜烟的 不同. 全都重建了. 为了单条目的点赞刷新, 而重建了整个列表对象; 这是否是 拿设备性能 换取 开发效率?
7.开始使用
7.1 Fragment:
实例化 Adapter, RecycleView. 然后绑定一下 PagingData 的监听即可
@ExperimentalPagingApi
override fun onLazyLoad() {
mAdapter = SimplePagingAdapter(R.layout.item_room_test, object : Handler<RoomEntity>() {
override fun onClick(view: View, info: RoomEntity) {
when(view.id){
R.id.tv_praise -> { //点赞操作
mViewModel?.praise(info)
}
R.id.btn_del -> { //删除操作
mViewModel?.del(info)
}
}
}
}, DiffCallbackPaging())
val stateAdapter = mAdapter.withLoadStateFooter(MyLoadStateAdapter(mAdapter::retry))
mDataBind.rvRecycle.let {
it.layoutManager = LinearLayoutManager(mActivity)
// **** 这里不要给 mAdapter(主数据 Adapter); 而是给 stateAdapter ***
it.adapter = stateAdapter
}
//Activity 用 lifecycleScope
//Fragments 用 viewLifecycleOwner.lifecycleScope
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
mViewModel?.flow?.collectLatest {
mAdapter.submitData(it)
}
}
}
7.2 Adapter
这里就不封装了, 有兴趣的小伙伴, 可以参考我 ListAdapter 封装系列
open class SimplePagingAdapter<T: BaseItem>(
private val layout: Int,
protected val handler: BaseHandler? = null,
diffCallback: DiffUtil.ItemCallback<T>
) :
PagingDataAdapter<T, RecyclerView.ViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return NewViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context), layout, parent, false
), handler
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if(holder is NewViewHolder){
holder.bind(getItem(position))
}
}
}
7.3 layout
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="item"
type="com.example.kotlinmvpframe.test.testroom.RoomEntity" />
<variable
name="handler"
type="com.example.kotlinmvpframe.test.testtwo.Handler" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingHorizontal="16dp"
android:paddingVertical="28dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_index_item"
style="@style/tv_base_16_dark"
android:gravity="center_horizontal"
android:text="@{item.name}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/tv_title_item"
style="@style/tv_base_16_dark"
android:layout_width="0dp"
android:textStyle="bold"
android:lines="1"
android:ellipsize="end"
android:layout_marginStart="8dp"
android:layout_marginEnd="20dp"
android:text="@{item.title}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_index_item"
app:layout_constraintEnd_toStartOf="@id/tv_praise"/>
<TextView
style="@style/tv_base_14_gray"
android:gravity="center_horizontal"
android:text='@{item.content ?? "暂无内容"}'
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@id/tv_index_item"
app:layout_constraintStart_toStartOf="parent"/>
<Button
android:id="@+id/btn_del"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除它"
android:onClick="@{(view)->handler.onClick(view, item)}"
android:layout_marginEnd="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_praise"/>
<TextView
android:id="@+id/tv_praise"
style="@style/tv_base_14_gray"
android:layout_marginStart="12dp"
android:padding="6dp"
android:drawablePadding="8dp"
android:onClick="@{(view)->handler.onClick(view, item)}"
android:text='@{item.hasChecked? "已赞": "赞"}'
android:drawableStart="@{item.hasChecked? @drawable/ic_dynamic_praise_on: @drawable/ic_dynamic_praise}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
8.点赞无效, 有八哥?
原来这段代码有问题:
fun praise(info: RoomEntity) {
info.hasChecked = !info.hasChecked
info.name = "我名变了"
viewModelScope.launch {
RoomTestDatabase.getInstance(getApplication()).roomDao().updRoom(info)
}
}
以上info 是旧实体对象. 我们把点赞状态变为true后才去更新的数据库; 而数据库更新后, 新实体对象的点赞状态 也是 true;
当下面这段代码执行时, 新旧对象的状态一样. Equals 为 true; 所以列表没有刷新;
override fun areContentsTheSame(oldItem: RoomEntity, newItem: RoomEntity): Boolean {
return oldItem == newItem
}
怎么办? 只能让旧实体的数据不变化: 如下所示, 单独写更新Sql; 或者 copy 一个新的实体对象再去更新db, 我只能说 那好吧!
//ViewModel
fun praise(info: RoomEntity) {
//这里可以用 新实体对象来做更新. 也可以单独写 SQL
viewModelScope.launch {
RoomTestDatabase.getInstance(getApplication()).roomDao().updPraise(info.id, !info.hasChecked)
}
}
//Dao
//修改单条数据
@Query("update RoomEntity set hasChecked = :isPraise where id = :id")
suspend fun updPraise(id: String, isPraise: Boolean) //修改点赞状态;
9.最后 效果图
总结
Paging 数据源不开放, 只能通过 Room 做增删改操作;
如果只要求存储第一页数据, 用于网络状态差时,尽快的页面渲染. 而强制整个列表持久化存储的话,博主认为这是一种资源浪费
本地增删改, 会让列表数据失效. 为了单条记录, 去重复创建整个列表对象. 无异于资源性能的浪费.
因为是用Equals判断条目变化, 所以需要额外注意, 旧对象的内容千万不要更改. 更新时要用 Copy 对象去做. 这很别扭;
博主对 Paging 的了解不算深, 源码也没看多少. 不知道上面几条的理解是否有偏差. 但就目前来看,博主可能要 从入门到放弃了 [苦笑]
上一篇: paging3-分页数据加载库(入门)
下一篇: Room-数据持久化存储(入门)
最后
以上就是高挑野狼为你收集整理的Paging3-分页数据加载库(结合Room)前言一、使用步骤总结的全部内容,希望文章能够帮你解决Paging3-分页数据加载库(结合Room)前言一、使用步骤总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复