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
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With