https://kotlinlang.org logo
#coroutines
Title
# coroutines
f

Franco

04/12/2020, 9:05 AM
Hi everyone, I was wondering if there is a way to get a callback when a
suspendCancellableCoroutine
completes so I can unsubscribe from the callback that might return more values later on. My use case is that I'm trying to get the user from Firebase using the
onAuthStateChanged()
listener as they suggest here. However, I don't want to keep listening for this after getting it at the start of the app and if I don't unsubscribe from it then I get the error
IllegalStateException: Already resumed, but proposed with update
. This is an example of my code:
Copy code
suspend fun getUser(): User = suspendCancellableCoroutine { continuation ->
    val unsubscribe = firebaseAuth.onAuthStateChanged(
        nextOrObserver = { user -> continuation.resume(user) },
        error = { error -> continuation.resumeWithException(...) }
    )
}
e

Erik

04/12/2020, 9:59 AM
If that's Firebase Auth for web, then I think the
onAuthStateChanged
function returns an unsubscribe function. When you get the first callback, invoke the unsubscribe function before passing the user to resume the continuation.
f

Franco

04/12/2020, 10:04 AM
Thanks for the response @Erik. I have tried that already but it doesn't seem to work cause the
unsubscribe
function can't be found inside the callback itself. Do you have any idea if I might be doing something wrong? The code is the same I shared plus adding the
unsubscribe()
call inside the callback and the error I see is
Unresolved reference: unsubscribe
e

Erik

04/12/2020, 10:06 AM
Ah yes, I see now that you already figured that out 🤦‍♂️ since you have
val unsubscribe
in your example. Let me think of something
f

Franco

04/12/2020, 10:08 AM
Thanks 🙂
e

Erik

04/12/2020, 10:13 AM
Would something like this work?
Copy code
suspendCancellableCoroutine { continuation ->
        lateinit var unsubscribe: firebase.Unsubscribe /* or whatever type it is */
        val onUser = { user: User ->
            unsubscribe()
            continuation.resume(user)
        }
        
        unsubscribe = firebaseAuth.onAuthStateChanged(
	        nextOrObserver = onUser,
	        error = { error -> continuation.resumeWithException(...) }
		)
    }
It's not pretty, but it should work
Instead of
lateinit var
you could also use a nullable
var
, which also isn't pretty 😛
And since the coroutine might be cancelled, don't forget to cancel your observer in
continuation.invokeOnCancellation { }
f

Franco

04/12/2020, 10:51 AM
That actually does work, thanks @Erik 🙂 It is indeed not the prettiest but it'll do for now given the restrictions, I wish there was just a callback
onComplete()
on the continuation 😕 I had tried something similar which had not worked, I guess the trick is to take the callback outside of the
onAuthStateChanged
call.
f

fatih

04/12/2020, 10:54 AM
Isn't there a continuation already? Completed one @Franco https://firebase.google.com/docs/reference/js/firebase.auth.Auth#on-auth-state-changed
f

Franco

04/12/2020, 11:07 AM
That is the
completed
from the
onAuthStateChanged
@fatih, the one I'd like to have is the one from the coroutine continuation
👍 1
Not sure when that completed is called from
onAuthStateChanged
but it doesn't seem to be called after a value is received, I tested it just now
e

Erik

04/12/2020, 11:17 AM
The coroutine continuation is completed when you call
resume
on it, i.e. when the suspend fun returns a value (or throws)
f

Franco

04/12/2020, 12:07 PM
That is not what I meant, what I mean is having an
onComplete
callback so I can call the
unsubscribe
from without having to add the hacky code
d

Dominaezzz

04/12/2020, 1:11 PM
How about this?
Copy code
suspend fun getUser(): User {
    return callbackFlow<User> {
        val unsubscribe = firebaseAuth.onAuthStateChanged(
            nextOrObserver = { user -> channel.offer(user) },
            error = { error -> channel.close(error) }
        )
        awaitClose { unsubscribe() }
    }.first()
}
f

Franco

04/12/2020, 2:17 PM
Thanks for the suggestion @Dominaezzz 👍, that code is cleaner which I like better than the one I have now, but forcing the use of Flows where there is no need for a stream of data seems a bit much
d

Dominaezzz

04/12/2020, 2:24 PM
I'd argue a flow is a better representation of that firebase API but I sort of see your point. So how about this.
Copy code
suspend fun getUser(): User {
    val latch = CompletableDeferred<User>()
    val unsubscribe = firebaseAuth.onAuthStateChanged(
        nextOrObserver = { user -> latch.complete(user) },
        error = { error -> latch.completeExceptionally(error) }
    )
    try {
        return latch.await()
    } finally {
        unsubscribe()
    }
}
e

Erik

04/12/2020, 3:00 PM
Dominic's answers grasp the problem nicely: the call to
onAuthStateChanged
and
unsubscribe
must be decoupled through some communication mechanism, e.g. a channel or the completable deferred. I agree that a flow better represents this Firebase API.
f

Franco

04/12/2020, 5:05 PM
Thanks for the info @Dominaezzz, that code looks great, I will give it a go :) I totally agree that Flow better represents the Firebase API and I do use it for some cases where I want to keep getting the streams of data, however, for this specific scenario I just want to get one value for a synchronous process. Thanks for all the help guys!! 👍
6 Views