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. ArcGIS JS API使用PrintTask打印地图问题解决汇总

    环境:来源于工作过程,使用的API是  arcgis js 3.*  3系API,4.*暂时没测试: 1.数据与打印服务跨域情况下,不能打印问题. 一般情况下,我们发布的数据服务和打印服务是在一台服务 ...

  2. C++ primer plus读书笔记——第11章 使用类

    第11章 使用类 1. 运算符重载是一种形式的C++多态. 2. 不要返回指向局部变量或临时对象的引用.函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据. 3. 运算符重载的格式如下: ...

  3. uni-app 微信支付

    小程序 支付 先看官方文档 https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_2.shtml 知晓有那些比不可少的流程,之后后端 ...

  4. 关于在pycharm上使用git(保姆级别教程)

    文件 → 设置 先在pycharm上面登录github账号,版本控制 → GitHub → '+' → 通过GitHub登录 会自动跳转至浏览器,然后点击"Authorize in GitH ...

  5. 痞子衡嵌入式:串行NOR Flash的Continuous read模式下软复位后i.MXRT无法启动问题解决方案之RESET#

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是i.MXRT上使能NOR Flash的Continuous read模式在软复位后无法正常启动问题的解决经验. 前一篇文章 <在i ...

  6. Python批量图片去水印,提高工作效率

    ​平常工作中,有时为了采用网络的一些素材,但这些素材往往被打了水印,如果我们不懂PS就无法去掉水印,或者无法批量去掉水印.这些就很影响我们的工作效率. 今天我们就一起来,用Python + OpenC ...

  7. Zabbix 监控系统部署

    Zabbix 监控系统部署 实验环境 Zabbix server:RHEL8 ip:192.168.121.10 一.关闭防火墙和selinux [root@Zabbix-server ~]# sys ...

  8. strcpy和memcpy的区别-(转自stone Jin)

    strcpy和memcpy都是标准C库函数,它们有下面的特点.strcpy提供了字符串的复制.即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符. 已知strcpy函 ...

  9. 从栈上理解 Go语言函数调用

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/518 本文使用的go的源码 1.15.7 前言 函数调用类型 这篇文 ...

  10. 十一、.net core(.NET 6)搭建ElasticSearch(ES)系列之ElasticSearch、head-master、Kibana环境搭建

    搭建ElasticSearch+Kibana环境 前提条件:已经配置好JDK环境以及Nodejs环境.如果还未配置,请查看我的上一篇博客内容,有详细配置教程. 先下载ElasticSearch(以下文 ...