Paging3-分页数据加载库(结合Room)


前言

上一篇写了Paging3的基本使用. 这一片 结合Room, 实现 NetWork 和 Db 数据处理. 实现 条目增删改操作.

下面是Paging存在的问题:

  • 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. 实体类

首先实体类需要映射数据库中的表及字段.

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

BaseItem

interface BaseItem {
    var hasChanged: Boolean
    fun getMItemType(): Int = 0
}
interface BaseCheckedItem : BaseItem{
    var hasChecked: Boolean     // 是否被勾选中
}

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类;

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

4.1上代码

上代码, 让我们先把代码敲起来

@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 {
        	//获取页码 key
            val loadKey = when (loadType) {
                //表示 刷新.
                LoadType.REFRESH -> null    //loadKey 是页码标志, null代表第一页;
                LoadType.PREPEND ->
                    return MediatorResult.Success(endOfPaginationReached = true)
                LoadType.APPEND -> {
                    val lastItem = state.lastItemOrNull()
                    
                    //这里用 NoMoreException 方式显示没有更多;
                    if(index>=15){
                        return MediatorResult.Error(NoMoreException())
                    }

                    if (lastItem == null) {
                        return MediatorResult.Success(
                            endOfPaginationReached = true
                        )
                    }

                    lastItem.index
                }
            }

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

createListData() 是模拟数据, 就不用多说了.

4.2 内容讲解

LoadType:
表示加载状态的密封类.

类名意义
LoadType.REFRESH表示刷新, loadKey=null 表示第一页
LoadType.PREPEND分页开始前会调用一次, 类似于确认是否分页
LoadType.APPEND数据追加, 此时需要从服务器拉数据

MediatorResult:
表示 加载请求结果的密封类.

类名意义
MediatorResult.Error表示加载请求失败,类似于 LoadResult; 可以承载失败异常信息, 可以做 没有更多
MediatorResult.Success表示加载请求成功.
endOfPaginationReached = true 表示仍有未加载数据, 可以加载更多
endOfPaginationReached = false 表示最后一页了, 没有更多数据可供加载

整个 Load() 看起来代码不少. 实际上就三小步

  • 第一步: 根据 loadType 判断并获取 加载的 key(可以理解为页码); 并注意额外状态返回即可
  • 第二步: 网络请求拉取数据, 博主这里用的模拟数据
  • 第三步: 将数据持久化存储, 如果是刷新状态 则先清空旧数据

页码获取方式:

  • 官方文档用的 lastItem.id 方式, 意思就是从这个ID后面加载数据. 如果是用页码的就不太合适了. 而且这方式似乎有问题, 在博主的模拟数据中, 第一页last.index 应当是9. 但博主这里总是0
  • 也可以数据库存储. SharePrefences等存储当前页码
  • 如果room仅用作配合Paging. 而不设置数据的有效状态或有效时长时, 则可以直接在 RemoteMediator 中定义变量页码计数;

4.3 initialize()

有的时候,我们刚更新的数据, 此时立刻再更新一遍数据 无异于是对网络资源和设备性能的一种浪费. 此时我们只想用 db 中的数据,而不需要从服务器拉数据.

此时就要重写 initialize() 告诉 RemoteMediator: 数据是否仍然有效;

返回值意义
InitializeAction.SKIP_INITIAL_REFRESH表示数据有效, 无需刷新
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(Dispatchers.IO) {
            RoomTestDatabase.getInstance(getApplication()).roomDao().updRoom(info)
        }
    }

    fun del(info: RoomEntity) {
        viewModelScope.launch(Dispatchers.IO) {
            RoomTestDatabase.getInstance(getApplication()).roomDao().deleteById(info.id)
        }
    }
}

6.DiffCallback

看过我 ListAdapter 系列 的小伙伴,应该知道. 我曾经用 状态标记方式作为 判断 Item 是否变化的依据; 但是在 Paging+Room 的组合中, 就不能这样用了;

原因:

  • 在Paging中 列表数据的改变, 完全取决于 Room 数据库中存储的数据.
  • 当我们要删除或点赞操作时, 必须要更新数据库指定条目的内容;
  • 而当数据库中数据发生改变时, PagingSource 失效, 原有对象将会重建. 所以 新旧 Item 可能不再是同一实体, 也就是说内存地址不一样了.

OK, 我们上代码

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.开始使用

7.1 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)
        }
    }
}

7.2 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))
        }
    }

}

7.3 layout

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

8.点赞无效, 有八哥?

原来这段代码有问题:

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 == newItem
}

怎么办? 只能让旧实体的数据不变化: 如下所示, 单独写更新Sql; 或者 copy 一个新的实体对象再去更新db, 我只能说 那好吧!

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

9.最后 效果图

在这里插入图片描述


总结

  1. Paging 数据源不开放, 只能通过 Room 做增删改操作;
  2. 如果只要求存储第一页数据, 用于网络状态差时,尽快的页面渲染. 而强制整个列表持久化存储的话,博主认为这是一种资源浪费
  3. 本地增删改, 会让列表数据失效. 为了单条记录, 去重复创建整个列表对象. 无异于资源性能的浪费.
  4. 因为是用Equals判断条目变化, 所以需要额外注意, 旧对象的内容千万不要更改. 更新时要用 Copy 对象去做. 这很别扭;

博主对 Paging 的了解不算深, 源码也没看多少. 不知道上面几条的理解是否有偏差. 但就目前来看,博主可能要 从入门到放弃了 [苦笑]


上一篇: paging3-分页数据加载库(入门)
下一篇: Room-数据持久化存储(入门)

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
以下是一个使用 Kotlin 和 Paging3+RecyclerView 实现数据分页的示例: 1. 首先,我们需要定义一个包含数据的列表和当前页码的类,使用 PagingData 类型: ``` data class Data( val id: Int, val name: String, val description: String ) ``` 2. 接下来,我们需要定义一个 PagingSource,它负责从数据源获取数据: ``` class DataPagingSource(private val api: DataApi) : PagingSource<Int, Data>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> { try { val page = params.key ?: 1 val pageSize = params.loadSize val response = api.getData(page, pageSize) val data = response.data val prevKey = if (page == 1) null else page - 1 val nextKey = if (data.isEmpty()) null else page + 1 return LoadResult.Page( data = data, prevKey = prevKey, nextKey = nextKey ) } catch (e: Exception) { return LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, Data>): Int? { return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } } } ``` 3. 然后,我们需要定义一个 PagingData 类型的数据流,并使用它创建一个 PagingDataAdapter: ``` class DataAdapter : PagingDataAdapter<Data, DataViewHolder>(DataDiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_data, parent, false) return DataViewHolder(view) } override fun onBindViewHolder(holder: DataViewHolder, position: Int) { val item = getItem(position) holder.bindData(item) } object DataDiffCallback : DiffUtil.ItemCallback<Data>() { override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean { return oldItem == newItem } } } ``` 4. 最后,在 Activity 或 Fragment 中使用 RecyclerView 和 DataAdapter: ``` class MainActivity : AppCompatActivity() { private lateinit var recyclerView: RecyclerView private lateinit var adapter: DataAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) recyclerView = findViewById(R.id.recycler_view) recyclerView.layoutManager = LinearLayoutManager(this) adapter = DataAdapter() recyclerView.adapter = adapter val api = DataApi() val pagingSourceFactory = { DataPagingSource(api) } val pagingConfig = PagingConfig(pageSize = 10) val dataFlow = Pager( config = pagingConfig, pagingSourceFactory = pagingSourceFactory ).flow dataFlow.cachedIn(lifecycleScope).collectLatest { pagingData -> adapter.submitData(pagingData) } } } ``` 这是一个简单的示例,你可以根据自己的需求进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值