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

相关推荐
小比卡丘1 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭2 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss3 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.4 小时前
数据库语句优化
android·数据库·adb
GEEKVIP6 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20058 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6898 小时前
Android广播
android·java·开发语言
与衫9 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了16 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵16 小时前
Android Debug Bridge(ADB)完全指南
android·adb