背景
- 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的分页展示。欢迎大家提出改进建议。