Paging3 组件是谷歌公司推出的分页加载库。个人认为Paging3库是非常强大,但是学习难点比较大的一个库。Paging3组件可用于加载和显示来自本地存储或网络中更大的数据集中的数据页面。此方法可让移动应用更高效地利用网络带宽和系统资源。在具体实现上,Paging3与前面的版本完全不同。
val paging_version = "3.2.0"
implementation("androidx.paging:paging-runtime:$paging_version")
implementation("androidx.paging:paging-compose:$paging_version")
// optional - RxJava3 support
implementation("androidx.paging:paging-rxjava3:$paging_version")
//支持viewmodel
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
//增加RxJava库的依赖
implementation("io.reactivex.rxjava3:rxjava:3.0.7")
implementation("io.reactivex.rxjava3:rxandroid:3.0.0")
//增加Retrofit库的支持
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
//增加Retrofit支持RxJava3的CallAdapter
implementation("com.squareup.retrofit2:adapter-rxjava3:2.9.0")
Paging3的架构分为三层,如下图所示:
PagingSource:定义数据源(来自网络或来自本地数据库,已经从该数据源检索数据);
RemoteMediator:用来处理来自分层数据(例如:来自缓存的网络数据源)的分页
Pager组件提供了一个公共 API,基于PagingSource 对象和 PagingConfig配置对象来构造在响应式流中公开的 PagingData 实例。
将 ViewModel 层连接到界面的组件是PagingData。PagingData 对象是用于存放分页数据快照的容器。它会查询PagingSource对象并存储结果。
界面层通过结合Compose组件的LazyColumn以列表方式显示分页加载的数据。
为了更好地解释Paging3组件,笔者爬取了一些视频数据保存到本地MySQL数据库,并结合Python+Flaskj将MySQL数据库的保存的数据生成按照页面和每一页展示数据的个数生成对应json数据,通过这种方式获得网络资源。
这时可以通过浏览器浏览相关内容,类似下图所示:这里传递了两个参数 page表示第几页,size表示页面显示记录数。
上列展示的json数组包含了多个json对象,每个json对象的格式类似下列形式:
{"actors":"演员",
"directors":"导演",
"intro":"电影简介",
"poster":"http://localhost:5000/photo/s_ratio_poster/public/p2626067725.jpg",
"region":"地区",
"release":"发布年份",
"trailer_url":"https://localhost:5000/trailer/268661/#content",
"video_url":"https://localhost:5000/d04d3c0d2132a29410dceaeefa97e725/view/movie/M/402680661.mp4"}
当然,在具体实现时也可以考虑JavaEE + Tomcat来搭建后台应用。
在这里,是通过访问单一网络数据源"http://127.0.0.1:5000/film.json?page=1&size=5"来获取分页数据,如上图所示。具体的架构如下图所示:
data class Film(
@SerializedName(“name”)
val name:String,
@SerializedName(“release”)
val release:String,
@SerializedName(“region”)
val region:String,
@SerializedName(“directors”)
val directors:String,
@SerializedName(“actors”)
val actors:String,
@SerializedName(“intro”)
val intro:String,
@SerializedName(“poster”)
val poster:String,
@SerializedName(“trailer_url”)
val trailer:String,
@SerializedName(“video_url”)
val video:String
)
在此处说明一下,@SerializedName表示对应json形式的单一film的数据。这样就可以在后续的处理中将请求的json数据根据json数据的关键字获取对应的值,利用这些值生成对应Film对象。
interface FilmApiService {
@GET("film.json")
suspend fun getData(
@Query("page") page:Int,
@Query("size") size:Int
):List<Film>
}
此处,page属性对应URL中的page表示页,size表示每一页的记录数;
object RetrofitBuilder {
private const val BASE_URL = "http://10.0.2.2:5000/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService:FilmApiService =
getRetrofit().create(FilmApiService::class.java)
}
(1)定义代码层要实现的操作接口
interface FilmRepository {
/**
* 获取指定page页面的信息
* @param page Int
* @return List<Film>
*/
suspend fun getFilms(page:Int,limit:Int = 5):List<Film>
}
4.P
(2)具体代码层的实现
class FilmRepositoryImp:FilmRepository {
private val apiService: FilmApiService = RetrofitBuilder.apiService
override suspend fun getFilms(page: Int,limit:Int): List<Film>
= apiService.getData(page, limit)
}
class FilmSource(private val filmRepository: FilmRepository): PagingSource<Int, Film>() {
override suspend fun load(params: LoadParams<Int>):LoadResult<Int, Film> {
return try{
val currentPage = params.key ?:1
Log.d("请求页面标记:","请求第${currentPage}页")
val filmResponse = filmRepository.getFilms(currentPage)
val prevKey = if(currentPage==1) null else currentPage-1
val nextKey = if(filmResponse.isEmpty()) null else currentPage+1
LoadResult.Page(
data = filmResponse,
prevKey = prevKey,
nextKey = nextKey
)
}catch(e:Exception){
if (e is IOException) {
Log.d("测试错误数据", "-------连接失败")
}
Log.d("测试错误数据", "-------${e.message}")
LoadResult.Error(throwable = e)
}
}
override fun getRefreshKey(state: PagingState<Int, Film>): Int? {
return state.anchorPosition
}
}
class MainViewModel: ViewModel() {
private val filmRepository: FilmRepository = FilmRepositoryImp()
fun getFilms(): Flow<PagingData<Film>> = Pager(PagingConfig(pageSize = 5)){
FilmSource(filmRepository)
}.flow
}
在上面的代码中配置每一页的记录数默认为5,在函数getFilms中获得一个协程流的数据封装了每页的记录。
(1)定义单独Film一行记录的界面内容
@Composable
fun FilmCard(film: Film?) {
Card(modifier = Modifier.fillMaxSize().padding(2.dp),
elevation = CardDefaults.cardElevation(5.dp),
colors = CardDefaults.cardColors(containerColor = Color.Black)){
Column{
Row(modifier = Modifier.fillMaxSize()){
AsyncImage(model = "${film?.poster}",
contentDescription = "${film?.name}")
Column{
Text("${film?.name}",fontSize = 18.sp,color = Color.White)
Text("导演:${film?.directors}",fontSize = 14.sp,color = Color.Green)
Text("演员:${film?.actors}", fontSize = 14.sp,color = Color.White)
}
}
Text("${film?.intro?.subSequence(0,50)} ...",fontSize = 14.sp,color=Color.Green)
Row(horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxSize()){
Text("More",fontSize=12.sp)
IconButton(onClick ={}){
Icon(imageVector = Icons.Default.MoreVert,tint=Color.Green,contentDescription = "更多...")
}
}
}
}
}
(2)定义列表
@Composable
fun FilmScreen(mainViewModel: MainViewModel) {
val films:LazyPagingItems<Film> = mainViewModel.getFilms().collectAsLazyPagingItems()
val TAG = "加载状态"
Column(horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.background(Color.White)){
LazyColumn{
items(films.itemCount){
FilmCard(films[it])
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val mainViewModel:MainViewModel = viewModel()
Ch11_DemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
FilmScreen(mainViewModel)
}
}
}
}
}
运行效果如下:
Paging库概览
https://developer.android.google.cn/topic/libraries/architecture/paging/v3-overview?hl=zh-cn