You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Jetpack Compose LazyColumn的游标分页MVI实现求助

LazyColumn 游标分页(MVI架构实现)

1. 定义MVI状态(State)

明确页面核心状态,包含交易数据、分页标识、加载状态,同时避免重复请求:

sealed class LoadState {
    object Idle : LoadState()
    object LoadingFirstPage : LoadState()
    object LoadingNext : LoadState()
    object LoadingPrev : LoadState()
    data class Error(val message: String) : LoadState()
}

data class TransfersState(
    val transactions: List<TransactionDataDto>,
    val paging: PagingDto,
    val loadState: LoadState,
    val isLoadingNext: Boolean = false,
    val isLoadingPrev: Boolean = false
) {
    companion object {
        val Initial = TransfersState(
            transactions = emptyList(),
            paging = PagingDto(next = null, prev = null),
            loadState = LoadState.Idle
        )
    }
}

2. 定义用户意图(Intent)

封装UI层触发的所有操作:

sealed class TransfersIntent {
    object LoadFirstPage : TransfersIntent()
    object LoadNextPage : TransfersIntent()
    object CheckAndLoadPrevPage : TransfersIntent()
}

3. Repository层实现

封装API请求逻辑,根据参数返回分页数据:

interface TransfersRepository {
    suspend fun getTransfers(
        take: Int = 20,
        after: String? = null,
        before: String? = null
    ): Result<TransfersListResponse>
}

class TransfersRepositoryImpl(private val apiService: ApiService) : TransfersRepository {
    override suspend fun getTransfers(take: Int, after: String?, before: String?): Result<TransfersListResponse> {
        return runCatching {
            apiService.getTransfers(take = take, after = after, before = before)
        }
    }
}

4. ViewModel核心逻辑处理

处理意图并更新状态,严格遵循需求中的分页规则:

class TransfersViewModel(
    private val repository: TransfersRepository
) : ViewModel() {
    private val _state = MutableStateFlow(TransfersState.Initial)
    val state = _state.asStateFlow()

    fun handleIntent(intent: TransfersIntent) {
        when (intent) {
            is TransfersIntent.LoadFirstPage -> loadFirstPage()
            is TransfersIntent.LoadNextPage -> loadNextPage()
            is TransfersIntent.CheckAndLoadPrevPage -> checkAndLoadPrevPage()
        }
    }

    private fun loadFirstPage() {
        viewModelScope.launch {
            _state.update { it.copy(loadState = LoadState.LoadingFirstPage) }
            val result = repository.getTransfers()
            result.onSuccess { response ->
                _state.update {
                    it.copy(
                        transactions = response.items,
                        paging = response.paging,
                        loadState = LoadState.Idle
                    )
                }
            }.onFailure {
                _state.update { it.copy(loadState = LoadState.Error(it.message ?: "加载失败")) }
            }
        }
    }

    private fun loadNextPage() {
        val currentState = _state.value
        if (currentState.paging.next == null || currentState.isLoadingNext) return

        viewModelScope.launch {
            _state.update { it.copy(isLoadingNext = true) }
            val result = repository.getTransfers(after = currentState.paging.next)
            result.onSuccess { response ->
                _state.update {
                    it.copy(
                        transactions = it.transactions + response.items,
                        paging = response.paging,
                        isLoadingNext = false
                    )
                }
            }.onFailure {
                _state.update {
                    it.copy(
                        isLoadingNext = false,
                        loadState = LoadState.Error(it.message ?: "加载下一页失败")
                    )
                }
            }
        }
    }

    private fun checkAndLoadPrevPage() {
        val currentState = _state.value
        if (currentState.isLoadingPrev) return

        viewModelScope.launch {
            _state.update { it.copy(isLoadingPrev = true) }
            // 重新加载第一页检查prev状态
            val firstPageResult = repository.getTransfers()
            firstPageResult.onSuccess { firstPageResponse ->
                firstPageResponse.paging.prev?.let { prevId ->
                    // prev存在则加载上一页数据
                    val prevPageResult = repository.getTransfers(before = prevId)
                    prevPageResult.onSuccess { prevPageResponse ->
                        _state.update {
                            it.copy(
                                transactions = prevPageResponse.items + it.transactions,
                                paging = firstPageResponse.paging.copy(prev = prevPageResponse.paging.prev),
                                isLoadingPrev = false
                            )
                        }
                    }.onFailure {
                        _state.update {
                            it.copy(
                                isLoadingPrev = false,
                                loadState = LoadState.Error(it.message ?: "加载上一页失败")
                            )
                        }
                    }
                } ?: run {
                    // prev为空仅更新分页状态
                    _state.update {
                        it.copy(
                            paging = firstPageResponse.paging,
                            isLoadingPrev = false
                        )
                    }
                }
            }.onFailure {
                _state.update {
                    it.copy(
                        isLoadingPrev = false,
                        loadState = LoadState.Error(it.message ?: "检查顶部分页失败")
                    )
                }
            }
        }
    }
}

5. UI层实现(LazyColumn)

处理滚动监听,触发对应意图,展示加载状态:

@Composable
fun TransfersScreen(viewModel: TransfersViewModel = viewModel()) {
    val state by viewModel.state.collectAsState()
    val lazyListState = rememberLazyListState()

    // 滚动到顶部时触发顶部分页检查
    LaunchedEffect(lazyListState.firstVisibleItemIndex) {
        if (lazyListState.firstVisibleItemIndex == 0 && state.transactions.isNotEmpty()) {
            viewModel.handleIntent(TransfersIntent.CheckAndLoadPrevPage)
        }
    }

    // 滚动到底部时触发下一页加载
    LaunchedEffect(lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()) {
        val lastVisibleItem = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()
        if (lastVisibleItem?.index == state.transactions.size - 1 && state.paging.next != null) {
            viewModel.handleIntent(TransfersIntent.LoadNextPage)
        }
    }

    when (state.loadState) {
        is LoadState.LoadingFirstPage -> {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                CircularProgressIndicator()
            }
        }
        is LoadState.Error -> {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                Text((state.loadState as LoadState.Error).message)
            }
        }
        else -> {
            LazyColumn(state = lazyListState) {
                // 顶部加载占位
                if (state.isLoadingPrev) {
                    item {
                        Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
                            CircularProgressIndicator(modifier = Modifier.padding(16.dp))
                        }
                    }
                }

                items(state.transactions) { transaction ->
                    TransactionItem(transaction)
                }

                // 底部加载占位
                if (state.isLoadingNext) {
                    item {
                        Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
                            CircularProgressIndicator(modifier = Modifier.padding(16.dp))
                        }
                    }
                }
            }
        }
    }

    // 页面初始化加载第一页
    LaunchedEffect(Unit) {
        viewModel.handleIntent(TransfersIntent.LoadFirstPage)
    }
}

@Composable
fun TransactionItem(transaction: TransactionDataDto) {
    Card(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
        Text(text = transaction.toString(), modifier = Modifier.padding(16.dp))
    }
}

关键逻辑说明

  • 首次加载:仅传递take参数获取第一页,prev为null,next返回分页ID
  • 加载下一页:滚动到底部时,用当前next作为after参数请求,新数据追加到列表末尾
  • 顶部检查:滚动到顶部时重新请求第一页,若prev不为null则用before参数请求上一页,新数据插入列表头部
  • 防重复请求:通过isLoadingNext和isLoadingPrev标记,避免同一请求重复触发

内容的提问来源于stack exchange,提问作者tasjapr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 21:30:15