Jetpack之Paging

Paging 3

Paging库的架构

Paging库组件在应用的三个层运行


paging3-library-architecture.png

Paging的关键组件

PagingSource

定义数据源,以及如何从该数据源检索数据,PagingSource 对象可以从任何单个数据源(包括网络来源和本地数据库)加载数据

RemoteMediator

将来自网络的页面数据加载到数据库中,但不会直接将数据加载到界面中

Pager

Pager 基于 PagingSource 对象和 PagingConfig 配置对象来构造在响应式流中的 PagingData 实例

PagingData

PagingData 对象是用于存放分页数据快照的容器,它会查询 PagingSource 对象并存储结果

PagingDataAdapter

一种处理分页数据的 RecyclerView 适配器

Paging数据库分页实例

添加依赖

implementation "androidx.room:room-runtime:2.3.0"
// optional - Paging 3 Integration
kapt("androidx.room:room-compiler:2.3.0")
implementation "androidx.paging:paging-runtime:3.1.0"

实现数据库相关

数据库访问使用Room框架

创建实体类User

// 使用@Entity注解,将User声明成了一个实体类
@Entity
data class User(var name: String, var age: Int) {
    // 使用@PrimaryKey注解将它设为了主键,再把autoGenerate参数指定成true,使得主键的值是自动生成的
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

创建UserDao

Dao必须使用接口,访问数据库的操作无非就是增删改查这4种,但是业务需求却是千变万化的。而Dao要做的事情就是覆盖所有的业务需求,使得业务方永远只需要与Dao层进行交互,而不必和底层的数据库打交道

// 使用了一个@Dao注解,这样Room才能将它识别成一个Dao
@Dao
interface UserDao {
    @Insert
    fun insertUser(user: User): Long

    // 将方法中传入的参数指定到SQL语句
    @Query("select * from User where age > :age")
    fun loadAllUserOlderThan(age: Int): PagingSource<Int, User>
}

注意:上面的 loadAllUserOlderThan 方法返回值为 PagingSource<Int, User>,代表 Paging 的数据源,Int 代表传入数据类型,User 代码获取的数据类型。使用 Room 框架时,从数据库获取数据可以直接封装为 PagingSource,但从网络获取的就需要自己定义 PagingSource 了

定义Database

定义好3个部分的内容:数据库的版本号、包含哪些实体类,以及提供Dao层的访问实例

// 使用@Database注解,在注解中声明数据库的版本号以及包含哪些实体类,多个实体类之间用逗号隔开
@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        private var instance: AppDatabase? = null

        @Synchronized
        fun getDatabase(context: Context): AppDatabase {
            instance?.let {
                return it
            }
            return Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app_database"
            ).build().apply {
                instance = this
            }
        }
    }
}

创建仓库类

object Repository {
    private const val PAGE_SIZE = 5
    val userDao = AppDatabase.getDatabase(MyApp.context).userDao()

    fun query(minAge: Int) = Pager(config = PagingConfig(PAGE_SIZE)) {
        userDao.loadAllUserOlderThan(minAge)
    }.flow
}

实现RecyclerView的Adapter

RecyclerView的Adapter必须继承自PagingDataAdapter并提供一个DiffUtil.ItemCallback对象,Paging 3在内部会使用DiffUtil来管理数据变化,不需要传递数据源给Adapter,因为数据源是由Paging 3在内部自己管理

class UserAdapter : PagingDataAdapter<User, UserAdapter.ViewHolder>(diffCallback) {

    inner class ViewHolder(private val binding: PagingRecyclerviewItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        /**
         * 绑定界面数据
         * @param user User
         */
        fun bind(user: User) {
            binding.userName.text = user.name
            binding.userAge.text = user.age.toString()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = DataBindingUtil.inflate<PagingRecyclerviewItemBinding>(
            LayoutInflater.from(parent.context),
            R.layout.paging_recyclerview_item,
            parent,
            false
        )
        val holder = ViewHolder(binding)
        binding.root.setOnClickListener {
            val pos = holder.bindingAdapterPosition
            "你点击了第${pos}个数据,信息为${getItem(pos)}".showToast(MyApp.context)
        }
        return holder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        item?.let { holder.bind(it) }
    }


    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<User>() {
            override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
                return oldItem == newItem
            }
        }
    }
}

实现ViewModel

class PagingViewModel : ViewModel() {
    /**
     * Paging查询
     */
    fun queryPaging(minAge: Int): Flow<PagingData<User>> {
        return Repository.query(minAge).cachedIn(viewModelScope)
    }

    /**
     * 插入数据
     * @param user User
     */
    fun insert(user: User) {
        viewModelScope.launch {
            Repository.insert(user)
        }
    }
}

Activity中使用

class PagingActivity : AppCompatActivity() {
    private lateinit var userAdapter: UserAdapter
    private val pagingViewModel by lazy {
        ViewModelProvider(this, PagingViewModelFactory()).get(PagingViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)

        userAdapter = UserAdapter()
        paging_recyclerview.adapter = userAdapter
        paging_recyclerview.layoutManager = LinearLayoutManager(this)

        // 用于给数据库插入一些数据
        insert_btn.setOnClickListener {
            val name = user_name.text.toString()
            val age = user_age.text.toString().toInt()
            pagingViewModel.insert(User(name, age))
        }

        val minAge = min_age.text.toString().toInt()
        lifecycleScope.launch {
            pagingViewModel.queryPaging(minAge).collect {
                LogUtil.d(javaClass.name, "onCreate: ${it.toString()}")
                userAdapter.submitData(it)
            }
        }
    }
}

Paging 网络请求分页实例

以获取笑话为例,请求数据使用 retrofit

创建实体类 Joke

data class Joke(
    val code: Int,
    val message: String,
    val result: List<Result>
) {
    data class Result(
        val comment: String,
        val down: String,
        val forward: String,
        val header: String,
        val images: Any,
        val name: String,
        val passtime: String,
        val sid: String,
        val text: String,
        val thumbnail: String,
        val top_comments_content: String,
        val top_comments_header: String,
        val top_comments_name: String,
        val top_comments_uid: String,
        val top_comments_voiceuri: String,
        val type: String,
        val uid: String,
        val up: String,
        val video: String
    )
}

创建网络请求接口

interface JokeService {
    @GET("getJoke")
    fun getJokes(@QueryMap params: Map<String, String>): Call<Joke>
}

创建数据源

使用 PagingSource<Key, Value> 类即可通过 Kotlin 协程进行异步加载,Key 定义了用于加载数据的标识符,也就是请求数据时的参数类型。例如,将 Int 页码传递给 Retrofit 来从网络加载各页 User 对象,应选择 Int 作为 Key 类型,选择 User 作为 Value 类型

// PagingSource的泛型,第一个表示页数的数据类型,一般是整型;第二个表示每一项的数据类型
class JokePagingSource(private val jokeService: JokeService) : PagingSource<Int, Joke.Result>() {
    override fun getRefreshKey(state: PagingState<Int, Joke.Result>): Int? {
        return null
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Joke.Result> {
        // 当前页数
        val curPage = params.key ?: 1
        // 每一页包含多少条数据
        val pageSize = params.loadSize
        // 获取当前页的数据,这里需要try一下,因为你第一次打开的时候就可能没网,
        // 因此要在catch块中返回LoadResult.Error,否则程序这里就挂掉了,
        // Adapter.addLoadStateListener 添加的监听,也不会有LoadState.Error状态
        try {
            val jokes = jokeService.getJokes(
                hashMapOf(
                    "page" to curPage.toString(),
                    "count" to pageSize.toString(),
                    "type" to "video"
                )
            ).await().result

            // 前一页和下一页
            val prevPage = if (curPage > 1) curPage - 1 else null
            var nextPage = if (jokes.isNullOrEmpty()) null else curPage + 1

            // 构建LoadResult.Page对象并返回,第一个参数是通过网络获取的列表数据
            return LoadResult.Page(jokes, prevPage, nextPage)
        } catch (e: Exception) {
            LogUtil.d(javaClass.name, "load: ${e.message}")
            return LoadResult.Error(e)
        }
    }
}

仓库类

object Repository {

    private const val PAGE_SIZE = 5
    private val jokeService = ServiceCreator.create<JokeService>()

    /**
     * 获取笑话
     * @return Flow<PagingData<Result>>
     */
    fun getJokes(): Flow<PagingData<Joke.Result>> = Pager(
        config = PagingConfig(PAGE_SIZE), pagingSourceFactory = { JokePagingSource(jokeService) }
    ).flow
}

ViewModel

class PagingViewModel : ViewModel() {
    /**
     * 请求笑话
     * @return Flow<PagingData<Joke.Result>>
     */
    fun getJokes(): Flow<PagingData<Joke.Result>> {
        // cachedIn()用于将数据在viewModelScope这个作用域内进行缓存,这样旋转手机就会不再发出网络请求
        return Repository.getJokes().cachedIn(viewModelScope)
    }
}

实现RecyclerView的Adapter

我这里为了简单,直接复用的前面 “Paging数据库分页实例” 的同一个布局,也只是显示了请求的两个文本内容

class JokeAdapter : PagingDataAdapter<Joke.Result, JokeAdapter.ViewHolder>(diffCallback) {

    inner class ViewHolder(private val binding: PagingRecyclerviewItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        /**
         * 绑定界面数据
         * @param joke Joke.Result
         */
        fun bind(joke: Joke.Result) {
            binding.userName.text = joke.name
            binding.userAge.text = joke.text
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = DataBindingUtil.inflate<PagingRecyclerviewItemBinding>(
            LayoutInflater.from(parent.context),
            R.layout.paging_recyclerview_item,
            parent,
            false
        )
        val holder = ViewHolder(binding)
        binding.root.setOnClickListener {
            val pos = holder.bindingAdapterPosition
            "你点击了第${pos}个数据,信息为${getItem(pos)}".showToast(MyApp.context)
        }
        return holder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        item?.let { holder.bind(it) }
    }


    companion object {
        val diffCallback = object : DiffUtil.ItemCallback<Joke.Result>() {
            override fun areItemsTheSame(oldItem: Joke.Result, newItem: Joke.Result): Boolean {
                return oldItem.sid == newItem.sid
            }

            override fun areContentsTheSame(oldItem: Joke.Result, newItem: Joke.Result): Boolean {
                return oldItem == newItem
            }
        }
    }
}

底部适配器

class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {
    inner class ViewHolder(private val binding: PagingFooterBinding) :
        RecyclerView.ViewHolder(binding.root) {
        /**
         * 如果是正在加载中那么就显示加载进度条,如果是加载失败那么就显示重试按钮
         * @param loadState LoadState
         */
        fun bind(loadState: LoadState) {
            binding.footerProgressBar.isVisible = loadState is LoadState.Loading
            binding.footerRetry.isVisible = loadState is LoadState.Error
        }
    }

    override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
        holder.bind(loadState)
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
        val binding = DataBindingUtil.inflate<PagingFooterBinding>(
            LayoutInflater.from(parent.context),
            R.layout.paging_footer,
            parent,
            false
        )
        // 点击重试
        binding.footerRetry.setOnClickListener {
            retry()
        }
        return ViewHolder(binding)
    }
}

Activity中使用

class PagingActivity : AppCompatActivity() {
    private val pagingViewModel by lazy {
        ViewModelProvider(this, PagingViewModelFactory()).get(PagingViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)

        // 下面是使用Paging进行网络请求笑话的
        val jokeAdapter = JokeAdapter()
        // withLoadStateFooter添加重试,FooterAdapter布局的高度记得不要设为match_parent,否则加载失败后,还可以滑动
        paging_recyclerview.adapter =
            jokeAdapter.withLoadStateFooter(FooterAdapter { jokeAdapter.retry() })
        paging_recyclerview.layoutManager = LinearLayoutManager(this)

        lifecycleScope.launch {
            pagingViewModel.getJokes().collect {
                jokeAdapter.submitData(it)
            }
        }

        /**
         * 监听加载状态,控制的是第一次加载数据的状态,之后滑到下面加载失败,是由FooterAdapter控制的
         */
        jokeAdapter.addLoadStateListener {
            it.refresh.let { loadState ->
                progress_bar.isVisible = loadState is LoadState.Loading
                retry.isVisible = loadState is LoadState.Error
            }
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容