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 小时前
『OpenGL学习滤镜相机』- Day5: 纹理变换与矩阵操作
android·opengl
撩得Android一次心动2 小时前
Android 四大组件——BroadcastReceiver(广播)
android·java·android 四大组件
努力学习的小廉2 小时前
初识MYSQL —— 复合查询
android·数据库·mysql
ii_best2 小时前
安卓/IOS工具开发基础教程:按键精灵一个简单的文字识别游戏验证
android·开发语言·游戏·ios·编辑器
Digitally11 小时前
如何用5种实用方法将电脑上的音乐传输到安卓手机
android·智能手机·电脑
HahaGiver66612 小时前
Unity与Android原生交互开发入门篇 - 打开Unity游戏的设置
android·unity·交互
2501_9159090612 小时前
WebView 调试工具全解析,解决“看不见的移动端问题”
android·ios·小程序·https·uni-app·iphone·webview
IT乐手14 小时前
android 下载管理工具类
android
2501_9151063214 小时前
App 怎么上架 iOS?从准备资料到开心上架(Appuploader)免 Mac 上传的完整实战流程指南
android·macos·ios·小程序·uni-app·iphone·webview
科技峰行者15 小时前
安卓16提前发布能否改写移动生态格局
android