Функция приостановки может быть вызвана только в теле сопрограммы

Я пытаюсь обновлять свое представление в реальном времени с помощью Kotlin Flows и Firebase.

Вот как я собираю данные в реальном времени из ViewModel:

class MainViewModel(repo: IRepo): ViewModel() {

    val fetchVersionCode = liveData(Dispatchers.IO) {
        emit(Resource.Loading())

        try {
            repo.getVersionCode().collect {
                emit(it)
            }

        } catch (e: Exception){
            emit(Resource.Failure(e))
            Log.e("ERROR:", e.message)
        }
    }
}

И вот как я испускаю каждый поток данных из моего репо всякий раз, когда значение изменяется в Firebase:

class RepoImpl: IRepo {

    override suspend fun getVersionCodeRepo(): Flow<Resource<Int>> = flow {

        FirebaseFirestore.getInstance()
            .collection("params").document("app").addSnapshotListener { documentSnapshot, firebaseFirestoreException ->
                val versionCode = documentSnapshot!!.getLong("version")
                emit(Resource.Success(versionCode!!.toInt()))
            }
    }

Проблема в том, что когда я использую:

 emit(Resource.Success(versionCode!!.toInt()))

Android Studio выделяет вызов emit с помощью:

Функция приостановки 'emit' должна вызываться только из сопрограммы или другой функции приостановки

Но я вызываю этот код из CoroutineScope в моем ViewModel.

В чем проблема?

Спасибо

2 ответа

Решение

Слушатель моментальных снимков Firestore - это, по сути, асинхронный обратный вызов, который выполняется в другом потоке, не имеющем ничего общего с потоками сопрограмм, управляемыми Kotlin. Вот почему ты не можешь позвонитьemit() внутри асинхронного обратного вызова - обратный вызов просто не находится в контексте сопрограммы, поэтому он не может приостанавливаться, как сопрограмма.

То, что вы пытаетесь сделать, требует, чтобы вы поместили свой вызов для передачи обратно в контекст сопрограммы, используя любой метод, который вы считаете подходящим (например, launch) или, возможно, запустить callbackFlow, который позволяет вам предлагать объекты из других потоков.

В suspend ключевое слово на getVersionCodeRepo() не относится к emit(Resource.Success(versionCode!!.toInt()))потому что он вызывается из лямбды. Поскольку вы не можете изменитьaddSnapshotListener вам нужно будет использовать построитель сопрограмм, например launch призвать suspend функция.

Когда лямбда передается в функцию, объявление соответствующего параметра функции определяет, может ли она вызвать функцию приостановки без построителя сопрограмм. Например, вот функция, которая принимает параметр функции без аргументов:

fun f(g: () -> Unit)

Если эта функция называется так:

f {
    // do something
}

все, что находится в фигурных скобках, выполняется так, как если бы оно находилось внутри функции, объявленной как:

fun g() {
    // do something
}

поскольку g не заявлен с suspend ключевое слово, оно не может вызвать suspend без использования построителя сопрограмм.

Однако если f() объявляется так:

fun f(g: suspend () -> Unit)

и называется так:

f {
    // do something
}

все, что находится в фигурных скобках, выполняется так, как если бы оно находилось внутри функции, объявленной как:

suspend fun g() {
    // do something
}

поскольку g будет объявлен сsuspendключевое слово, оно может вызыватьsuspend без использования построителя сопрограмм.

На случай, если addEventListener лямбда вызывается так, как если бы она вызывалась внутри функции, объявленной как:

public abstract void onEvent (T value, FirebaseFirestoreException error)

Поскольку это объявление функции не имеет suspend ключевое слово (не может, оно написано на Java), то любая переданная ему лямбда должна использовать построитель сопрограмм для вызова функции, объявленной с suspend ключевое слово.

Другие вопросы по тегам