Suspending function can only be called within coroutine body
Asked Answered
C

2

8

I'm trying to deliver realtime updates to my view with Kotlin Flows and Firebase.

This is how I collect my realtime data from my 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)
        }
    }
}

And this is how I emit each flow of data from my repo whenever a value changes in 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()))
            }
    }

The problem is that when I use:

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

Android Studio highlights the emit invocation with:

Suspend function 'emit' should be called only from a coroutine or another suspend function

But I'm calling this code from a CoroutineScope in my ViewModel.

What's the problem here?

thanks

Corenda answered 24/2, 2020 at 19:44 Comment(0)
C
11

A Firestore snapshot listener is effectively an asynchronous callback that runs on another thread that has nothing to do with the coroutine threads managed by Kotlin. That's why you can't call emit() inside an asynchronous callback - the callback is simply not in a coroutine context, so it can't suspend like a coroutine.

What you're trying to do requires that you put your call to emit back into a coroutine context using whatever method you see fit (e.g. launch), or perhaps start a callbackFlow that lets you offer objects from other threads.

Cyril answered 24/2, 2020 at 20:38 Comment(2)
i am exploring the Kotlin coroutine as well. Do you know any reference to support your explanation? Thanks!Indiscrimination
@YazidEF here is a very good explanation and example of the problem: developer.android.com/kotlin/flow#create With the flow builder, the producer cannot emit values from a different CoroutineContext. Therefore, don't call emit in a different CoroutineContext by creating new coroutines or by using withContext blocks of code. You can use other flow builders such as callbackFlow in these cases.Burbot
J
6

The suspend keyword on getVersionCodeRepo() does not apply to emit(Resource.Success(versionCode!!.toInt())) because it being called from within a lambda. Since you can't change addSnapshotListener you'll need to use a coroutine builder such as launch to invoke a suspend function.

When a lambda is passed to a function, the declaration of its corresponding function parameter governs whether it can call a suspend function without a coroutine builder. For example, here is a function that takes a no-arg function parameter:

fun f(g: () -> Unit)

If this function is called like so:

f {
    // do something
}

everything within the curly braces is executed as though it is within a function that is declared as:

fun g() {
    // do something
}

Since g is not declared with the suspend keyword, it cannot call a suspend function without using a coroutine builder.

However, if f() is declared thus:

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

and is called like so:

f {
    // do something
}

everything within the curly braces is executed as though it is within a function that is declared as:

suspend fun g() {
    // do something
}

Since g is declared with the suspend keyword, it can call a suspend function without using a coroutine builder.

In the case of addEventListener the lambda is being called as though it is called within a function that is declared as:

public abstract void onEvent (T value, FirebaseFirestoreException error)

Since this function declaration does not have the suspend keyword (it can't, it is written in Java) then any lambda passed to it must use a coroutine builder to call a function declared with the suspend keyword.

Judicious answered 24/2, 2020 at 21:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.