本期内容主要介绍在Compose中使用Paging实现静默上拉加载和下拉刷新功能。
Android Studio Giraffe | 2022.3.1 AS更新到Giraffe啦,新的UI视角上简约清爽,大家可以体验一波。
简述
Compose Paging是由Compose和Paging库结合的一种技术,主要就是帮助开发者解决列表的分页加载。首先熟悉下Paging中有几个重要的角色:
PagingSource
用于定义数据的来源和加载方式Pager
用于配置分页的大小和关联PagingSource
,并且可以通过flow()
将结果转换为Flow<PagingData<T>>
LazyPagingItems
它可以从PagingData
中收集数据值,并且在列表中展示其收集到的值。
使用
了解了Paging重要的角色之后,我们直接通过代码示例来熟悉如何在Compose中去使用它。
定义PagingSource
kotlin
class HomeArticleDataSource(
private val api: Api
) : PagingSource<Int, HomeArticleEntity.Data>() {
override fun getRefreshKey(state: PagingState<Int, HomeArticleEntity.Data>): Int? {
// 根据preKey和nextKey中找到离anchorPosition最近页面的键值
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, HomeArticleEntity.Data> {
// 定义键值
val currentKey = params.key ?: 0
return try {
val response = api.fetchHomeArticleList(currentKey)
val datas = response.data.datas
LoadResult.Page(
data = datas,
prevKey = if (currentKey == 0) null else currentKey - 1,
nextKey = if (currentKey == response.data.pageCount) null else currentKey + 1
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
}
首先我们要定义数据的加载方式,HomeArticleDataSource
继承了PagingSource
抽象类,其类内部有两个抽象方法
getRefreshKey()
这个方法主要就是在刷新时寻找key
值,实现也是很简单,根据前后的键值找到相连的键值;load()
方法是比较关键的地方,数据是从WanAndroid的APi中获取,通过LoadResult.Page()
返回,他有三个参数,第一个就是需要返回的数据,第二个则是前一个键值,只需要处理当前键值为0时传null
即可,第三个参数是下一个键值,也需要处理下当前键值为最终页时传null
即可;如果在获取数据时发生异常可以通过LoadResult.Error(exception)
返回。
PagingSource
的逻辑还是比较清晰的,如何获取数据源(接口、本地数据库)和管理好获取数据时前后的键值变化。
配置Pager
定义好数据获取的方式之后,此时就需要定义Pager的配置了,这就得交给Pager
对象来完成
kotlin
class HomeRepository(
private val api: Api
) {
fun fetchHomeArticleList(): Flow<PagingData<HomeArticleEntity.Data>> {
// 通过Pager.flow返回流对象
return Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = {
HomeArticleDataSource(api)
}
).flow
}
}
Pager
对象有两个参数,其一就是config
参数,用于管理每一页加载的数据大小,这里需要和接口对应,避免造成接口数据的混乱;第二个pagingSourceFactory
参数就是提供PagingSource
对象而已,我们只需要将之前定义好的PagingSource
传入即可。
最终可以通过Pager.flow
将数据转换成Flow<PagingData>
对象。
展示数据
以上操作都完成之后,此时我们就可以将获取到的数据显示到LazyColumn
中啦,并且在上滑的过程中几乎看不到加载下一页的过程,整体效果非常的丝滑流畅。


ViewModel
中articleList
还是Flow<PagingData<HomeArticleEntity.Data>>
对象,此时还不可以直接作用于Compose的LazyColumn
,我们需要再进行一步转换,通过collectAsLazyPagingItems()
方法转换成LazyPagingItems
对象,这样就可以直接在LazyColumn
中直接展示加载到的数据。最终的效果见下方录屏
通过录屏可以看出,在整个上滑的过程中就看不出来"加载更多"的提示啦。
下拉刷新
在Compose中如果想实现下拉刷新功能,可以直接使用accompanist-swiperefresh库,依赖如下
implementation("com.google.accompanist:accompanist-swiperefresh:$accompanist_version")

具体使用如上面代码,图片重点标红了四处地方,一个一个的解释下:
refresh
表示当前下拉刷新状态,默认为false
,它是一个State
状态哦pullRefreshState
对象是用于管理刷新状态和刷新之后具体的动作,这里直接调用articleList.refresh()
就可以触发Paging
进行数据的刷新动作Modifier.pullRefersh()
表示在哪个可组项上可以触发下拉刷新动作PullRefreshIndicator
是一个刷新指示器,也就是我们通常看到的转圈圈的动画
通过上述代码我们就完成了在Paging中下拉刷新数据的效果咯,下面是具体的效果

完整代码
scss
class HomeViewModel(
private val repository: HomeRepository
) : ViewModel() {
private val _state = MutableStateFlow(HomeState())
val state = _state.asStateFlow()
val articleList = repository.fetchHomeArticleList().cachedIn(viewModelScope)
}
data class HomeState(
val articleList: List<HomeArticleEntity.Data> = listOf()
)
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun HotScreen() {
val homeViewModel: HomeViewModel = koinViewModel()
// 通过collectAsLazyPagingItems()收集数据并转成LazyPagingItems对象,
// 此对象可直接作用于LazyColumn
val articleList = homeViewModel.articleList.collectAsLazyPagingItems()
val refresh by remember {
mutableStateOf(false)
}
val pullRefreshState = rememberPullRefreshState(refresh, {
articleList.refresh()
})
Box(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
.pullRefresh(pullRefreshState)
) {
LazyColumn(modifier = Modifier
.padding(8.dp), content = {
items(articleList.itemCount) {
val data = articleList[it] ?: return@items
HomeArticleItem(data = data)
}
})
PullRefreshIndicator(refresh, pullRefreshState, Modifier.align(Alignment.TopCenter))
}
}
@Preview
@Composable
fun HomeArticleItem(data: HomeArticleEntity.Data = HomeArticleEntity.Data()) {
Box(modifier = Modifier.fillMaxWidth()) {
Column {
Column(
modifier = Modifier
.fillMaxWidth()
.border(
width = 0.5.dp, color = Color.Gray, shape = RoundedCornerShape(
topStart = 8.dp,
bottomEnd = 8.dp
)
)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Text(text = data.title)
Spacer(modifier = Modifier.height(16.dp))
Row {
HomeArticleItemTv(text = "分享人:${data.shareUser.ifEmpty { data.author }}")
Spacer(modifier = Modifier.width(8.dp))
HomeArticleItemTv(text = "分类:${data.chapterName}")
Spacer(modifier = Modifier.width(8.dp))
HomeArticleItemTv(text = "时间:${data.niceShareDate}")
}
Spacer(modifier = Modifier.height(16.dp))
}
Divider(thickness = 0.5.dp, color = Color.Gray)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
fun HomeArticleItemTv(text: String) {
Text(
text = text,
color = MaterialTheme.colorAdapter().value.mainTvColor,
fontSize = 12.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
关于我
我是Taonce,如果觉得本文对你有所帮助,帮忙点个赞或者收藏,谢谢~