Передача ошибок, возникающих при вызове API

#android #kotlin #mvvm

Вопрос:

Я использую 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()
}
 

mainactivityviewмодель

     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
    }
}
 

Ответ №1:

Это создает сложность и увеличивает вероятность ошибки кодирования, если у вас есть два канала LiveData для успеха и неудачи. У вас должны быть единственные данные LiveData, которые могут предоставить данные или ошибку, чтобы вы знали, что они поступают упорядоченно, и вы могли наблюдать их в одном месте. Тогда, например, если вы добавите политику повторных попыток, вы не рискуете каким-либо образом выдать ошибку после ввода допустимого значения. Котлин может облегчить это типобезопасным способом, используя закрытый класс. Но вы уже используете класс-оболочку для успеха и неудачи. Я думаю, вы можете обратиться к источнику и упростить его. Вы даже можете просто использовать собственный класс результатов Kotlin.

(Обратите внимание, что ваши getCreditReportObserver() getServerErrorLiveDataObserver() функции и полностью избыточны, потому что они просто возвращают то же самое, что и свойство. Вам не нужны функции получения в Kotlin, потому что свойства в основном являются функциями получения, за исключением функций приостановки получения, поскольку Kotlin не поддерживает suspend свойства.)

Итак, чтобы сделать это, исключите свой класс CreditReportResponse. Измените функцию репо на:

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

Если вам необходимо использовать LiveData (я думаю, что проще не использовать ни одно полученное значение, см. Ниже), ваша модель представления может выглядеть так:

 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. Вы можете в частном порядке использовать отложенный, чтобы при повороте экрана не пришлось повторять выборку (результат все равно будет получен и сохранен в кэше в модели просмотра). Так что я бы сделал:

 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()
        }
}
 

Комментарии:

1. большое вам спасибо за подробное объяснение и предложения, которые очень полезны и отлично исправлены.

2. )4, спасибо за ваш ответ, когда я пытаюсь сделать это в репозитории, я получаю 'kotlin.Result' cannot be used as a return type

3. извините, я решил, что мне пришлось добавить freeCompilerArgs = ["-Xallow-result-return-type"] в Gradle ` kotlinOptions { jvmTarget = ‘1.8’ freeCompilerArgs = [«-Xallow-результат-тип возврата»] }`

4. Убедитесь, что вы обновились до последней версии Kotlin (в настоящее время 1.5.2). Тогда вам не придется возиться с kotlinOptions. Это было ограничение, которое они имели в версиях до 1.5.