Compose中结合Paging实现上拉丝滑加载和下拉刷新

本期内容主要介绍在Compose中使用Paging实现静默上拉加载和下拉刷新功能。

Android Studio Giraffe | 2022.3.1 AS更新到Giraffe啦,新的UI视角上简约清爽,大家可以体验一波。

Compose版本为1.4.3

kotlin版本为1.8.10

paging-compose版本为3.2.0

swiperefresh版本为0.31.5-beta

简述

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中啦,并且在上滑的过程中几乎看不到加载下一页的过程,整体效果非常的丝滑流畅。

ViewModelarticleList还是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,如果觉得本文对你有所帮助,帮忙点个赞或者收藏,谢谢~

相关推荐
COSMOS_*17 小时前
2025最新版 Android Studio安装及组件配置(SDK、JDK、Gradle)
android·ide·jdk·gitee·android studio
jian1105817 小时前
android studio Profiler性能优化,查看内存泄漏
android·性能优化·android studio
建群新人小猿19 小时前
陀螺匠企业助手——组织框架图
android·java·大数据·开发语言·容器
TheNextByte120 小时前
如何将文件从Android无线传输到 iPad
android·ios·ipad
赫萝的红苹果20 小时前
实验探究并验证MySQL innoDB中的各种锁机制及作用范围
android·数据库·mysql
叶落无痕5221 小时前
Android Studio 2024.3.1 连接夜神模拟器
android·ide·android studio
玲子的猫21 小时前
安卓原生开发实现图片双指放大预览功能
android
2501_915106321 天前
如何在iPad上高效管理本地文件的完整指南
android·ios·小程序·uni-app·iphone·webview·ipad
似霰1 天前
AIDL Hal 开发笔记5----实现AIDL HAL
android·framework·hal
2501_915106321 天前
iOS 成品包加固,在只有 IPA 的情况下,能做那些操作
android·ios·小程序·https·uni-app·iphone·webview