Android compose 无限滚动列表

实现无限滚动列表。 添加依赖:

复制代码
implementation("androidx.compose.material:material")

创建ViewModel, 用于列表滚动到底部加载数据,以及下拉刷新时重置数据。

Kotlin 复制代码
package com.example.testcompose1

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

// 创建无限滚动列表的数据源和 ViewModel
class InfiniteListViewModel : ViewModel() {
    private val _items = MutableStateFlow<List<String>>(emptyList())
    val items: StateFlow<List<String>> = _items.asStateFlow()

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    private var currentPage = 0
    private val pageSize = 20
    private val totalItems = 100  // 总共 100 条数据

    init {
        loadMore()
    }

    fun loadMore() {
        // 如果正在加载,或者已经加载完所有数据,则不再加载
        if (_isLoading.value || _items.value.size >= totalItems) return

        viewModelScope.launch(Dispatchers.IO) { // 启动协程,在io线程中执行
            _isLoading.value = true
            // 模拟网络延迟
            delay(1000)

            // 生成新数据
            val start = _items.value.size
            val end = minOf(start + pageSize, totalItems)
            val newItems = (start until end).map { "项目 $it" }

            _items.value += newItems
            _isLoading.value = false
        }
    }

    fun reset() {
        _items.value = emptyList()
        currentPage = 0
        loadMore()
    }
}

ui单独创建个文件:

Kotlin 复制代码
package com.example.testcompose1

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.*
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun InfiniteListPage(
    viewModel: InfiniteListViewModel = viewModel()
) {
    val items by viewModel.items.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()

    // 1. 控制下拉刷新的状态
    var isRefreshing by remember { mutableStateOf(false) }
    val refreshState = rememberPullRefreshState(
        refreshing = isRefreshing,
        onRefresh = {
            // 下拉刷新时,清空列表并重新加载第一页
            isRefreshing = true
            // 这里简单重置:清空 ViewModel 中的数据,重新加载
            viewModel.reset()
            isRefreshing = false
        }
    )

    // 2. 监听滚动到底部,自动加载更多。 创建并记忆LazyColumn的滚动状态
    val listState = rememberLazyListState()
    // LaunchedEffect监听listState的变化,创建协程作用域
    LaunchedEffect(listState) {
        // 将"最后一个可见item的索引"转换成可监听的数据流Flow.
        snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
            // 收集数据流的变化(每次滚动,这个值都会变)。 订阅数据流
            .collect { lastVisibleIndex ->
                // 如果最后一个可见项索引接近列表末尾(比如倒数第 3 项),且没有正在加载,则加载更多
                if (lastVisibleIndex != null &&
                    lastVisibleIndex >= items.size - 3 &&
                    !isLoading &&
                    items.size < 100) {
                    viewModel.loadMore()
                }
            }
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .pullRefresh(refreshState) // 下拉刷新修饰符
    ) {
        LazyColumn(
            state = listState,
            modifier = Modifier.fillMaxSize()
        ) {
            items(items) { item ->
                ListItem(item = item)
            }

            // 当正在加载时,在列表末尾显示加载指示器
            if (isLoading) {
                item { // 插入单个列表项
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(16.dp),
                        contentAlignment = Alignment.Center
                    ) {
                        CircularProgressIndicator()
                    }
                }
            }
        }

        // 下拉刷新指示器
        PullRefreshIndicator(
            refreshing = isRefreshing,
            state = refreshState,
            modifier = Modifier.align(Alignment.TopCenter)
        )
    }
}

@Composable
fun ListItem(item: String) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp, vertical = 4.dp),
        elevation = 2.dp
    ) {
        Text(
            text = item,
            modifier = Modifier.padding(16.dp)
        )
    }
}

修改下MainActivity, 显示这个无限滚动列表:

运行下,先显示loading:

再显示数据:

滑动到底部加载更多数据:

滑动到底部:

然后再滑动到顶部下拉会重置刷新数据。ok。

mainActivity完整代码:

Kotlin 复制代码
package com.example.testcompose1

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.testcompose1.ui.theme.MyAppTheme
import com.example.testcompose1.ui.theme.TestCompose1Theme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        enableEdgeToEdge()
        setContent {
            MyAppTheme(
                darkTheme = ThemeManager.isDarkTheme // 覆盖系统设置
                ,dynamicColor = false // 暂时禁用动态颜色。
            ) {
                todoJobList()
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun todoJobList(
    viewModel: TodoViewModel = viewModel()  // 获取ViewModel实例。 在同一个activity作用域中是单例。
) {
    var showInfiniteList by remember { mutableStateOf(true) }
    if (showInfiniteList) {
        // 显示无限滚动列表,并提供一个返回按钮
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text("无限滚动列表") },
                    navigationIcon = {
                        IconButton(onClick = { showInfiniteList = false }) {
                            Icon(Icons.Default.ArrowBack, contentDescription = "返回")
                        }
                    }
                )
            }
        ) { innerPadding ->
            // 给 InfiniteListPage 添加内边距
            Box(modifier = Modifier.padding(innerPadding)) {
                InfiniteListPage()
            }
        }
    } else { // 显示原待办事项列表
        val context = LocalContext.current

        // 使用 remember 和 mutableStateOf 保存输入框的文本
        var text by remember { mutableStateOf("") }
        // 使用 mutableStateListOf 保存待办项列表
//    val todoItems = remember { mutableStateListOf<String>() }
        // 将 StateFlow 转换为 Compose 可观察的 State
        val todoItems by viewModel.todoItems.collectAsState()

        Column(modifier = Modifier.padding(16.dp)) {
            ThemeSwitch()  // 添加开关
            Spacer(modifier = Modifier.height(8.dp))
            // 文本输入框
            TextField(
                value = text,
                onValueChange = { text = it }, // 反向绑定,视图变化--> 数据变化
                label = { Text("输入待办事项") },
                colors = TextFieldDefaults.colors(
                    focusedContainerColor = MaterialTheme.colorScheme.surface, // 获得焦点时的背景色
                    unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, // 失去焦点时,输入框背景色
                    focusedIndicatorColor = MaterialTheme.colorScheme.primary, // 输入框底部下划线的颜色。
                    unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
                ),
                modifier = Modifier.fillMaxWidth()
            )
            // 添加按钮
            Button(
                onClick = {
                    viewModel.addItem(text)
                    text = ""
                },
                shape = MaterialTheme.shapes.small,  // 使用主题形状
                colors = ButtonDefaults.buttonColors(
                    containerColor = MaterialTheme.colorScheme.primary, // 容器背景色,按钮底色
                    contentColor = MaterialTheme.colorScheme.onPrimary // 内容颜色,按钮上文字 / 图标的颜色
                ),
                modifier = Modifier.padding(top = 8.dp)
            ) {
                Text("添加")
            }

            // 显示待办列表
            Spacer(modifier = Modifier.height(16.dp))
            Text("待办列表", style = MaterialTheme.typography.titleMedium)
            LazyColumn {
                items(items = todoItems) { item ->
                    TodoItem(item = item
                        , onDelete = { viewModel.removeItem(item) })
                }
            }
        }
    }
}

@Composable
fun TodoItem(item: String, onDelete: () -> Unit  // 添加删除回调,删除逻辑放在上层。即把回调传给里面的按钮。
 ) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp),
        shape = MaterialTheme.shapes.medium,  // 使用主题形状
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surface
        )
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 8.dp),
            horizontalArrangement = Arrangement.SpaceBetween // 横向布局子元素两端对齐,剩余空白空间平均分配到子元素之间
        ) {
            Text(text = item
                ,style = MaterialTheme.typography.bodyLarge,
                color = MaterialTheme.colorScheme.onSurface)
            IconButton(onClick = onDelete) {
                Icon(Icons.Default.Delete, contentDescription = "删除"
                , tint = MaterialTheme.colorScheme.error)
            }
        }
    }
}

// 主题切换开关
@Composable
fun ThemeSwitch() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(MaterialTheme.shapes.medium)
            .background(MaterialTheme.colorScheme.surface)
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = "深色模式",
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurface
        )
        Switch(
            checked = ThemeManager.isDarkTheme,
            onCheckedChange = { ThemeManager.toggleTheme() }
        )
    }
}

// 为了允许手动切换深色/浅色模式,在应用中保存用户的选择,并在主题中读取. 后面改用DataStore保存
object ThemeManager {
    var isDarkTheme by mutableStateOf(false)
        private set

    fun toggleTheme() { // 切换是否为深色主题
        isDarkTheme = !isDarkTheme
    }
}

ok.

InfiniteListPage组合函数中用到LaunchedEffect启动协程,当Composable 进入组合 或key 变化时自动触发。若key为空,仅在首次进入组合时触发,后续即使 Composable 重组(比如父组件状态变化),也不会重新执行。LaunchedEffect是Compose UI 层的 API,协程的生命周期和单个Composable 组件绑定。注意区别于viewModelScope,viewModelScope处理业务层异步逻辑。

相关推荐
诸神黄昏EX1 小时前
Android Binder 系列专题【篇六:自定义AIDL HAL进程】
android
Fate_I_C1 小时前
Android现代开发:Kotlin&Jetpack
android·开发语言·kotlin·android jetpack
Densen20141 小时前
[.NET 9] BlazorWebView 无法在较旧的 Android 设备上加载, 附临时解决方法
android
轩情吖2 小时前
MySQL Connect(2)
android·mysql·adb·workbench·mysql连接池·图形化mysql
三少爷的鞋2 小时前
从“调用方的如履薄冰”到“接口的天然语义”:Room/DataStore/Retrofit 的启示
android
XiaoLeisj3 小时前
Android Kotlin 全链路系统化指南:从基础语法、类型系统与面向对象,到函数式编程、集合操作、协程并发与 Flow 响应式数据流实战
android·开发语言·kotlin·协程
恋猫de小郭4 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
mygljx14 小时前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql
xinhuanjieyi16 小时前
ruoyimate导入sql\antflow\bpm_init_db.sql报错
android·数据库·sql