Jetpack Compose分页加载扩展库

背景

  • Jetpack Paging3库已经推出了新的API来支持Jetpack Compose列表的分页加载能力,只不过涉及到的配置过多,要写的模版代码比较多,对于简易的分页加载列表来说不是太灵活
  • 此外,对于分页加载的各种状态也需要自己去区分并展示UI,没有现成的默认视图及封装好的状态管理能力可以使用
  • 下面以一个最简单的分页列表为例

我们首先要对数据进行配置:

class ComposePagingViewModel @Inject constructor() : ViewModel() {
    private val appConfig = AppPagingConfig()
    private val pagerConfig = PagingConfig(
        appConfig.pageSize,
        initialLoadSize = appConfig.initialLoadSize,
        prefetchDistance = appConfig.prefetchDistance,
        maxSize = appConfig.maxSize,
        enablePlaceholders = appConfig.enablePlaceholders
    )
    val pager by lazy {
        Pager(
            config = pagerConfig,
            initialKey = 0,
            pagingSourceFactory = {
                object : PagingSource<Int, String>() {
                    override fun getRefreshKey(state: PagingState<Int, String>): Int {
                        return 0
                    }
    
                    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
                        val response = try {
                            loadData(params)
                        } catch (exception: Exception) {
                            return@basePager PagingSource.LoadResult.Error(exception)
                        }
                        val page = params.key ?: 0
                        return LoadResult.Page(
                            response,
                            prevKey = if (page - 1 < 0) null else page - 1,
                            nextKey = if (page > 5) null else page + 1
                        )
                    }
                }
            }
        ).flow.cachedIn(viewModelScope)
  }
}

此外,在使用时还需要区分加载的状态来展示不同的UI:

if (pagerData.loadState.refresh is LoadState.Loading) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "正在加载中")
    }
} else if (pagerData.loadState.refresh is LoadState.Error) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "加载错误")
    }
} else {
    LazyColumn {
        itemsIndexed(pagerData) { index, value ->
            Text(
                text = "Index=$index $value",
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 10.dp),
                textAlign = TextAlign.Center
            )
        }
        if (pagerData.loadState.append is LoadState.Loading) {
            item {
                Text(
                    text = "正在加载中。。。。。",
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 10.dp),
                    textAlign = TextAlign.Center
                )
            }
        }else if (pagerData.loadState.append is LoadState.Error) {
            item {
                Text(
                    text = "加载错误",
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 10.dp),
                    textAlign = TextAlign.Center
                )
            }
        }
    }

}

方案

  • 编写kotlin扩展,完成Pager配置的样板代码抽离,提供了极简配置版本以及自定义配置版本
  • 编写了分页加载不同状态的默认UI,并能自动根据paging加载状态展示UI
  • 整合了一个分页加载列表组件,只需提供load方法即可快速使用分页列表

成果

封装了一个分页加载库并已在GitHub开源:compose-pagingList

  

 

功能详细介绍

下面讲对几个功能点做一个详细的介绍

Pager数据配置

  • 要使用paging库,首先需要在ViewModel中定义Pager,向其提供基础配置信息,例如加载页面大小,预取距离,列表上限等
  • 此外,还需要提供数据加载逻辑,错误处理逻辑,是否加载更多逻辑等
  • 编写了一个函数来完成这些逻辑的封装,来达到快速编写简易分页列表的目的
  • 此外还提供了一个原始版的封装,来给用户提供更多的自由度来定义数据相关的加载逻辑

easyPager

  • 最简单的配置就是使用easyPager,它封装了基础的以page作为key的分页加载能力
  • 用户只需要提供加载函数即可,剩下的转换逻辑都已经封装了
fun <T : Any> easyPager(
    pagerConfig: GlobalPagingConfig = GlobalPagingConfig(),
    loadData: suspend (page: Int) -> PagingListWrapper<T>
): Flow<PagingData<T>> {}
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
    val pager = easyPager {
        loadData(it)
    }

    private suspend fun loadData(page: Int): PagingListWrapper<String> {
        delay(2000)
        val data = mutableListOf("Page $page")
        repeat(20) {
            data.add("Item $it")
        }
        return PagingListWrapper(data, page < 3)
    }
}
data class PagingListWrapper<T>(var list: List<T>, var hasMore: Boolean)

easyPager2

  • 当然,上面只能适用于以page作为key的情况,且数据结构只有list
  • 但我们大部分情况是我们的key是后端特定的字段,且数据结构复杂
  • 这时我们可以使用easyPager2
fun <K : Any, V : Any> easyPager2(
    pagerConfig: GlobalPagingConfig = GlobalPagingConfig(),
    initialKey: K,
    loadData: suspend (page: K) -> IHasMoreListVO<K, V>
): Flow<PagingData<V>> {}
val pager2 = easyPager2(initialKey = 0){
    return@easyPager2 loadData2(it)
}

private suspend fun loadData2(page: Int): PageListVO {
    delay(1500)
    val data = mutableListOf("Page $page")
    repeat(20) {
        data.add("Item $it")
    }
    return PageListVO(page, data, page < 2)
}

它会要求我们的数据模型实现IHasMoreListVO来提供必要的数据

interface IHasMoreListVO<K, V> {
    /**
     * Return whether there are more items.
     */
    fun hasMore(): Boolean {
        return false
    }

    /**
     * Return the data list.
     */
    fun getList(): List<V> = emptyList()

    /**
     * Return the preKey. Normally, apps do not need to override this method and just leave it to null.
     * Unless you can get the preKey from the backend or you can calculate the preKey yourself.
     */
    fun getPreKey(): K? = null

    /**
     * Return the nextKey which will be used for the request for the next page.
     */
    fun getNextKey(): K? = null
}
data class PageListVO(var page: Int, var items: MutableList<String>, var hasMore: Boolean) : IHasMoreListVO<Int,String> {
    override fun hasMore(): Boolean {
        return hasMore
    }

    override fun getList(): List<String> {
        return items
    }

    override fun getPreKey(): Int? {
        return if (page - 1 < 0) null else page - 1
    }

    override fun getNextKey(): Int? {
        return page + 1
    }
}

pager

  • 对于成熟的项目,已经有自己的网络库框架,异常可能不再是直接抛出,而是封装成了不同的错误返回,这时我们就不太方便使用简易版本了
  • 可以使用这个方法来做一个二次封装,内部完成对于不同的返回结果和Result的映射
fun <K : Any, V : Any> pager(
    pagerConfig: GlobalPagingConfig = GlobalPagingConfig(),
    initialKey: K,
    loadData: suspend (page: K) -> Result<IHasMoreListVO<K, V>?>
): Flow<PagingData<V>> {}
pager(pagerConfig, initialKey) { key ->when (val result = loadData(key)) {
        is HttpResult.Error.BusinessError -> {
            return@pager Result.failure(Exception("${result.code} - ${result.message}"))
        }
        is HttpResult.Error.NetworkError -> {
            return@pager Result.failure(result.error)
        }
        is HttpResult.Success -> {
            return@pager Result.success(result.response)
        }
        else -> {
            return@pager Result.failure(Exception("Other Exception"))
        }
    }
}

列表展示

PagingLazyColumn

  • 最简单的方式是直接使用这个组合好的,可以直接使用的PagingLazyColumn
  • 用户只需提供pagerData和列表Item样式即可使用
@Composable
fun <T : Any> PagingLazyColumn(
    modifier: Modifier = Modifier,
    pagingData: LazyPagingItems<T>,
    loadingContent: @Composable (() -> Unit)? = { DefaultLoadingContent() },
    noMoreContent: @Composable (() -> Unit)? = { DefaultNoMoreContent() },
    errorContent: @Composable ((retry: (() -> Unit)?) -> Unit)? = { retry ->
        DefaultErrorContent(
            retry
        )
    },
    refreshingContent: @Composable (() -> Unit)? = { DefaultRefreshingContent() },
    firstLoadErrorContent: @Composable ((retry: (() -> Unit)?) -> Unit)? = { retry ->
        DefaultFirstLoadErrorContent(
            retry
        )
    },
    emptyListContent: @Composable (() -> Unit)? = { DefaultEmptyListContent() },
    pagingItemContent: @Composable (index: Int, value: T?) -> Unit,
) {}
@Composable
fun EasyPagingListScreen(viewModel: MainViewModel = hiltViewModel()) {
    val pagerData = viewModel.pager.collectAsLazyPagingItems()
    PagingLazyColumn(pagingData = pagerData) { _, value ->
        Text(value)
    }
}

当然,上面的方法限定死了只能使用lazyColumn并且无法添加自定义的header等组件

所以,提供了PagingListContainer和itemPaging分别封装了首次加载状态和加载更多的状态

PagingListContainer

  • 首次加载的状态涉及到全页面级别的UI样式
  • 提供了一个PagingListContainer来完成状态展示的封装并展示列表
/**
 *
 * Creates a [PagingListContainer] which will show different content for different refreshing status.
 *
 * This function only cares about the first loading status, for append loading status, use [itemPaging]
 *
 * @param pagingData the paging data
 * @param refreshingContent the content will show when list is loading data. By default this will use a [DefaultRefreshingContent]
 * @param firstLoadErrorContent the content will show when list encounters an error data. By default this will use a [DefaultFirstLoadErrorContent]
 * @param emptyListContent the content will show when list encounters an empty list. By default this will use a [DefaultEmptyListContent]
 * @param listContent the real list content
 */
@Composable
fun <T : Any> PagingListContainer(
    pagingData: LazyPagingItems<T>,
    refreshingContent: @Composable (() -> Unit)? = { DefaultRefreshingContent() },
    firstLoadErrorContent: @Composable ((retry: (() -> Unit)?) -> Unit)? = { retry ->
        DefaultFirstLoadErrorContent(
            retry
        )
    },
    emptyListContent: @Composable (() -> Unit)? = { DefaultEmptyListContent() },
    listContent: @Composable () -> Unit,
) {}

itemPaging

  • 提供了LazyListScope扩展,来根据不同的状态提供加载项的样式
  • 这样在多种列表组件下我们都可以添加这个item来提供加载更多的样式
/**
 *
 * Creates a [itemPaging] as an extension for [LazyListScope] that create [item] for different loading status.
 *
 * This function need to be used inside the [LazyListScope] like [LazyColumn] or [LazyRow]
 *
 * @param pagingData the paging data
 * @param loadingContent the content will show when list is loading data. By default this will use a [DefaultLoadingContent]
 * @param noMoreContent the content will show when list does not have more data. By default this will use a [DefaultNoMoreContent]
 * @param errorContent the content will show when list encounters an error data. By default this will use a [DefaultErrorContent]
 */
fun <T : Any> LazyListScope.itemPaging(
    pagingData: LazyPagingItems<T>,
    loadingContent: @Composable (() -> Unit)? = { DefaultLoadingContent() },
    noMoreContent: @Composable (() -> Unit)? = { DefaultNoMoreContent() },
    errorContent: @Composable ((retry: (() -> Unit)?) -> Unit)? = { retry ->
        DefaultErrorContent(
            retry
        )
    },
) {}

自由组合

我们可以自由组合这些可组合项来灵活展示分页列表

@Composable
fun RawPagingListScreen(viewModel: MainViewModel = hiltViewModel()) {
    val pagerData = viewModel.pager.collectAsLazyPagingItems()
    // show first loading statusPaging
    ListContainer(pagingData = pagerData) {
        LazyRow {
            item {
                Text(
                    text = "Raw PagingList",
                    modifier = Modifier
                        .height(40.dp)
                        .fillParentMaxWidth()
                        .padding(top = 15.dp),
                    textAlign = TextAlign.Center
                )
            }
            itemsIndexed(pagerData) { _, value ->PagingContent(value)
            }
            // show load more status
            itemPaging(pagerData)
        }
    }
}

加载状态

默认加载样式

  • 提供了 加载中,加载失败,没有更多,首次加载中,首次加载失败,空列表六种状态的默认样式

默认加载样式
提供了 加载中,加载失败,没有更多,首次加载中,首次加载失败,空列表六种状态的默认样式
@Composable
fun DefaultLoadingContent() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(40.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        CircularProgressIndicator(
            color = Color.Red,
            modifier = Modifier.size(25.dp),
            strokeWidth = 2.dp
        )
    }
}

@Composable
fun DefaultNoMoreContent(noMoreText: String = "No More Data") {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(40.dp)
            .padding(vertical = 10.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = noMoreText,
            modifier = Modifier
                .padding(horizontal = 5.dp),
            textAlign = TextAlign.Center
        )
    }

}

@Composable
fun DefaultErrorContent(retry: (() -> Unit)?, ErrorText: String = "NetWork Error!") {
    Text(
        text = ErrorText,
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 10.dp)
            .clickable {
                retry?.invoke()
            },
        textAlign = TextAlign.Center
    )
}

自定义样式

 

  • 六种刷新状态都开放了自定义样式的填充及取消展示
@Composable
fun <T : Any> PagingLazyColumn(
    modifier: Modifier = Modifier,
    pagingData: LazyPagingItems<T>,
    loadingContent: @Composable (() -> Unit)? = { DefaultLoadingContent() },
    noMoreContent: @Composable (() -> Unit)? = { DefaultNoMoreContent() },
    errorContent: @Composable ((retry: (() -> Unit)?) -> Unit)? = { retry ->
        DefaultErrorContent(
            retry
        )
    },
    refreshingContent: @Composable (() -> Unit)? = { DefaultRefreshingContent() },
    firstLoadErrorContent: @Composable ((retry: (() -> Unit)?) -> Unit)? = { retry ->
        DefaultFirstLoadErrorContent(
            retry
        )
    },
    emptyListContent: @Composable (() -> Unit)? = { DefaultEmptyListContent() },
    pagingItemContent: @Composable (index: Int, value: T?) -> Unit,
)
  • 若不需要关心某种状态,直接传null即可
@Composable
fun CustomLoadMoreScreen(viewModel: MainViewModel = hiltViewModel()) {
    val pagerData = viewModel.pager.collectAsLazyPagingItems()
    PagingLazyColumn(
        pagingData = pagerData,
        loadingContent = { CustomLoadMoreContent() },
        noMoreContent = null,
        refreshingContent = { CustomRefreshingContent() }) { _, value ->
        PagingContent(value)
    }
}

@Composable
fun CustomLoadMoreContent() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(40.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        LinearProgressIndicator(
            color = Color.Red,
            modifier = Modifier
                .width(100.dp)
                .height(2.dp),
        )
    }
}

@Composable
fun CustomRefreshingContent() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        CustomLoadMoreContent()
    }
}

引用

目前已在github开源,并上传到maven。目前最新版本是0.0.3

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.github.kevinnzou:compose-paginglist:<version>")
}

总结

当初写这个库的起因就是自己在Compose中使用Paging3库时感觉太过于繁琐,样本代码过多。尤其是在简易的分页列表时,每个列表都需要写大量的重复代码。因此,想要封装一个可以开箱即用的分页列表库。目前项目已经在GitHub开源且发布到0.0.3版本,支持了GridView的分页展示。欢迎大家提出改进建议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值