Paging3 (二)  结合Room

Paging 数据源不开放, 无法随意增删改操作;  只能借助 Room;

这就意味着:  从服务器拉下来的数据全缓存.  刷新时数据全清再重新缓存,  查询条件变更时重新缓存 [让我看看]

当Room数据发生变化时,  会使内存中 PagingSource 失效。从而重新加载库表中的数据

Room: 官方文档点这里

Paging3: 官方文档点这里.

本文内容:

  1. 实体类, Dao, DataBase 代码
  2. RemoteMediator 代码与讲解
  3. ViewModel, DiffCallback, Adapter, Layout 代码
  4. 效果图
  5. 总结

本文导包: 

//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. 第一步, 创建实体类.

Room 需要 用 @Entity 注释类;  @PrimaryKey 注释主键

@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
}
}

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(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.请求成功状态. 

注意:   

endOfPaginationReached = true 表示: 已经加载到底了,没有更多数据了

MediatorResult.Error 类似于 LoadResult.Error;

@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 {
val loadKey = when (loadType) {
//表示 刷新.
LoadType.REFRESH -> null //loadKey 是页码标志, null代表第一页;
LoadType.PREPEND ->
return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
val first = state.firstItemOrNull() Log.d("pppppppppppppppppp", "last index=${lastItem?.index} id=${lastItem?.id}")
Log.d("pppppppppppppppppp", "first index=${first?.index} id=${first?.id}") //这里用 NoMoreException 方式显示没有更多;
if(index>=15){
return MediatorResult.Error(NoMoreException())
} if (lastItem == null) {
return MediatorResult.Success(
endOfPaginationReached = true
)
} lastItem.index
}
} //页码标志, 官方文档用的 lastItem.index 方式, 但这方式似乎有问题,第一页last.index 应当是9. 但博主这里总是0 ,
//也可以数据库存储. SharePrefences等;
//如果数据库数据仅用作 没有网络时显示. 不设置有效状态或有效时长时, 则可以直接在 RemoteMediator 页码计数;
// 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
}
}

4.1 重写 initialize()  检查缓存的数据是否已过期

有的时候,我们刚查询的数据, 不需要立刻更新.  所以需要告诉 RemoteMediator: 数据是否有效;

这时候就要重写  initialize();   判断策略嘛,  例如db, Sp存储上次拉取的时间等

InitializeAction.SKIP_INITIAL_REFRESH:  表示数据有效, 无需刷新

InitializeAction.LAUNCH_INITIAL_REFRESH: 表示数据已经失效, 需要立即拉取数据替换刷新;

例如:

/**
* 判断 数据是否有效
*/
override suspend fun initialize(): InitializeAction {
val lastUpdated = 100 //db.lastUpdated() //最后一次更新的时间
val timeOutVal = 300 * 1000
return if (System.currentTimeMillis() - lastUpdated >= timeOutVal)
{
//数据仍然有效; 不需要重新从服务器拉取数据;
InitializeAction.SKIP_INITIAL_REFRESH
} else {
//数据已失效, 需从新拉取数据覆盖, 并刷新
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 {
RoomTestDatabase.getInstance(getApplication()).roomDao().updRoom(info)
}
} fun del(info: RoomEntity) {
viewModelScope.launch {
RoomTestDatabase.getInstance(getApplication()).roomDao().deleteById(info.id)
}
}
}

6. 有一点必须要注意:  DiffCallback

看过我 ListAdapter 系列 的小伙伴,应该知道.  我曾经用 状态标记方式作为 判断 Item 是否变化的依据;

但是在 Paging+Room 的组合中, 就不能这样用了;

因为 在Paging中 列表数据的改变,  完全取决于 Room 数据库中存储的数据.

当我们要删除或点赞操作时, 必须要更新数据库指定条目的内容;

而当数据库中数据发生改变时,  PagingSource 失效, 原有对象将会重建. 所以 新旧 Item 可能不再是同一实体, 也就是说内存地址不一样了.

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. 贴出 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)
}
}
}

8. 贴出 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))
}
} }

9. 布局文件代码:

<?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>

10. 当博主运行时, 发现点赞没变化 ...  什么情况

原来这段代码有问题:

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.hasChanged
return oldItem == newItem
}

怎么办?  只能让旧实体的数据不变化: 如下所示, 单独写更新Sql;

或者 copy 一个新的实体对象, 变更状态, 然后用新对象 更新数据库;  我只能说  那好吧!

//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) //修改点赞状态;


11. 贴出效果图


总结:
1.Paging 数据源不开放, 只能通过 Room 做增删改操作;
2.如果只要求存储第一页数据, 用于网络状态差时,尽快的页面渲染. 而强制整个列表持久化存储的话,博主认为这是一种资源浪费
3.本地增删改, 会让列表数据失效. 为了单条记录, 去重复创建整个列表对象. 无异于资源性能的浪费.
4.因为是用Equals判断条目变化, 所以需要额外注意, 旧对象的内容千万不要更改. 更新时要用 Copy 对象去做. 这很别扭;
5.博主对 Paging 的了解不算深, 源码也没看多少. 不知道上面几条的理解是否有偏差. 但就目前来看,博主可能要 从入门到放弃了 [苦笑]

Over

回到顶部

 

孟老板 Paging3 (二) 结合Room的更多相关文章

  1. 孟老板 Paging3 (一) 入门

    BaseAdapter系列 ListAdapter系列 Paging3 (一) 入门 Paging3 (二) 结合 Room Paging3 (一)  入门 前言: 官方分页工具,  确实香.   但 ...

  2. 孟老板 BaseAdapter封装 (二) Healer,footer

    BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...

  3. 孟老板 ListAdapter封装, 告别Adapter代码 (三)

    BaseAdapter系列 ListAdapter封装, 告别Adapter代码 (一) ListAdapter封装, 告别Adapter代码 (二) ListAdapter封装, 告别Adapter ...

  4. 孟老板 ListAdapter封装, 告别Adapter代码 (四)

    BaseAdapter系列 ListAdapter封装, 告别Adapter代码 (一) ListAdapter封装, 告别Adapter代码 (二) ListAdapter封装, 告别Adapter ...

  5. 孟老板 BaseAdapter封装(五) ListAdapter

    BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...

  6. 孟老板 BaseAdapter封装 (一) 简单封装

    BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...

  7. 孟老板 BaseAdapter封装 (三) 空数据占位图

    BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...

  8. 孟老板 BaseAdapter封装(四) PageHelper

    BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...

  9. 孟老板 ListAdapter封装, 告别Adapter代码 (上)

    BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...

随机推荐

  1. java+selenium使用JS、键盘滑动滚动条

    本篇文章介绍如何使用JS和键盘对象对页面进行滑动滚动条-------------主要针对java做自动化测试的同学 一:使用键盘对象操作滚动条 //导包 import org.openqa.selen ...

  2. spring源码解析之属性编辑器propertyEditor

    异常信息造成此异常的原因bean配置文件调用代码特别说明:异常解决注册springt自带的属性编辑器 CustomDateEditor控制台输出属性编辑器是何时并如何被注册到spring容器中的?查看 ...

  3. MySQL5.7升级到8.0过程详解

    前言: 不知不觉,MySQL8.0已经发布好多个GA小版本了.目前互联网上也有很多关于MySQL8.0的内容了,MySQL8.0版本基本已到稳定期,相信很多小伙伴已经在接触8.0了.本篇文章主要介绍从 ...

  4. babylin使用思路

  5. JAVA 面试相关

    1. int和Integer有什么区别? 答:Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类 ...

  6. VSCode·备份&还原配置及拓展项

    阅文时长 | 0.54分钟 字数统计 | 924字符 主要内容 | 1.引言&背景 2.备份VSCode配置 3.还原VSCode配置 4.Syncing常用命令 5.声明与参考资料 『VSC ...

  7. 24.Qt Quick QML-Canvas和Context2D详解

    1.Canvas介绍Canvas是一个允许绘制直线和曲线.简单和复杂的形状.图形和引用的图形图像.它还可以添加文本.颜色.阴影.渐变和图案,并执行低级别像素操作.Canvas输出可以另存为图像文件或序 ...

  8. 折腾gcc/g++链接时.o文件及库的顺序问题

    gcc/g++链接时.o文件以及库的顺序问题 1 写在前面 最近换了xubuntu12.4,把原来的项目co出来编译的时候报"undefined reference to".猜测是 ...

  9. PID基础

    经常有人会问到PID的用法,今天小编在这里例举温度控制中的PID部分,希望能够把PID的具体应用说明白. 先说几个名词: 1.直接计算法和增量算法:这里的所谓增量算法就是相对于标准算法的相邻两次运算之 ...

  10. 如何正确地使用RecyclerView.ListAdapter

    默认是在一个fragment中实现RecyclerView. private inner class CrimeAdapter() : ListAdapter<Crime, CrimeHolde ...