当前位置 博文首页 > 孟老板:孟老板 Paging3 (二) 结合Room

    孟老板:孟老板 Paging3 (二) 结合Room

    作者:孟老板 时间:2021-06-22 18:26

    Paging3 结合 Room使用. 实现 NetWork 和 Db 数据处理. 实现 条目增删改操作. MVVM模式数据绑定. 以及部分注意事项.
    • BaseAdapter系列
    • ListAdapter系列
    • Paging3 (一) 入门
    • Paging3 (二) 结合 Room

    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