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实现上拉丝滑加载和下拉刷新

相关推荐
介一安全41 分钟前
【Frida Android】基础篇15(完):Frida-Trace 基础应用——JNI 函数 Hook
android·网络安全·ida·逆向·frida
吞掉星星的鲸鱼1 小时前
android studio创建使用开发打包教程
android·ide·android studio
陈老师还在写代码1 小时前
android studio 签名打包教程
android·ide·android studio
csj501 小时前
android studio设置
android
hifhf1 小时前
Android Studio gradle下载失败报错
android·ide·android studio
陈老师还在写代码1 小时前
android studio,java 语言。新建了项目,在哪儿设置 app 的名字和 logo。
android·java·android studio
2501_916007473 小时前
Fastlane 结合 开心上架(Appuploader)命令行实现跨平台上传发布 iOS App 的完整方案
android·ios·小程序·https·uni-app·iphone·webview
listhi5205 小时前
Vue.js 3的组合式API
android·vue.js·flutter
用户69371750013845 小时前
🚀 Jetpack MVI 实战全解析:一次彻底搞懂 MVI 架构,让状态管理像点奶茶一样丝滑!
android·android jetpack
2501_915918416 小时前
iOS 上架应用市场全流程指南,App Store 审核机制、证书管理与跨平台免 Mac 上传发布方案(含开心上架实战)
android·macos·ios·小程序·uni-app·cocoa·iphone