Я хотел бы реорганизовать некоторый код, использующий стиль обратного вызова, в код Coroutine, но я ломаю голову.
Код выглядит следующим образом:
fun authenticate(/* parameters */) {
val builder = // Obtaining the builder
builder
.withCallback(getAuthenticationCallback())
.build()
// Using the builder
}
где getAuthenticationCallback определяется как:
private fun getAuthenticationCallback(): AuthenticationCallback {
return object : AuthenticationCallback {
override fun onSuccess(authenticationResult: AuthenticationResult) {
}
override fun onError(exception: AuthenticationException) {
}
override fun onCancel() {
}
}
}
Поскольку создание построителя изменить нельзя (это не мой код), я могу действовать только по suspend fun authenticate и второй части кода.
Есть ли чистый способ изменить код с помощью Coroutine? Я думал передать CoroutineScope в качестве параметра, но не уверен, что это правильно.





Вы ищете suspendCancellableCoroutine, который предоставляет обширную документацию о том, как обрабатывать случаи, практически такие же, как у вас.
Хороший способ аккуратно обрабатывать обратные вызовы — преобразовать их в поток:
fun authenticate(/* parameters */): Flow<AuthState> = callbackFlow {
val builder = builder
.withCallback(getAuthenticationCallback())
.build()
// Using the builder
doSomeStuffWithBuilder(builder)
awaitClose {
// cleanup here, for example:
// * unregister listener
// * close resources
// * whatever else is necessary
}
}
Это возвращает поток результатов обратного вызова.
Хотя authenticate не является функцией приостановки, лямбда callbackFlow есть, и она будет выполняться только как часть сопрограммы. Мы вернемся к этому позже, чтобы увидеть, как именно это делается.
Общая идея заключается в том, что все, что нужно сделать с помощью builder, происходит здесь (или в doSomeStuffWithBuilder(builder), чтобы код было легче читать). awaitClose в конце приостанавливает выполнение, даже когда doSomeStuffWithBuilder завершается, поэтому потенциальные обратные вызовы, которые появятся в какой-то более поздний момент времени, не будут потеряны. Его лямбда-выражение выполняется, когда обратные вызовы больше не нужны, и его можно использовать для очистки. Также может быть пустым, если ничего не нужно убирать.
Теперь перейдем к самому интересному: фактические обратные вызовы преобразуются в значения потока. Нам нужно изменить getAuthenticationCallback вот так:
private fun ProducerScope<AuthState>.getAuthenticationCallback(): AuthenticationCallback =
object : AuthenticationCallback {
override fun onSuccess(authenticationResult: AuthenticationResult) {
trySendBlocking(AuthState.Success(authenticationResult))
}
override fun onError(exception: AuthenticationException) {
trySendBlocking(AuthState.Error(exception))
}
override fun onCancel() {
trySendBlocking(AuthState.Cancelled)
}
}
Все, что вы ранее делали в этих обратных вызовах, должно возвращать результат в виде объекта AuthState:
sealed interface AuthState {
data class Success(val authenticationResult: AuthenticationResult) : AuthState
data class Error(val exception: AuthenticationException) : AuthState
data object Cancelled : AuthState
}
Эта структура данных — это то, что я только что придумал, не стесняйтесь адаптировать ее ко всему, что вам нужно.
Я сказал, что обратные вызовы должны возвращать такой объект: они, очевидно, не могут вернуть его как возврат функции, потому что у вас нет контроля над интерфейсом AuthenticationCallback. Однако это не важно, потому что вместо этого мы используем trySendBlocking. Обратите внимание, что я изменил getAuthenticationCallback, чтобы использовать ProducerScope в качестве приемника. В этой области предусмотрены различные методы send, выберите тот, который вам больше всего подходит (например, trySendBlocking). Фактическая область видимости, которая здесь используется, обеспечивается лямбдой callbackFlow, которую мы вызываем getAuthenticationCallback. Все отправленные значения передаются в поток, который возвращается из callbackFlow.
Наконец, давайте посмотрим, как используется эта новая функция authenticate:
val flow = authenticate(/* parameters */)
flow.collect {
when (it) {
is AuthState.Success -> TODO()
is AuthState.Error -> TODO()
is AuthState.Cancelled -> TODO()
}
}
Как уже говорилось вначале, authenticate не является функцией приостановки и немедленно возвращает поток.
Затем вы можете collect значения Flow. Это функция приостановки, поэтому ее нужно вызывать из сопрограммы. И только теперь в этой сопрограмме выполняется лямбда callbackFlow.
Преимущество потоков заключается в том, что их можно собирать из любого места вашей программы. После вызова authenticate все необходимое содержится в объекте возвращаемого потока, и вы можете передать его туда, где это необходимо. Как только вызывается терминальный оператор (например, collect), поток оживает, выполняет какие-то действия и слушает обратные вызовы. Когда сбор отменяется, поток также автоматически отменяется и выполняется лямбда awaitClose.
Если, например, у вас есть пользовательский интерфейс, поток можно (и нужно!) собирать там. Вам нужно будет передать поток вверх в пользовательский интерфейс, попутно, возможно, применив к потоку любую из различных функций преобразования, таких как mapLatest, onEach и т. д., или даже combine к другим потокам. Подробнее о потоках можно прочитать в документации: https://kotlinlang.org/docs/flow.html
В качестве примечания: это может быть актуально, поскольку большая часть кода Kotlin с пользовательским интерфейсом является частью приложения Android: в этом случае вы должны передать поток в свою модель представления и преобразовать его там в StateFlow, используя stateIn с viewModelScope как параметр. Затем вы можете использовать функцию Compose collectAsStateWithLifecycle в пользовательском интерфейсе для получения значений потока. Это уже даже не функция приостановки, она выбирает подходящую область сопрограммы под капотом для сбора потока.