Jetpack Compose(九)-列表和下拉刷新上拉加载

一、使用verticalScroll/horizontalScroll实现滚动

在XML中实现超出屏幕的滚动一般使用ScrollView,在Compose中一般通过给ColumnModifier添加verticalScroll()方法或通过给RowModifier添加horizontalScroll()方法来让多个子控件实现滚动,下面是一个竖向滚动例子,横向滚动类型:

kotlin 复制代码
@Composable
fun Greeting() {
    val items = listOf("语文", "数学", "物理", "化学")
    val scrollState = rememberScrollState()

    Column(
        modifier = Modifier
            .height(200.dp)    //父布局限定高度
            .fillMaxWidth()
            .background(Color.LightGray)
            .verticalScroll(scrollState)      //垂直滚动
    ) {
        items.forEachIndexed { index, itemText ->
            Box(
                modifier = Modifier
                    .height(80.dp)
                    .fillMaxWidth()
                    .background(Color.Cyan),
                contentAlignment = Alignment.Center
            ) {
                Text(text = itemText)
            }
            Spacer(modifier = Modifier.height(1.dp))
        }
    }
}

UI效果

二、LazyComposables

ColumnModifier添加verticalScroll()方法可以让列表实现滑动。但是如果列表过长,众多的内容会占用大量的内存。然而更多的内容对于用户其实都是不可见的,没必要加载到内存。所以Compose提供了专门用于处理长列表的组件,这些组件只会在我们能看到的列表部分进行重组和布局,它们分别是LazyColumnLazyRow。其作用类似于传统视图中的ListView或者RecyclerView

1、LazyListScope作用域

LazyColumnLazyRow内部都是基于LazyList组件实现的,虽然这是一个internal的内部组件,我们无法直接使用它。LazyList和其他布局类组件不同,不能在它的content里面直接裸写子Composable组件。它的content是一个LazyListScope.()-> Unit类型的作用域代码块,在内部通过LazyListScope提供的item等方法来描述列表中的内容,整体符合DSL的代码风格,下面是例子:

kotlin 复制代码
data class Subjects(val name:String)

@Composable
fun Greeting() {
    val itemList = listOf("语文", "数学", "物理", "化学", "美术")
    val subjectsList = itemList.map {
        Subjects(it)
    }
    LazyColumn {
        //创建1个条目
        item {
            Text(text = itemList[0])
        }

        //再创建5个条目
        items(5) { index ->
            Text(text = "${itemList[1]} $index")
        }

        //LazyListScope的扩展函数
        //再创建几个条目
        items(subjectsList) { subject ->
            Text(text = "$subject")
        }

        //LazyListScope的扩展函数,可以获取到index
        //再创建几个条目
        itemsIndexed(subjectsList){ index,subject->
            Text(text = "$subject $index")
        }
    }
}

UI效果

2、设置间距

有的时候也需要在列表中为内容整体设置外边距,这也非常容易,Lazy组件提供了contentPadding参数,如果想设置条目间竖直方向的间距,Lazy组件提供了verticalArrangement参数,下面是一个例子:

kotlin 复制代码
@Composable
fun Greeting() {
    val itemList = listOf("语文", "数学", "物理", "化学", "美术")
    val subjectsList = itemList.map {
        Subjects(it)
    }
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()                   //整个屏幕
            .background(Color.LightGray),    //背景浅灰色
        contentPadding = PaddingValues(15.dp),   //内边距
        verticalArrangement = Arrangement.spacedBy(15.dp)  //条目间竖直方向的间距
    ) {
        //LazyListScope的扩展函数,可以获取到index
        itemsIndexed(subjectsList) { index, subject ->
            Box(
                modifier = Modifier
                    .fillMaxWidth()      //最大宽度
                    .height(50.dp)
                    .background(Color.Cyan),   //条目青色
                contentAlignment = Alignment.Center
            ) {
                Text(text = "$subject $index")
            }
        }
    }
}

UI效果

三、下拉刷新

Modifier.pullRefresh 可以用于下拉刷新的实现。它的参数如下:

kotlin 复制代码
@ExperimentalMaterialApi
fun Modifier.pullRefresh(
    state: PullRefreshState,     //状态被PullRefreshState保存和更新
    enabled: Boolean = true
) = inspectable(...){
    Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
}
kotlin 复制代码
@ExperimentalMaterialApi
fun Modifier.pullRefresh(
    onPull: (pullDelta: Float) -> Float,   //垂直方向的量,向下为正
    onRelease: suspend (flingVelocity: Float) -> Float,   //拖动被释放时的回调
    enabled: Boolean = true
) = inspectable(...) {
    Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
}

第一种是基于第二种实现的。相关联的这个 state(PullRefreshState) 自然也有对应的 remember 方法用于创建,函数如下:

kotlin 复制代码
@Composable
@ExperimentalMaterialApi
fun rememberPullRefreshState(
    refreshing: Boolean,      //是否正在刷新
    onRefresh: () -> Unit,    //刷新时的回调
    refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,   //若超过阀值(默认80dp),则放手后会触发onRefresh
    refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,   //刷新时指示器的底部位置(默认56dp)
): PullRefreshState {...}

下面是一个代码示例:

kotlin 复制代码
@Composable
fun Greeting() {
    SwipeToRefreshTest()
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeToRefreshTest(
    modifier: Modifier = Modifier
) {
    val list = remember {
        List(4) { "Item $it" }.toMutableStateList()
    }
    //是否正在刷新
    var refreshing by remember {
        mutableStateOf(false)
    }
    // 用协程模拟一个耗时加载
    val scope = rememberCoroutineScope()
    val state = rememberPullRefreshState(refreshing = refreshing, onRefresh = {
        scope.launch {
            refreshing = true
            //模拟延时数据加载
            delay(1000)
            //添加一条数据
            list += "Item ${list.size + 1}"
            refreshing = false
        }
    })
    Box(
        modifier = modifier
            .fillMaxSize()
            .pullRefresh(state)   //下拉刷新

    ) {
        LazyColumn(
            Modifier
                .fillMaxSize()
                .background(Color.LightGray),
            contentPadding = PaddingValues(15.dp),
            verticalArrangement = Arrangement.spacedBy(15.dp)
        ) {
            items(list) { item ->
                Box(              //item的布局
                    modifier =
                    Modifier
                        .fillMaxWidth()
                        .height(50.dp)
                        .background(Color.Cyan),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = item)
                }
            }
        }
        //基于Android的SwipeRefreshLayout创建的指示器
        PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
    }
}

UI效果

四、上拉加载

这里是使用Jetpack Compose的Paging库实现的,需要先导包:

kotlin 复制代码
implementation("androidx.paging:paging-compose:1.0.0-alpha18")

然后先定义网络请求和分页的逻辑,继承PagingSource代码如下:

kotlin 复制代码
class RequestDataSource : PagingSource<Int, String>() {

    override fun getRefreshKey(state: PagingState<Int, String>): 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, String> {
        // 定义键值
        val currentKey = params.key ?: 0
        return try {
            //随便给的数据,这里应该写网络请求
            val list = List(10) { "item ${(params.key ?: 0) * 10 + it}" }
            //prevKey和nextKey标记页面
            LoadResult.Page(
                data = list,
                prevKey = if (currentKey == 0) null else currentKey - 1,
                nextKey =  currentKey + 1
            )
        } catch (exception: Exception) {
            //异常的处理
            LoadResult.Error(exception)
        }
    }
}

这种很多都是模板写法,还处于初学阶段,就不深入源码了。再然后就是定义我们的请求,例如下面的代码仓库:

kotlin 复制代码
class RequestRepository{

    fun testRequest(): Flow<PagingData<String>> {
        // 通过Pager.flow返回流对象
        return Pager(
            config = PagingConfig(pageSize = 10),   //每页10条
            pagingSourceFactory = {
                //网络请求和分页逻辑
                RequestDataSource()
            }
        ).flow
    }
}

具体代码调用:

kotlin 复制代码
@Composable
fun Greeting() {
    LoadMoreTest()
}

@SuppressLint("FlowOperatorInvokedInComposition")
@Composable
fun LoadMoreTest() {
    //这行代码应该写在ViewModel中, demo代码就直接写在Activity中了
    //数据源
    val requestList =
        RequestRepository().testRequest().cachedIn(lifecycleScope).collectAsLazyPagingItems()

    Box(
        modifier = Modifier
            .fillMaxSize()
    ) {
        LazyColumn(
            Modifier
                .fillMaxSize()
                .background(Color.LightGray),
            contentPadding = PaddingValues(15.dp),
            verticalArrangement = Arrangement.spacedBy(15.dp)
        ) {
            items(requestList.itemCount) { index ->    //加载数据源
                Box(
                    modifier =
                    Modifier
                        .fillMaxWidth()
                        .height(50.dp)
                        .background(Color.Cyan),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = requestList[index] ?: "")
                }
            }
        }
    }
}

最后看看UI效果,如下:

这种写法确实很丝滑,但一般我们实际开发中更倾向于传统带视图的上拉加载形式。

参考内容:

Jetpack Compose博物馆

实体书 Jetpack Compose从入门到实战

Jetpack-Compose-Playground

Compose中结合Paging实现上拉丝滑加载和下拉刷新

相关推荐
芦半山2 分钟前
2025:生活是个缓慢受锤的过程
android·年终总结
Kapaseker8 小时前
你不看会后悔的2025年终总结
android·kotlin
alexhilton11 小时前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
ji_shuke12 小时前
opencv-mobile 和 ncnn-android 环境配置
android·前端·javascript·人工智能·opencv
sunnyday042614 小时前
Spring Boot 项目中使用 Dynamic Datasource 实现多数据源管理
android·spring boot·后端
幽络源小助理15 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
inBuilder低代码平台15 小时前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐15 小时前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
白熊小北极15 小时前
Android Jetpack Compose折叠屏感知与适配
android
HelloBan15 小时前
setHintTextColor不生效
android