【Jetpack Compose】LazyColumn 使用Paging3分页+SwipeRefresh下拉刷新

81 篇文章 2 订阅
26 篇文章 1 订阅

1.数据源

这里采用GitHub REST API的搜索的api来作为数据源:

image.png

https://api.github.com/search/repositories?q=compose+language:kotlin&sort=stars&order=desc 

大家可以用AS的JsonToKotlinClass插件来把这个响应数据生成data class,这里就不贴代码了。

2.依赖项

//network & serialization
implementation "com.google.code.gson:gson:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.retrofit2:retrofit:2.9.0"//
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'

//swiperefresh 的compose 版本
implementation "com.google.accompanist:accompanist-swiperefresh:0.23.1"
// paging 3 的compose 版本
implementation "androidx.paging:paging-compose:1.0.0-alpha14"
//这个可以在Compose中得到viewmodel
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"

//coil Compose 图片加载库
implementation "io.coil-kt:coil-compose:2.0.0-rc01" 

compose_version用的是1.1.1版本的这里就不贴了

3.Api调用

interface GithubService {
    /**
     * 用户列表
     */
    @GET("users")
    suspend fun getUsers(@Query("since") userId: Int,@Query("per_page") pageSize: Int = 30): List<UserEntity>

    /**
     * 仓库搜索
     */
    @GET("search/repositories")
    suspend fun searchRepositors(
        @Query("q") words: String,
        @Query("page") page: Int = 1,
        @Query("per_page") pageSize: Int = 30,
        @Query("sort") sort:String = "stars",
        @Query("order") order: String = "desc",
    ): RepositorResult
}

private val service: GithubService by lazy {
    val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BODY) })
        .build()

    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.github.com")
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    retrofit.create(GithubService::class.java)
}

fun getGithubService() = service 

Retrofit 2.6.0版本及以上就支持kotlin协程了

4.使用paging3 进行分页:

class MyPagingSource(
    val githubService: GithubService = getGithubService(),
    val words: String,
) : PagingSource<Int, RepositorItem>() {

    override fun getRefreshKey(state: PagingState<Int, RepositorItem>): Int? {
        return state.anchorPosition?.let {
            val anchorPage = state.closestPageToPosition(it)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RepositorItem> {
        try {
            val nextPage: Int = params.key ?: 1
            val repositorRst = githubService.searchRepositors(words, nextPage, 20)
            return LoadResult.Page(
                data = repositorRst.items,
                prevKey = if (nextPage == 1) null else nextPage - 1,
                nextKey = if (repositorRst.items.isEmpty()) null else nextPage + 1
            )
        }catch (e:Exception){
            return LoadResult.Error(e)
        }
    }
} 

5.viewModel:

class GithubViewModel:ViewModel() {

    val repositorPager = Pager(config = PagingConfig(pageSize = 6)){
        MyPagingSource(getGithubService(),"compose")
    }.flow.cachedIn(viewModelScope)

} 

6.最后是Compose:

@Composable
fun ListContent() {
    val viewModel: GithubViewModel = viewModel()
    val lazyPagingItems = viewModel.repositorPager.collectAsLazyPagingItems()

    val state: LazyListState = rememberLazyListState()
    SwipeRefresh(
        state = rememberSwipeRefreshState((lazyPagingItems.loadState.refresh is LoadState.Loading && lazyPagingItems.itemCount > 0)),
        onRefresh = { lazyPagingItems.refresh() },
    ) {
        LazyColumn(
            state =state,
            contentPadding = PaddingValues(10.dp),
            verticalArrangement = Arrangement.spacedBy(5.dp)) {

            items(items = lazyPagingItems) { item ->
                item?.let {
                    RepositorCard(item)
                }
            }
            if (lazyPagingItems.loadState.append is LoadState.Loading) {
                //下一页的load状态
                item {
                    Box(modifier = Modifier
                        .fillMaxWidth()
                        .height(50.dp)) {
                        CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
                    }
                }
            }
        }
    }

    if (lazyPagingItems.loadState.refresh is LoadState.Loading) {
        if (lazyPagingItems.itemCount == 0) {//第一次响应页面加载时的loading状态
            Box(modifier = Modifier.fillMaxSize()) {
                CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
            }
        }
    }else if(lazyPagingItems.loadState.refresh is LoadState.Error){
        //加载失败的错误页面
        Box(modifier = Modifier.fillMaxSize()) {
            Button(modifier = Modifier.align(alignment = Alignment.Center),
            onClick = { lazyPagingItems.refresh() }) {
                Text(text = "加载失败!请重试")
            }
        }
   }
} 

item:

@Composable
fun RepositorCard(repositorItem: RepositorItem) {
    Card(modifier = Modifier
        .fillMaxWidth()
        .padding(8.dp)) {
        Row(modifier = Modifier
            .fillMaxWidth()
            .height(88.dp)) {
            Spacer(modifier = Modifier.width(10.dp))
            Surface(shape = CircleShape, modifier = Modifier
                .size(66.dp)
                .align(Alignment.CenterVertically)) {
                AsyncImage(model = repositorItem.owner.avatar_url,
                    contentDescription = "",
                    contentScale = ContentScale.Crop)
            }

            Spacer(modifier = Modifier.width(15.dp))
            Column(modifier = Modifier.fillMaxWidth()) {
                Spacer(modifier = Modifier.height(8.dp))
                Text(text = repositorItem.name,
                    color = MaterialTheme.colors.primary,
                    style = MaterialTheme.typography.h6)
                Text(text = repositorItem.full_name, style = MaterialTheme.typography.subtitle1)
            }
        }
    }
} 

最后运行是这样的:

ztzjy-bpbov (1).gif

7.自动向上翻页的问题

有的同学想上下都能翻页加载,还是用这个搜索的api来模拟一下,首次进入页面就加载第10页的数据,在 PagingSource这里改一下:

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RepositorItem> {
    try {
        val nextPage: Int = params.key ?: 10//改成10 默认首次从第10页加载
        val repositorRst = githubService.searchRepositors(words, nextPage, 20)
        return LoadResult.Page(
            data = repositorRst.items,
            prevKey = if (nextPage == 1) null else nextPage - 1,
            nextKey = if (repositorRst.items.isEmpty()) null else nextPage + 1
        )
    }catch (e:Exception){
        return LoadResult.Error(e)
    }
} 

compose里面也改一下去掉SwipeRefresh:

LazyColumn(
    state =state,
    contentPadding = PaddingValues(10.dp),
    verticalArrangement = Arrangement.spacedBy(5.dp)) {

    if (lazyPagingItems.loadState.prepend is LoadState.Loading) {
        //list 顶部loading
        item {
            Box(modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)) {
                CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
            }
        }
    }

    items(items = lazyPagingItems) { item ->
        item?.let {
            RepositorCard(item)
        }
    }
    if (lazyPagingItems.loadState.append is LoadState.Loading) {
        //list 底部loading
        item {
            Box(modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)) {
                CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
            }
        }
    }
} 

ck20u-ysgz5.gif

肿么肥事!! 它怎么自动向上翻页!!! 直到 prevKey为null ,这难道是paging compose版本的bug,后来我想到了 compose lifecycle里面讲到的:

image.png

lifecycle-newelement-top-all-recompose.png

列表加载完初始数据后,因为prevKey不为空,就继续获取上页数据,然后就相当于列表在顶部新增内容,然后LazyColumn自动滑动到顶部,然后又会触发paging加载上一页的数据,如此循环直到prevKey为空。
compose lifecycle后面讲到了可以用key 可组合项,在LazyColumn中提供了key 可组合项的内置支持,我们加上key就行了:

items(items = lazyPagingItems, key = { item -> item.id }) { item ->
    item?.let {
        RepositorCard(item)
    }
} 

ok 解决了

hgrxh-bxxxh.gif

这里也可以加上重试按钮,这是底部的:

if (lazyPagingItems.loadState.append is LoadState.Loading) {
    // xxxx
}else if(lazyPagingItems.loadState.append is LoadState.Error){
    item {
        //底部的重试按钮
        Box(modifier = Modifier
            .fillMaxWidth()
            .height(50.dp)) {
            Button(modifier = Modifier.align(alignment = Alignment.Center),
                onClick = { lazyPagingItems.retry() }) {
                Text(text = "重试")
            }
        }
    }
} 

顶部的就是

if(lazyPagingItems.loadState.prepend is LoadState.Error) 

然后可以在PagingSource load方法里 随机抛出TimeoutException来模拟数据加载失败的情况。

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RepositorItem> {
    try {
        val nextPage: Int = params.key ?: 10//改成10 默认从第10页加载
        if(Random.nextBoolean()){
            throw SocketTimeoutException("SocketTimeout") 
        }
        val repositorRst = githubService.searchRepositors(words, nextPage, 20)
        return LoadResult.Page(
            data = repositorRst.items,
            prevKey = if (nextPage == 1) null else nextPage - 1,
            nextKey = if (repositorRst.items.isEmpty()) null else nextPage + 1
        )
    }catch (e:Exception){
        return LoadResult.Error(e)
    }
} 

有的童靴觉得列表底部加个重试按钮有点丑,然后想能不能当翻页的数据加载失败后滑动到底部自动重试

首先我们加个方法判断是否滑动到底部了:

@Composable
fun LazyListState.isScrollToEnd():Boolean {
    var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
    var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
    return remember(this) {
        derivedStateOf {
            if (previousIndex != firstVisibleItemIndex) {
                previousIndex < firstVisibleItemIndex
            } else {
                previousScrollOffset < firstVisibleItemScrollOffset
            }.also {
                previousIndex = firstVisibleItemIndex
                previousScrollOffset = firstVisibleItemScrollOffset
            } && layoutInfo.visibleItemsInfo.lastOrNull()?.let { it.index == layoutInfo.totalItemsCount-1 } ?: false
        }
    }.value
} 

然后就是根据条件调用重试了

val state: LazyListState = rememberLazyListState()
LazyColumn(state =state){
        xxx
}

//是否滑动到底部
val isScrollToEnd = state.isScrollToEnd()
//是否自动重试
val currectIsNextRetry by rememberUpdatedState(isScrollToEnd && 
        lazyPagingItems.loadState.append is LoadState.Error)

LaunchedEffect(isScrollToEnd){
    if(currectIsNextRetry){
        Log.i(TAG,"retry")
        lazyPagingItems.retry()
    }
} 

文末

我总结了一些Android核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。

需要的直接点击文末小卡片可以领取哦!我免费分享给你,以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持,需要的自己领取)

Android学习PDF+架构视频+面试文档+源码笔记

部分资料一览:

  • 330页PDF Android学习核心笔记(内含8大板块)

  • Android学习的系统对应视频

  • Android进阶的系统对应学习资料

  • Android BAT大厂面试题(有解析)

领取地址:

在这里插入图片描述

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Jetpack Compose 中,`LazyColumn` 可以使用 `remember` 函数来缓存数据以提高性能。如果数据源发生更改,`LazyColumn` 需要能够检测到这些更改并更新 UI。为此,可以使用 `key` 参数来告诉 `LazyColumn` 如何区分不同的数据项。 以下是一个示例,演示如何使用 `LazyColumn` 和 `remember` 函数来显示一个字符串列表,并在列表中添加或删除字符串时更新 UI: ```kotlin @Composable fun LazyColumnDemo() { // 用 remember 关键字缓存数据源,并使用 key 参数来区分不同的数据项 val items = remember { mutableStateListOf("Item 1", "Item 2", "Item 3") } LazyColumn { items(items.size, key = { index -> // 使用 index 和 item 的哈希值来区分不同的数据项 items[index].hashCode() }) { index -> Text(text = items[index]) // 添加一个按钮,用于在列表中添加新项目 Button(onClick = { items.add("New Item") }) { Text("Add Item") } // 添加一个按钮,用于从列表中删除项目 Button(onClick = { items.removeAt(index) }) { Text("Remove Item") } } } } ``` 在这个示例中,我们使用 `remember` 函数来缓存字符串列表。我们还使用 `key` 参数来告诉 `LazyColumn` 如何区分不同的数据项。在 `LazyColumn` 中迭代数据项时,我们可以使用 `index` 来访问每个项目,并使用 `items[index]` 来显示每个项目的文本。 我们还向 UI 添加了两个按钮,一个用于在列表中添加新项目,另一个用于从列表中删除项目。当我们单击这些按钮时,我们更新 `items` 列表,这将触发 `LazyColumn` 重新运行并更新 UI。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值