Code from Coroutines callbackFlow is called only one time - android

I have function:
#ExperimentalCoroutinesApi
override suspend fun checkIsPostLiked(userId: String, postId: String): Flow<FirebaseEventResponse> = callbackFlow {
try {
FirebaseFirestore.getInstance().collection(postCollection).document(postId).get().addOnSuccessListener {
trySend(SuccessDocument(it))
}.addOnFailureListener {
trySend(ExceptionEvent(it))
}.await()
} catch (e: Exception) {
trySend(ExceptionEvent(e))
}
awaitClose { this.cancel() }
}
When i want to use it second time code is not called. I tested another function with only Log.d inside and had the same problem.

A flow is a type that can emit multiple values sequentially. An addOnSuccessListener or addOnFailureListener would emit their results just once. So what you probably want to use is a suspendCancellableCoroutine builder that can be used for one-shot requests.
Here's what that might look like:
override suspend fun checkIsPostLiked(userId: String, postId: String): FirebaseEventResponse = suspendCancellableCoroutine { continuation ->
try {
FirebaseFirestore.getInstance().collection(postCollection).document(postId).get().addOnSuccessListener {
continuation.resume(SuccessDocument(it), null)
}.addOnFailureListener {
continuation.resumeWithException(ExceptionEvent(it))
}
} catch (e: Exception) {
continuation.resumeWithException(ExceptionEvent(e))
}
}
And you can call it in any coroutine scope (e.g viewModelScope) like so:
viewModelScope.launch {
try {
val isPostLiked = checkIfPostIsLiked(userId, postId)
} catch(e: Exception) {
// handle exception
}
}
Side benefit: You don't have to use the #ExperimentalCoroutinesApi decorator ;).
EDIT
If you're using await then you're already using Firestore with coroutines. So there's no need to use any coroutine builder, just call the suspended function without the addOnSuccessListener and addOnFailureListener
override suspend fun checkIsPostLiked(userId: String, postId: String): FirebaseEventResponse =
try {
FirebaseFirestore.getInstance()
.collection(postCollection)
.document(postId)
.get()
.await()
} catch (e: Exception) {
// handle exception
}
}

Related

How do I correctly handle an exception from the callback within a callbackFlow scope?

I wrote the following callbackFlow:
fun getData(id: String) = callbackFlow {
val listener = object : ValueEventListener {
// if I throw here, the app crash
override fun onDataChange(snapshot: DataSnapshot) {
snapshot.getValue(Data::class.java) ?: throw RuntimeException("Error while reading data")
}
override fun onCancelled(error: DatabaseError) {
throw error.toException()
}
}
// if I throw here, exception is handled correctly by CoroutineExceptionHandler
val dbRef = getDataRef(id)
dbRef.addValueEventListener(listener)
awaitClose { dbRef.removeEventListener(listener) }
}
And I am collecting the flow like this:
fun getData(id: String) = viewModelScope.launch(errorHandler) {
db.getData(id).collect {
// do something
}
}
Where errorHandler is:
val errorHandler: CoroutineExceptionHandler
get() = CoroutineExceptionHandler { _, throwable ->
// do something with error
}
If I throw an exception from withing the onDataChange or the onCancelled methods the app crash.
Is it possible to let the callbackFlow scope handle the exception? Am I throwing it in a wrong way? Should I call trySend with a proper sealed class instead of throwing an exception (and maybe decide in the receiver what to do with it)?
What's the best way to handle such a situation? Thanks in advance.
Callbacks are executed in the context of the API/database framework, so if we throw inside the callback, we actually crash that API/database component, not the flow.
To send the error through the flow you can use a result object as suggested by #alex-mamo . However, if you prefer to send the exception directly to fail on the collect side, you can just cancel the producer scope:
override fun onCancelled(error: DatabaseError) {
this#callbackFlow.cancel("error message", error)
}
This is similar to this:
flow {
delay(1000)
throw Exception("error")
}
Also, CoroutineExceptionHandler is not meant to replace how we handle exceptions in Kotlin. It is a last resort handler for unhandled exceptions, so we can e.g. log them in the way specific to our application. In your case it seems better to use a regular try ... catch.
It's not mandatory to use coroutines with the Firebase API, but it certainly makes our development easier. Maybe there are also other solutions out there, but this is how I would do:
#ExperimentalCoroutinesApi
fun getDataFromFirebase(id: String) = callbackFlow {
val listener = object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
trySend(Result.Success(snapshot.getValue(Data::class.java)))
}
override fun onCancelled(error: DatabaseError) {
trySend(Result.Error(error))
}
}
val dbRef = getDataRef(id)
dbRef.addValueEventListener(listener)
awaitClose {
dbRef.removeEventListener(listener) }
}
In the ViewModel class I would use:
#ExperimentalCoroutinesApi
fun getData(id: String) = liveData(Dispatchers.IO) {
repository.getDataFromFirebase(id).collect { response ->
emit(response)
}
}
And inside the activity class I would use something like this:
#ExperimentalCoroutinesApi
private fun getData() {
viewModel.getData().observe(this) { response ->
when(response) {
is Result.Success -> print("Success")
is Result.Error -> print("Error")
}
}
}

Coroutine and Callback handler in kotlin

I am currently building an app using AWS SDK. One of the API is a sign in and is requiring, in addition to email and password, a Callback in order to get back the status of the request. The issue is that I am not able to send back the result.
This is my code:
override suspend fun signIn(email: String, password: String): Result<SignInResult> =
withContext(ioDispatcher) {
try {
api.signIn(email, password, object : Callback<SignInResult> {
override fun onResult(result: SignInResult?) {
Result.Success(result!!)
}
override fun onError(e: Exception?) {
Result.Error(e!!)
}
})
} catch (e: Exception) {
Result.Error(e)
}
}
The issue is that coroutine sign in is requiring a return of Result but I do not know what to return because I should only return when onResult, onError and when catching an exception.
Any idea how to make it works ?
Thanks
You can use suspendCoroutine or suspendCancellableCoroutine to work with callbacks:
override suspend fun signIn(email: String, password: String): Result<SignInResult> =
suspendCoroutine { continuation ->
try {
api.signIn(email, password, object : Callback<SignInResult> {
override fun onResult(result: SignInResult) {
// Resume coroutine with a value provided by the callback
continuation.resumeWith(Result.Success(result))
}
override fun onError(e: Exception) {
continuation.resumeWith(Result.Error(e))
}
})
} catch (e: Exception) {
continuation.resumeWith(Result.Error(e))
}
}
suspendCoroutine suspends coroutine in which it executed until we decide to continue by calling appropriate methods - Continuation.resume.... suspendCoroutine mainly used when we have some legacy code with callbacks.
There is also suspendCancellableCoroutine builder function, it behaves similar to suspendCoroutine with additional feature - provides an implementation of CancellableContinuation to the block.

FirebaseAuth - how can I wait for value

I use FirebaseAuth for registration new user
class FirebaseAuthenticationServiceImpl(): FirebaseAuthenticationService {
override fun registerWithEmailAndPassword(email: String, password: String): Boolean {
val registration = FirebaseAuth.getInstance().createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
println(it.additionalUserInfo?.providerId.toString())
}.addOnFailureListener {
println(it.message.toString())
}
return registration.isSuccessful
}
}
I call function above and every time I get false. After some time I get true
coroutineScope {
try {
if (firebaseService.registerWithEmailAndPassword(email, password)) {
openHomeActivity.offer(Unit)
} else {}
} catch (e: Exception) {}
}
How can I wait for uth result (success/failure) and afer that get that value?
Where is FirebaseAuthenticationService from? Do you need it? The official getting started guide just uses Firebase.auth. With this, you can authenticate using the await() suspend function instead of using the callback approach.
// In a coroutine:
val authResult = Firebase.auth.registerWithEmailAndPassword(email, password).await()
val user: FirebaseUser = authResult.user
if (user != null) {
openHomeActivity.offer(Unit)
} else {
// authentication failed
}
If you are using coroutines you can use suspendCoroutine which is perfect bridge between traditional callbacks and coroutines as it gives you access to the Continuation<T> object, example with a convenience extension function for Task<R> objects :
scope.launch {
val registrationResult = suspendCoroutine { cont -> cont.suspendTask(FirebaseAuth.getInstance().createUserWithEmailAndPassword(email, password) }
}
private fun <R> Continuation<R>.suspendTask(task: Task<R>) {
task.addOnSuccessListener { this.success(it) }
.addOnFailureListener { this.failure(it) }
}
private fun <R> Continuation<R>.success(r : R) = resume(r)
private fun <R> Continuation<R>.failure(t : Exception) = resumeWithException(t)

callbackFlow not returning anything android kotlin

I want to implement firebase realtime database with coroutines, so I need to use flow because firebase just accept callbacks. the problem is the .collect{} block never gets executed
here is my code
#ExperimentalCoroutinesApi
override suspend fun getProduct(barcode: String): ProductItem? {
return withContext(Dispatchers.Default) {
println("Hi")
var item: ProductItem? = null
productFlow(barcode).collect {
//this never gets called
print("Getting product")
item = it
}
println("Ending product request ${item?.name}")
Log.i("GetProduct",item?.name)
item
}
}
#ExperimentalCoroutinesApi
private fun productFlow(barcode: String): Flow<ProductItem?> = callbackFlow {
val database = FirebaseDatabase.getInstance()
val productRef = database.getReference("products/$barcode")
val callback = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for(snapshot in dataSnapshot.children){
Log.i("Source", snapshot.value.toString())
}
val product = dataSnapshot.getValue(ProductItem::class.java)
Log.i("Source",product?.name) //everything is good until here
sendBlocking(dataSnapshot.getValue(ProductItem::class.java)) //after this i dont get anything on the collect{} block
}
override fun onCancelled(databaseError: DatabaseError) {
println("cancelling")
sendBlocking(null)
}
}
try {
productRef.addListenerForSingleValueEvent(callback)
} catch (e: FirebaseException) {
println("Firebase exception")
sendBlocking(null)
}
awaitClose{
println("Closing")
productRef.removeEventListener(callback)
}
}
First I would suggest to use the catch method to check if there is an error or not. Second, for callbackflow I remember using offer() instead of sendBlocking

RXjava2 method in fromCallable not getting exceuted

I am new to using rxjava and I am trying to run a function in background using rxjava2 but the method is not called the code I am using is given below let me know if its the right way to execute a function in background:
Observable.fromCallable<OrderItem>(Callable {
saveToDb(existingQty, newOty, product_id)
}).doOnSubscribe {
object : Observable<OrderItem>() {
override fun subscribeActual(emitter: Observer<in OrderItem>?) {
try {
val orderItem = saveToDb(existingQty, newOty, product_id)
emitter?.onNext(orderItem)
emitter?.onComplete()
} catch (e: Exception) {
emitter?.onError(e)
}
}
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).doOnSubscribe {
object : Observer<OrderItem> {
override fun onComplete() {
}
override fun onNext(t: OrderItem) {
}
override fun onError(e: Throwable) {
}
override fun onSubscribe(d: Disposable) {
}
}
}
You are dong it wrong way. doOnSubscribe() operator is called when observable is subscribed using subscribe() method and you haven't subscribed the observable using subscribe() method.
You have called saveToDb method in callable, then why are you calling it in doOnSubscribe? it doesn't make sense.
You should have written following code:
Observable.fromCallable { saveToDb(existingQty, newOty, product_id) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ orderItem ->
// set values to UI
}, { e ->
// handle exception if any
}, {
// on complete
})
to work with your logic.
DoOnSubscribe means "do when someone subscribe to it". But there is no subscribe in your code. Maybe you want to use subsribe instead of doOnSubscribe

Categories

Resources