前言
在上一篇中,主要讲解了Jetpack—Paging2的故事。因为Paging3改动较大,并且为了让更多人同时适应两个版本,因此在本篇中将会结合Flow与Paging3进行组合讲解。
因本篇主要围绕着Paging3以及与Paging2的区别进行讲解!
因此,本篇所需要知识点:
- ViewBinding+DataBinding+Flow+ViewModel+KT+协程(不够的,可以看看之前写好的文章)
- 熟悉paging2者,阅读体验更佳(没有也可尝试看看)
1、回顾Jetpack—Paging2
回顾一下上一篇所讲解的Paging2!
是不是提到过三大核心类??
那么!Paging3的核心类呢?有哪些呢?
- Paging3核心类之PagingDataAdapter(原PagedListAdpater)
- Paging3核心类之PagingData(原PagedList)
- Paging3核心类之PagingSource(原拥有三种格式的DataSource)
那么它们之间的改动就仅仅是命名上的改动么?
NO!NO!NO!要是真是如此,也不会单独另开一篇讲解了!
2、开讲Jetpack—Paging3
2.1 配置准备
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
...略
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding = true
}
dataBinding {
enabled = true
}
...略
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
implementation 'androidx.paging:paging-runtime:3.0.0-alpha03'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
这里可以看到,我这里同时开启了ViewBinding与DataBinding
,然后引入了kotlin-kapt
插件和一系列库。
网络权限以及允许Http不可少
<uses-permission android:name="android.permission.INTERNET" />
<application
...略
android:networkSecurityConfig="@xml/network_security_config">
<activity android:name=".activity.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
这没啥说的
network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
到这里准备工作做好了,现在依然按照上面顺序依次讲解Paging3!
2.2 PagingDataAdapter
class MovieAdapter(private val context: Context) :
PagingDataAdapter<Movie, BindingViewHolder>(object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
// kotlin === 引用 , == 内容
// Java == 引用, equals 内容
return oldItem == newItem
}
}) {
override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
val movie = getItem(position)
movie?.let {
//DataBinding相关逻辑,这里不做详解
val binding = holder.binding as MovieItemBinding
binding.movie = it
binding.networkImage = it.cover
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
val binding = MovieItemBinding.inflate(LayoutInflater.from(context), parent, false)
return BindingViewHolder(binding)
}
}
我们可以看到,除了Item改为DataBinding外,对应的Adapter和之前差不多,也需要实现对应的差分比较。而且逻辑和之前得到相似。
不过对应的BindingViewHolder
为了等哈方便,因此单开了一个类(没有写成类部类)
class BindingViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root)
结合ViewBinding,代码就这光溜溜的一句。
当然还有Json实体类
data class Movies(
@SerializedName("subjects")
val movieList: List<Movie>,
@SerializedName("has_more")
val hasMore: Boolean
)
这里准备用上一篇的4.2格式的返回结构!
总结
这个核心类感觉和之前Paging2的使用效果差不多。
那看看下一个!
2.3 PagingData
class MovieViewModel : ViewModel() {
private val movies by lazy {
Pager(
config = PagingConfig(pageSize = 8, prefetchDistance = 8, initialLoadSize = 16),
pagingSourceFactory = { MoviePagingSource() })
.flow //转为Flow流
.cachedIn(viewModelScope) //使其生命周期与ViewModel相互绑定
}
fun loadMovie(): Flow<PagingData<Movie>> = movies
}
代码解析:
-
我们这里可以看到在这通过懒加载lazy,实例化了Pager对象
pageSize
毋庸置疑,这里表示每页长度为多少prefetchDistance
预取距离,表示必须离加载内容边缘多远才能触发进一步加载。一般设置为大于0,小于等于pageSize
initialLoadSize
来自PagingSource 的初始加载定义请求的加载大小,通常大于pageSize ,因此在第一次加载数据时,加载的内容范围足够大以覆盖小滚动。
-
并将对应的Page对象转为了Flow流,然后通过
cachedIn
使其生命周期与ViewModel相互绑定 -
最后通过
loadMovie
方法,将对应的Flow返回出去
这里我们看到,在初始化Pager对象时,使用了pagingSourceFactory = { MoviePagingSource() }
指定了对应工厂为MoviePagingSource
因此
2.4 PagingSource
class MoviePagingSource : PagingSource<Int, Movie>() {
//currentPage,pageSize
//1,16
//3,8
//4,8
//prevKey,nextKey
//null,3
//2,4
//3,5
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
delay(2000) //这里为了显示加载更多的效果所以特意挂起2秒
val currentPage = params.key ?: 1
val pageSize = params.loadSize
val movies = RetrofitClient.createApi(MovieApi::class.java).getMovies(currentPage, pageSize)
Log.d("hqk", "currentPage:$currentPage,pageSize:$pageSize")
var prevKey: Int? = null
var nextKey: Int? = null
val realPageSize = 8
val initialLoadSize = 16
if (currentPage == 1) {
prevKey = null
nextKey = initialLoadSize / realPageSize + 1
} else {
prevKey = currentPage - 1
nextKey = if (movies.hasMore) currentPage + 1 else null
}
Log.d("hqk", "prevKey:$prevKey,nextKey:$nextKey")
return try {
LoadResult.Page(
data = movies.movieList,
prevKey = prevKey,
nextKey = nextKey
)
} catch (e: Exception) {
e.printStackTrace()
return LoadResult.Error(e)
}
}
}
注意哟,这里重点来了!
-
还记得上面创建
PagingConfig
这个对象时所传入的参数吧! -
第三个参数
initialLoadSize = 16
表示首次将会加载16条数据(这里官方做法也比pageSize大,最好是pageSize的n倍) -
那么在这里第一页加载也就是首次加载,也会加载16条数据,而我们设置的
pageSize=8
, -
也就是说,首次加载第一页的时候,已经成功的加载了第一页和第二页数据,
-
因此在加载下一页的时候,就应该从第三页开始加载
//currentPage,pageSize //分别表示:当前页数,以及对应页数加载的个数 //1,16 //3,8 //4,8 //prevKey,nextKey //当前页数,下一页数,null表示首次加载第一页 //null,3 //2,4 //3,5
-
也就是说:从第三页开始,后面的每一页的个数才是对应
pageSize
所设置的条数
再次注意《重点!!!》
- 这里一定要做对应处理,就算你没有设置
initialLoadSize
属性,官方默认在对应属性上扩大了3倍pageSize
。 - 如果你没有处理
prevKey,nextKey
,那么你第二页和第三页加载的都是首次已经加载过的数据 ! - 你将会在列表上看到有两页数据重复的bug!
来看看官方代码
@JvmField
@IntRange(from = 1)
//这里默认乘以3
val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,
接着,
2.5 来看看如何使用
class MainActivity : AppCompatActivity() {
private val movieViewModel by viewModels<MovieViewModel>()
private val mBinding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
val movieAdapter = MovieAdapter(this)
mBinding.apply {
recyclerView.adapter = movieAdapter.withLoadStateFooter(MovieLoadMoreAdapter(this@MainActivity))
swipeRefreshLayout.setOnRefreshListener {
movieAdapter.refresh()
}
}
lifecycleScope.launchWhenCreated {
movieViewModel.loadMovie().collectLatest {
movieAdapter.submitData(it)
}
}
lifecycleScope.launchWhenCreated {
movieAdapter.loadStateFlow.collectLatest { state ->
mBinding.swipeRefreshLayout.isRefreshing = state.refresh is LoadState.Loading
}
}
}
}
注意
- 这里使用
movieAdapter.withLoadStateFooter(MovieLoadMoreAdapter(this@MainActivity))
, - 设置了对应Adapter对应的加载尾(加载更多)为
MovieLoadMoreAdapter
最后来看看运行效果
哈哈哈哈,够具体吧,这样还不明白就来打我。坐标成都!!
Demo地址: 点我下载
结束语
好了,本篇到这里就结束了,相信看到这里的小伙伴已经对Jetpack-Paging3有了相当深刻的认知。下一篇将会结合Jetpack前面所讲解的知识点,整合成一个实战应用!