Это интересная проблема, и я хочу поделиться подходом к ее решению, надеясь, что это поможет.
Основное решение
Вы можете восстановить вычисление myMethod()
, используя Deferred
. В основном код может выглядеть следующим образом:
val myMethodDeferred = GlobalScope.async {
myMethod()
}
suspend fun foo(){
myMethodDeferred.await()
}
Функция async
возвращает объект Deferred
, который содержит вычисление myMethod
. Когда вы вызываете myMethodDeferred.await()
, он ожидает окончания вычисления и возвращает результат myMethod
(для использования при необходимости).
Если вы хотите, чтобы вычисление выполнялось только при вызове foo()
Вы можете добавить параметр к вызову async
следующим образом: async(start = CoroutineStart.LAZY){ ...
. Это приведет к ленивому запуску вычисления при первом вызове .await()
.
(GlobalScope
можно заменить любым CoroutineScope
или областью, которая передается в качестве параметра конструктора классу. Если myMethod()
не является функцией приостановки, а функцией блокировки, как вы описали, вы можете использовать соответствующий CoroutineScope, например, CoroutineScope(Dispatchers.IO)
, если он выполняет вычисления ввода-вывода.)
Обработка ошибок
Недостатком этого решения является случай, когда myMethod()
завершился неудачей с исключением. Если произошел сбой myMethod()
, сбой будет сохранен в отложенном режиме, и каждый вызов на foo()
тоже будет неудачным, вместо того, чтобы пытаться снова запустить myMethod()
.
Для обработки сбоев myMethod()
,Я предлагаю объявить класс с именем Retryable
, который будет содержать вычисления, которые можно повторить, аналогично тому, как Deferred
проводит вычисления. Код:
class Retryable<T>(private val computation: suspend CoroutineScope.() -> T) {
private var deferred: Deferred<T>? = null
suspend fun await(): T {
val oldDeferred = deferred
val needsRetry = oldDeferred == null || oldDeferred.isCompleted && oldDeferred.getCompletionExceptionOrNull() != null
if (needsRetry) {
deferred = GlobalScope.async(block = computation)
}
return deferred!!.await()
}
}
Использование должно выглядеть следующим образом:
val myMethodRetryable = Retryable { myMethod() }
suspend fun foo(){
myMethodRetryable.await()
}
Теперь каждый вызов foo()
будет ожидать завершения вычисления myMethod()
, ноесли он уже завершился с ошибкой, он перезапустит его и будет ждать новых вычислений.
(Здесь CoroutineScope
должен быть передан в качестве параметра Retryable
, но я не хочуусложнить пример кода.)
Альтернативное решение: Используйте асинхронную фабричную функцию
Поскольку myMethod()
следует вызывать только один раз, если вы не можете вызвать ее на init {}
, вы можетевместо этого вызовите ее для заводской функции.
Пример кода:
class MyClass(private val myMethodResult: String) {
companion object {
suspend fun create(): MyClass {
val myMethodResult = myMethod()
return MyClass(myMethodResult)
}
private suspend fun myMethod(): String {
...
}
}
...
}
Полезно, если вы можете инициализировать MyClass
один раз из функции приостановки перед ее использованием.
(Если myMethod()
не является функцией приостановки, а блокирующей функцией, как вы описали, вы можете захотеть обернуть ее соответствующим контекстом, например withContext(Dispatchers.IO) { myMethod() }
, если она выполняет вычисления ввода / вывода.)