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

如何在Kotlin+Flow+MVVM+Jetpack Compose中实现刷新按钮最佳实践

实现刷新按钮重新触发API调用的最佳实践

我想实现一个刷新按钮,点击时重新触发API调用,但不清楚最佳实践。以下是我的ViewModel和Composable代码:

ViewModel 代码

@HiltViewModel
class CoinListViewModel @Inject constructor(
    private val getAllCoinsUseCase: GetListOfCoinsUseCase
): ViewModel() {

    private val _state = mutableStateOf(CoinsListState()) // 不暴露可变状态
    val state: State<CoinsListState> = _state // 对外暴露不可变状态

    init {
        getData()
    }

    // 调用用例并更新状态,供UI展示
    private fun getData(){

        getAllCoinsUseCase().onEach { resourceResult ->

            when(resourceResult){
                is Resource.Loading -> {
                    _state.value = CoinsListState(isLoading = true)
                }
                is Resource.Success -> {
                    _state.value = CoinsListState(coins = resourceResult.data ?: emptyList())
                }
                is Resource.Error -> {
                    _state.value = CoinsListState(
                        error = resourceResult.message ?: "Unexpected Error"
                    )
                }
            }
        }.launchIn(viewModelScope) // 需在协程作用域启动,因为使用Flow
    }
}

刷新按钮代码

@Composable
fun RefreshButton(navController: NavController, viewModel: CoinListViewModel) {
    IconButton(
        onClick = {
            // 这里需要实现刷新逻辑
        },
        modifier = Modifier
            .semantics {
                contentDescription = "Refresh Button"
                testTag = "Refresh Button Test Tag"
            },

    ) {
        Icon(
            imageVector = Icons.Default.Refresh,
            contentDescription = "Refresh Icon"
        )
    }
}

优化方案及最佳实践

1. 改造ViewModel,完善状态更新逻辑

原代码每次更新状态都会直接创建新的CoinsListState对象,会导致刷新时清空现有数据,用户体验差。同时需要对外暴露明确的刷新方法:

@HiltViewModel
class CoinListViewModel @Inject constructor(
    private val getAllCoinsUseCase: GetListOfCoinsUseCase
): ViewModel() {

    private val _state = mutableStateOf(CoinsListState()) 
    val state: State<CoinsListState> = _state 

    init {
        getData()
    }

    // 对外暴露刷新方法,明确职责
    fun refresh() {
        // 刷新前保留现有数据,仅标记加载状态
        _state.value = _state.value.copy(isLoading = true)
        getData()
    }

    private fun getData(){
        getAllCoinsUseCase().onEach { resourceResult ->
            when(resourceResult){
                is Resource.Loading -> {
                    _state.value = _state.value.copy(isLoading = true)
                }
                is Resource.Success -> {
                    _state.value = _state.value.copy(
                        isLoading = false,
                        coins = resourceResult.data ?: emptyList(),
                        error = null // 成功后清空错误信息
                    )
                }
                is Resource.Error -> {
                    _state.value = _state.value.copy(
                        isLoading = false,
                        error = resourceResult.message ?: "未知错误"
                    )
                }
            }
        }.launchIn(viewModelScope)
    }
}

2. 绑定刷新按钮与ViewModel方法

在按钮的onClick事件中调用ViewModel的refresh()方法即可触发刷新:

@Composable
fun RefreshButton(navController: NavController, viewModel: CoinListViewModel) {
    IconButton(
        onClick = {
            viewModel.refresh()
        },
        modifier = Modifier
            .semantics {
                contentDescription = "刷新按钮"
                testTag = "刷新按钮测试标签"
            },
    ) {
        Icon(
            imageVector = Icons.Default.Refresh,
            contentDescription = "刷新图标"
        )
    }
}

关键最佳实践点

  • 使用copy()更新状态:避免替换整个State对象导致的UI重置,保留现有数据的同时更新指定字段,提升刷新时的用户体验
  • 暴露明确的刷新方法:ViewModel对外提供refresh()而非直接暴露getData(),符合单一职责原则,也方便后续扩展(比如添加防抖、刷新间隔限制)
  • 合理处理状态:刷新时显示加载状态但不清空已有数据,成功后清空错误状态,避免错误信息残留
  • 协程作用域管理:始终使用viewModelScope启动协程,确保ViewModel销毁时自动取消任务,避免内存泄漏

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 04:25:38