Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Suspending function can only be called within coroutine body

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

like image 912
SNM Avatar asked Feb 24 '20 19:02

SNM


2 Answers

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.

like image 41
bartonstanley Avatar answered Sep 28 '22 07:09

bartonstanley


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.

like image 147
Doug Stevenson Avatar answered Sep 28 '22 08:09

Doug Stevenson