首页 > 解决方案 > 传递来自 API 调用的错误

问题描述

我使用 2 个单独liveData的暴露来显示来自 API 的错误。我基本上是在检查 API 调用是否有异常,传递一个失败状态serverErrorLiveData并将被观察。

所以我有serverErrorLiveData错误和creditReportLiveData没有错误的结果。

我认为我这样做的方式不对。您能否指导我从 API 调用中捕获错误的正确方法。此外,关于将数据从存储库传递到视图模型的任何问题/建议。

处理加载状态的正确方法是什么?

信用分数片段

    private fun initViewModel() {
    viewModel.getCreditReportObserver().observe(viewLifecycleOwner, Observer<CreditReport> {
        showScoreUI(true)
        binding.score.text = it.creditReportInfo.score.toString()
        binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
        initDonutView(
            it.creditReportInfo.score.toFloat(),
            it.creditReportInfo.maxScoreValue.toFloat()
        )
    })
    viewModel.getServerErrorLiveDataObserver().observe(viewLifecycleOwner, Observer<Boolean> {
        if (it) {
            showScoreUI(false)
            showToastMessage()
        }
    })
    viewModel.getCreditReport()
}

MainActivityViewModel

    class MainActivityViewModel @Inject constructor(
    private val dataRepository: DataRepository
) : ViewModel() {
    var creditReportLiveData: MutableLiveData<CreditReport>
    var serverErrorLiveData: MutableLiveData<Boolean>

    init {
        creditReportLiveData = MutableLiveData()
        serverErrorLiveData = MutableLiveData()
    }

    fun getCreditReportObserver(): MutableLiveData<CreditReport> {
        return creditReportLiveData
    }

    fun getServerErrorLiveDataObserver(): MutableLiveData<Boolean> {
        return serverErrorLiveData
    }

    fun getCreditReport() {
        viewModelScope.launch(Dispatchers.IO) {
            val response = dataRepository.getCreditReport()

            when(response.status) {
                CreditReportResponse.Status.SUCCESS -> creditReportLiveData.postValue(response.creditReport)
                CreditReportResponse.Status.FAILURE -> serverErrorLiveData.postValue(true)
            }
        }
    }
}

资料库

class DataRepository @Inject constructor(
        private val apiServiceInterface: ApiServiceInterface
) {

    suspend fun getCreditReport(): CreditReportResponse {
        return try {
            val creditReport = apiServiceInterface.getDataFromApi()
            CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
        } catch (e: Exception) {
            CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
        }
    }
}

ApiService接口

interface ApiServiceInterface {
    @GET("endpoint.json")
    suspend fun getDataFromApi(): CreditReport
}

信用评分响应

data class CreditReportResponse constructor(val creditReport: CreditReport?, val status: Status) {
    enum class Status {
        SUCCESS, FAILURE
    }
}

标签: androidkotlinmvvm

解决方案


有两个 LiveData 通道用于成功和失败,这会增加复杂性并增加编码错误的机会。您应该有一个 LiveData 可以提供数据或错误,以便您知道它正在有序地出现并且您可以在一个地方观察它。然后,如果您添加重试策略,例如,您将不会冒险在输入有效值后以某种方式显示错误。Kotlin 可以使用密封类以类型安全的方式促进这一点。但是您已经在使用包装类来衡量成功和失败。我认为你可以去源头并简化它。你甚至可以只使用 Kotlin 自己的 Result 类。

(旁注,你的getCreditReportObserver()andgetServerErrorLiveDataObserver()函数是完全多余的,因为它们只是返回与属性相同的东西。你不需要 Kotlin 中的 getter 函数,因为属性基本上是 getter 函数,除了挂起 getter 函数,因为 Kotlin 不支持suspend特性。)

因此,要做到这一点,请消除您的 CreditReportResponse 类。将您的回购功能更改为:

suspend fun getCreditReport(): Result<CreditReport> = runCatching {
    apiServiceInterface.getDataFromApi()
}

如果您必须使用 LiveData(我认为不使用单个检索值更简单,请参见下文),您的 ViewModel 可能如下所示:

class MainActivityViewModel @Inject constructor(
    private val dataRepository: DataRepository
) : ViewModel() {
    val _creditReportLiveData = MutableLiveData<Result<CreditReport>>()
    val creditReportLiveData: LiveData<Result<CreditReport>> = _creditReportLiveData 

    fun fetchCreditReport() { // I changed the name because "get" implies a return value
    // but personally I would change this to an init block so it just starts automatically
    // without the Fragment having to manually call it.
        viewModelScope.launch { // no need to specify dispatcher to call suspend function
            _creditReportLiveData.value = dataRepository.getCreditReport()
        }
    }
}

然后在你的片段中:

private fun initViewModel() {
    viewModel.creditReportLiveData.observe(viewLifecycleOwner) { result -> 
        result.onSuccess {
            showScoreUI(true)
            binding.score.text = it.creditReportInfo.score.toString()
            binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
            initDonutView(
                it.creditReportInfo.score.toFloat(),
                it.creditReportInfo.maxScoreValue.toFloat()
            )
        }.onFailure {
            showScoreUI(false)
            showToastMessage()
        }
    viewModel.fetchCreditReport()
}

编辑:下面将简化您当前的代码,但使您无法轻松添加失败时的重试策略。保留 LiveData 可能更有意义。

由于您只检索单个值,因此公开挂起函数而不是 LiveData 会更简洁。您可以私下使用 Deferred,这样如果屏幕旋转就不必重复获取(结果仍会到达并缓存在 ViewModel 中)。所以我会这样做:

class MainActivityViewModel @Inject constructor(
    private val dataRepository: DataRepository
) : ViewModel() {
    private creditReportDeferred = viewModelScope.async { dataRepository.getCreditReport() }

    suspend fun getCreditReport() = creditReportDeferred.await()
}

// In fragment:
private fun initViewModel() = lifecycleScope.launch {
    viewModel.getCreditReport()
        .onSuccess {
            showScoreUI(true)
            binding.score.text = it.creditReportInfo.score.toString()
            binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
            initDonutView(
                it.creditReportInfo.score.toFloat(),
                it.creditReportInfo.maxScoreValue.toFloat()
            )
        }.onFailure {
            showScoreUI(false)
            showToastMessage()
        }
}

推荐阅读