Кажется, я не могу выполнить обработку ошибок в сопрограммах. Я читал много статей и документация по обработке исключений, но не могу заставить его работать.
Вот моя установка:
Моя ViewModel
запускает сопрограмму со своей областью видимости
class MyViewModel(private var myUseCase: MyUseCase) : ViewModel() {
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun doSomething() {
uiScope.launch {
try {
myUseCase()
} catch (exception: Exception) {
// Do error handling here
}
}
}
}
Мой UseCase
просто обрабатывает несколько логических операций, и в данном случае это какой-то валидатор.
class MyUseCase(private val myRepository: MyRepository) {
suspend operator fun invoke() {
if (checker()) {
throw CustomException("Checker Failed due to: ...")
}
myRepository.doSomething()
}
}
Тогда мой Repository
просто обрабатывает сетевой уровень / локальный уровень
object MyRepository {
private val api = ... // Retrofit
suspend fun doSomething() = api.doSomething()
}
А вот и мой модифицированный интерфейс
interface MyInterface {
@POST
suspend fun doSomething()
}
Команда try/catch из ViewModel
может обработать ошибку вызова Retrofit, но не может перехватить ошибку из CustomException
, выброшенную UseCase
. Из статей, которые я читал, это должно работать. Если я использую async
, я могу сделать await
и использовать ошибку, но мне не нужно использовать async
в этом случае, и я обдумывал это. Я могу потеряться.
Любая помощь будет принята с благодарностью! Заранее спасибо!
Редактировать:
Вот журнал ошибок, который я получаю:
com.example.myapp.domain.errors.CustomException
at com.example.myapp.domain.FeatureOne$invoke$2.invokeSuspend(FeatureOne.kt:34)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
Ошибка напрямую указывает на явный оператор throw
.
Ага. Я использую его вместе с последней версией. 1.2.1 и 1.3.30 для Котлина
Обновлен вопрос, чтобы показать трассировку стека
Попытка с CoroutineExceptionHandler
может быть обходным путем для обработка исключений внутри сопрограмм.
Элемент CoroutineExceptionHandlerконтекст используется как общий блок catch сопрограммы, где может выполняться обработка пользовательское ведение журнала или исключение. Это похоже на использование Thread.uncaughtExceptionHandler
.
Как это использовать?
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
throw ArithmeticException() // Nothing will be printed, relying on user to call
deferred.await()
}
joinAll(job, deferred)
В своем ViewModel
убедитесь, что ваш uiScope
использует SupervisorJob
, а не Job
. SupervisorJob
может справиться с детской неудачей индивидуально. Job
будет отменен, в отличие от SupervisorJob
Если вы используете 2.1.0
для AAC Lifecycle и ViewModel, вместо этого используйте расширение viewModelScope
.
Я тоже пробовал! Но это спасает исключение. В следующий раз, когда он будет вызван, он просто вернет то же самое исключение без повторного выполнения блока запуска.
Может быть из-за вашего варианта использования? Вы все равно бросаете CustomException
перед вызовом вызов репо. Возможно, вы каждый раз получаете исключение. Как насчет этого checker()
метода?
Предположительно, CustomException
должен быть пойман блоком try/catch
в ViewModel
. Или я ошибаюсь? checker()
— это просто своего рода валидатор, который я использую. Например, если ввод нулевой, я бросаю NoInputException
или что-то в этом роде. Чего я пытаюсь добиться, так это распространить исключение на Activity
и получить соответствующую строку из строковых ресурсов и показать диалог.
Нет, в том то и дело!! Блок Попробуйте поймать обычно не работает в сопрограммы таким образом. Вот почему нам нужно передать контекст как CoroutineExceptionHandler
, чтобы он работал как Thread.uncaughtExceptionHandler
.
Да, в том-то и дело. Однако CoroutineExceptionHandler
поймал это, как только исключение поймано, это результат каждый раз. Есть ли способ, чтобы он этого не делал?
Никогда не пробовал, так что не могу быть в этом уверен. Постараюсь скоро сообщить.
Это работает, когда я использую GlobalScope
, но терпит неудачу, если я использую область действия ViewModel
, используя uiScope
. Документы говорят, что if there is a Job in the context, then Job.cancel is invoked;
Наверное, поэтому?
Думаю, это поможет: CoroutineScope(Dispatchers.Main + viewModelJob)
для вашего uiScope.
Спасибо! Это заставило меня приблизиться к проблеме. Я обновил код ViewModel
выше. Я использовал Job
, а не SupervisorJob
. Изменил и теперь исправлено!
Просто для справки: try-catch
вокруг suspend fun
является должен работать естественным образом и перехватывать любые исключения из вызываемого кода, и это все еще будет работать даже после приостановки и возобновления сопрограммы. Неудача ОП, скорее всего, связана с неправильным использованием async-await
. Если задача async
выдает исключение, это отменяет сопрограмму async
, распространяя отмену на родительскую сопрограмму. SupervisorJob
игнорирует отмену, но это обходной путь, а не решение.
Хорошо, @MarkoTopolnik, поэтому основная причина в том, что SupervisorJob
обрабатывает Попробуйте поймать сам, не сообщая о конечной точке, откуда он был вызван.
Нет, здесь действуют два различных механизма. Один — это отмена и его обнаружение, а другой — обработка исключений. Экземпляр Job
связан с сопрограммой (она находится в ее контексте), но напрямую не участвует в ее выполнении. Должен быть явный код, который перехватывает исключения на верхнем уровне сопрограммы и вызывает coroutineContext.job.cancel()
(этот код является внутренним для async
). Реализация Job.cancel()
по умолчанию, в свою очередь, вызывает parentJob.childCancelled()
, и таким образом отмена достигает родителя SupervisorJob
, чей childCancelled()
не работает.
Теперь любой правильно написанный код не позволит необработанному исключению уйти от сопрограммы. Однако пользователи обычно неправильно используют async-await
вместо withContext
, неосознанно создавая подпрограмму без обработки исключений и предполагая, что try-catch
вокруг deferred.await()
они обрабатывают исключения внутри блока async
. Но это просто обрабатывает исключение, с которым было завершено deferred
. К тому времени async
сопрограмма уже была отменена.
Насколько я знаю, Retrofit до сих пор не создал способ пометить методы ключевым словом suspend
. Вы можете сослаться на этот связь.
Таким образом, правильный способ вашего MyInterface
будет:
interface MyInterface {
@POST
fun doSomething(): Deferred<Response<YourDataType>>
}
Я использую 2.5.1-SNAPSHOT version
. Добавлена первоклассная поддержка suspend
. См. github.com/square/retrofit/pull/2886
Есть ли способ обрабатывать исключения, такие как тайм-аут, исключение сокета и т.д.
Да, подтвержденный ответ охватывает это
Другой способ решить эту проблему — скрыть ваш пользовательский объект ошибки для реализации CancellationException
Например:
Ваш CustomException
может быть реализован как:
sealed class CustomError : CancellationException() {
data class CustomException(override val message: String = "Checker Failed due to: ...") : CustomError
}
Это исключение попадет в блок try/catch модели представления.
'org.jetbrains.kotlinx:kotlinx-coroutines-android'
Вы используете этот артефакт? В основном это специфично для среды Android, поэтому в случае сбоя сопрограммы с необработанным исключением исключение регистрируется перед сбоем приложения Android.