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

Android MVVM下API错误捕获、加载状态处理及数据传递最佳实践咨询

问题解答

一、API调用错误捕获的正确实现方式

你当前的实现最大的问题是错误状态和业务数据拆分到两个独立的LiveData,容易出现状态不同步的问题,且错误信息仅返回布尔值,无法区分错误类型做差异化处理。正确的实现逻辑如下:

  • 用密封类统一封装接口请求的所有状态,把成功、失败、加载中都收拢到同一个数据类中,避免多数据源状态冲突
  • 不要捕获所有Exception后直接统一返回失败,要区分异常类型:比如IOException对应网络断开、HttpException对应服务端返回非2xx错误码、业务异常对应接口返回成功但业务状态不符合预期,针对不同异常返回对应提示信息
  • 不要吞掉异常栈,本地开发环境可以打印异常日志方便排查问题

二、Repository向ViewModel传递数据的注意事项与优化建议

  • 遵守单一职责原则:Repository仅负责数据的获取与拼接(网络、本地数据库、Sp等数据源的整合),不要处理UI相关的业务逻辑,也不要持有Android平台相关的上下文引用
  • 封装性优先:不要向ViewModel返回和平台强绑定的组件(比如LiveData),优先用suspend函数或者普通Kotlin对象返回数据,保证Repository的通用性
  • 状态封装完整:返回的结果要包含足够的信息,错误场景下要返回异常类型、错误提示等字段,不要仅返回成功/失败的标识
  • 可选添加缓存策略:可以根据业务需求在Repository层实现本地缓存逻辑,比如先返回本地缓存数据,再拉取网络数据更新,减少不必要的接口请求
  • 不要把异常直接抛给上层:所有网络、数据库的异常都要在Repository层捕获封装后再返回,避免上层未捕获异常导致崩溃

三、加载状态的正确处理方式

  • 状态收拢管理:所有加载相关的状态(加载中、成功、失败、空数据)都要放到同一个数据流中管理,不要拆分多个LiveData,避免出现「加载成功了还显示错误提示」这类状态不同步的问题
  • 覆盖全场景状态:不要只做成功和失败两种状态的处理,必须包含加载中状态给用户明确的加载提示,空数据场景也要做对应UI展示
  • 状态重置逻辑:每次发起新的请求时要重置之前的状态,避免上一次请求的错误状态影响本次请求的展示
  • ViewModel仅维护状态,不处理UI逻辑:ViewModel只负责更新状态,具体的UI展示逻辑(弹Toast、显示隐藏动画)都放到View层(Fragment/Activity)处理

优化后代码示例

1. 通用状态密封类定义

sealed class UiState<out T> {
    // 加载中状态
    object Loading : UiState<Nothing>()
    // 加载成功,携带返回数据
    data class Success<out T>(val data: T) : UiState<T>()
    // 加载失败,携带异常和错误提示
    data class Error(val exception: Exception? = null, val errorMsg: String? = null) : UiState<Nothing>()
}

2. Repository层优化

class DataRepository @Inject constructor(
    private val apiServiceInterface: ApiServiceInterface
) {
    suspend fun getCreditReport(): UiState<CreditReport> {
        return try {
            val creditReport = apiServiceInterface.getDataFromApi()
            UiState.Success(creditReport)
        } catch (e: IOException) {
            UiState.Error(e, "网络连接失败,请检查网络设置")
        } catch (e: HttpException) {
            UiState.Error(e, "服务端异常,请稍后重试")
        } catch (e: Exception) {
            UiState.Error(e, "未知错误")
        }
    }
}

3. ViewModel层优化

class MainActivityViewModel @Inject constructor(
    private val dataRepository: DataRepository
) : ViewModel() {
    // 内部用可变LiveData,外部暴露不可变LiveData,避免外部修改状态
    private val _creditReportState = MutableLiveData<UiState<CreditReport>>()
    val creditReportState: LiveData<UiState<CreditReport>> = _creditReportState

    fun getCreditReport() {
        // Retrofit的suspend函数自动切换到后台线程,不需要手动指定Dispatchers.IO
        viewModelScope.launch {
            // 先更新为加载中状态
            _creditReportState.value = UiState.Loading
            val state = dataRepository.getCreditReport()
            _creditReportState.postValue(state)
        }
    }
}

4. Fragment层优化

private fun initViewModel() {
    viewModel.creditReportState.observe(viewLifecycleOwner) { state ->
        when(state) {
            is UiState.Loading -> {
                // 展示加载动画
                showLoadingUI()
            }
            is UiState.Success -> {
                showScoreUI(true)
                binding.score.text = state.data.creditReportInfo.score.toString()
                binding.maxScoreValue.text = "out of ${state.data.creditReportInfo.maxScoreValue}"
                initDonutView(
                    state.data.creditReportInfo.score.toFloat(),
                    state.data.creditReportInfo.maxScoreValue.toFloat()
                )
            }
            is UiState.Error -> {
                showScoreUI(false)
                // 可以根据错误类型展示不同提示,这里直接用返回的错误信息
                Toast.makeText(requireContext(), state.errorMsg, Toast.LENGTH_SHORT).show()
            }
        }
    }
    viewModel.getCreditReport()
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 09:06:02