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. 【opencv】Java实现opencv 调用本地摄像头,实现人脸识别、人形识别、人眼识别

    本博客为老魏原创,如需转载请留言咨询. 效果预览:(没办法,为了效果只能上像了,丑别介意.哈哈..) 上代码: 1 package com.lw.test; 2 3 import java.awt.G ...

  2. Java GUI入门手册-AWT篇

    Java GUI入门手册: AWT是基本的GUI设计工具,重点学习其中的布局格式以及事件监听事件. 首先创建一个窗口,我们先分析Frame类中的方法: 通过上图,可以看出frame是由构造方法的重载: ...

  3. 论文翻译:Conv-TasNet: Surpassing Ideal Time–Frequency Magnitude Masking for Speech Separation

    我醉了呀,当我花一天翻译完后,发现已经网上已经有现成的了,而且翻译的比我好,哎,造孽呀,但是他写的是论文笔记,而我是纯翻译,能给读者更多的思想和理解空间,并且还有参考文献,也不错哈,反正翻译是写给自己 ...

  4. [前端] JSON

    背景 JavaScript对象表示法(JavaScript Object Notation):是一种存储数据的方式 JSON对象 创建 var gareen = {"name":& ...

  5. Django/Flask的一些实现方法

    一.导出当前项目用到的依赖到requirements.txt文件中 pip freeze > requirements.txt 二.安装当前项目需要的依赖: pip install -r req ...

  6. 《SystemVerilog验证-测试平台编写指南》学习 - 第2章 数据类型

    <SystemVerilog验证-测试平台编写指南>学习 - 第2章 数据类型 2.1 内建数据类型 2.2 定宽数组 2.2.1 声明 2.2.2 常量数组 2.2.3 基本的数组操作 ...

  7. 命令stat anaconda-ks.cfg会显示出文件的三种时间状态(已加粗):Access、Modify、Change。这三种时间的区别将在下面的touch命令中详细详解:

    7.stat命令 stat命令用于查看文件的具体存储信息和时间等信息,格式为"stat 文件名称". stat命令可以用于查看文件的存储信息和时间等信息,命令stat anacon ...

  8. C语言规范:C89、C90、C95、C99

    本文转载 [K&R C] 1978 年,Dennis Ritchie 和 Brian Kernighan 合作推出了<The C Programming Language>的第一版 ...

  9. Guava Cache,Java本地内存缓存使用实践

    Guava Cache,网上介绍很多,我就不赘述了. 分享一篇好的文章: Guava Cache内存缓存使用实践-定时异步刷新及简单抽象封装 Google Guava 3-缓存 在原作者基础上,我做了 ...

  10. Linux资源监控工具 glances

    使用资源监控工具 glances 前言 glances 可以为 Unix 和 Linux 性能专家提供监视和分析性能数据的功能,其中包括: CPU 使用率 内存使用情况 内核统计信息和运行队列信息 磁 ...