android - 传递来自 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
}
}
解决方案
有两个 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()
}
}
推荐阅读
- javascript - Vue 3 第三方组件是否与 Vue 2 兼容?
- javascript - 忽略正则表达式中的组匹配
- safari - 使用 iOS 的可见性层问题(使用 mapbox-gl)
- php - 如何在刀片视图中显示来自不同表的元素列表?
- javascript - 如何配置 monaco 编辑器只允许使用 es5 脚本(防止使用 let 和 const)
- amazon-web-services - 运行 Spark 作业后创建和销毁集群的步骤函数
- apache-spark - 在 DataFrame 中转义换行符
- pdf - 如何使用ghostscript缩小每页pdf页面的内容?
- vue.js - 将新数据插入列表时 watchEffect 不运行
- python - 如何根据 txt 文件上的描述对图像进行分类?