孟老板 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"
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!! } } }
官方解释:
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 } }
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