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

Time:04-12

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.

CodePudding user response:

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) {
    [email protected]("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.

CodePudding user response:

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")
        }
    }
}
  • Related