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

相关推荐
安卓理事人14 分钟前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学1 小时前
Android M3U8视频播放器
android·音视频
q***57742 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober2 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿3 小时前
关于ObjectAnimator
android
zhangphil4 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我5 小时前
从头写一个自己的app
android·前端·flutter
lichong9516 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户69371750013846 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我7 小时前
NekoBoxForAndroid 编译libcore.aar
android