Paging 3
Paging库的架构
Paging库组件在应用的三个层运行
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
}
}
}
}