孟老板 Paging3 (二) 结合Room
Paging3 (二) 结合Room
Paging 数据源不开放, 无法随意增删改操作; 只能借助 Room;
这就意味着: 从服务器拉下来的数据全缓存. 刷新时数据全清再重新缓存, 查询条件变更时重新缓存 [让我看看]
当Room数据发生变化时, 会使内存中 PagingSource
失效。从而重新加载库表中的数据
Room: 官方文档点这里
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. 第一步, 创建实体类.
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的更多相关文章
- 孟老板 Paging3 (一) 入门
BaseAdapter系列 ListAdapter系列 Paging3 (一) 入门 Paging3 (二) 结合 Room Paging3 (一) 入门 前言: 官方分页工具, 确实香. 但 ...
- 孟老板 BaseAdapter封装 (二) Healer,footer
BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...
- 孟老板 ListAdapter封装, 告别Adapter代码 (三)
BaseAdapter系列 ListAdapter封装, 告别Adapter代码 (一) ListAdapter封装, 告别Adapter代码 (二) ListAdapter封装, 告别Adapter ...
- 孟老板 ListAdapter封装, 告别Adapter代码 (四)
BaseAdapter系列 ListAdapter封装, 告别Adapter代码 (一) ListAdapter封装, 告别Adapter代码 (二) ListAdapter封装, 告别Adapter ...
- 孟老板 BaseAdapter封装(五) ListAdapter
BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...
- 孟老板 BaseAdapter封装 (一) 简单封装
BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...
- 孟老板 BaseAdapter封装 (三) 空数据占位图
BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...
- 孟老板 BaseAdapter封装(四) PageHelper
BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...
- 孟老板 ListAdapter封装, 告别Adapter代码 (上)
BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...
随机推荐
- Git 系列教程(8)- 远程仓库的使用
查看远程仓库 如果想查看你已经配置的远程仓库服务器,可以运行 git remote 命令,它会列出你指定的每一个远程服务器的名称 如果是刚 clone 下来的自己的库,能看到 origin,这是 Gi ...
- 运行程序显示丢失“MSVCR100D.dll”
前言 写了一个Dll注入工具,结果发现程序在其他机器上会出现丢失"MSVCR100D.dll".这个dll是vs2010自带的动态链接库,如果在没安装vs2010运行库的电脑中使用 ...
- 2020 ICPC EC Final西安现场赛游记
也不知道从何说起,也不知道会说些什么,最想表达的就是很累很累. 从第一天去的时候满怀希望,没什么感觉甚至还有一些兴奋.到后来一直在赶路,感觉很疲惫,热身赛的时候觉得马马虎虎,导致热身赛被咕.然后教练就 ...
- 如何用Vim搭建IDE?
推荐:http://harttle.com/2015/07/18/vim-cpp.html 转自:http://harttle.com/2015/11/04/vim-ide.html 一年前我从Vim ...
- OO_Unit4_UML模型化设计
CSDN博客传送门 @ 目录 一.架构设计 (一)第一次作业 作业需求分析 建立类图 bug修复 (二)第二次作业 作业需求分析 建立类图 checkForUml002实现方式 checkForUml ...
- 调用免费API查询全年工作日、周末、法定节假日、节假日调休补班数据
前言 日常开发中,难免会用到判断今天是工作日.周末.法定节假日.节假日调休补班做一些业务处理,例如:仅在上班时间给用户推送消息.本文记录调用免费API查询全年工作日.周末.法定节假日.节假日调休补班数 ...
- Go快速入门(二)
提示:本系列文章适合有其他语音基础并对Go有持续冲动的读者 一.package介绍 Go语言的代码是通过package来组织的,package的概念和你知道的其它语言 里的libraries或者m ...
- 深入源码,深度解析Java 线程池的实现原理
java 系统的运行归根到底是程序的运行,程序的运行归根到底是代码的执行,代码的执行归根到底是虚拟机的执行,虚拟机的执行其实就是操作系统的线程在执行,并且会占用一定的系统资源,如CPU.内存.磁盘.网 ...
- too much recursion
今天在火狐浏览器上调试swagger接口遇到一个浏览器报错: too much recursion 刚开始以为接口出问题了,但是调试之后发现,后台有数据返回,往下一拉,看到了差不多两千多条数据,一下子 ...
- Aliyun SSL 证书签发&安装
目录 HTTPS SSL证书 签发 和 应用 证书购买 证书申请 证书安装 参考文档 HTTPS SSL证书 签发 和 应用 - SSL证书服务(Alibaba Cloud SSL Certifica ...