Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to handle callback using kotlin coroutines

the following snippet returns the result as 'null' on sequential code flow. I understand coroutines could be a viable solution to handle the callback asynchronously.


    fun getUserProperty(path: String): String? {
        var result: String? = null
        database.child(KEY_USERS).child(getUid()).child(path)
            .addListenerForSingleValueEvent(object : ValueEventListener {
                override fun onCancelled(error: DatabaseError) {
                    Log.e(TAG, "error: $error")
                }

                override fun onDataChange(snapshot: DataSnapshot) {
                    Log.w(TAG, "value: ${snapshot.value}")
                    result = snapshot.value.toString()
                }
            })
        return result
    }

Can the coroutines be of any help in this scenario to wait until the result of the callbacks (onDataChange()/onCancelled())?

like image 434
Faisal Avatar asked Sep 07 '19 15:09

Faisal


People also ask

Why coroutines are better than RxJava?

The reason is coroutines makes it easier to write async code and operators just feels more natural to use. As a bonus, Flow operators are all kotlin Extension Functions, which means either you, or libraries, can easily add operators and they will not feel weird to use (in RxJava observable.

Can coroutines be suspended and resumed?

Coroutines can suspend themselves, and the dispatcher is responsible for resuming them. To specify where the coroutines should run, Kotlin provides three dispatchers that you can use: Dispatchers. Main - Use this dispatcher to run a coroutine on the main Android thread.

How do you communicate between two coroutines?

Channels form the foundational component for communicating between coroutines. A Channel implements both the SendChannel and ReceiveChannel interface.


2 Answers

Since the Firebase Realtime Database SDK doesn't provide any suspend functions, coroutines are not helpful when dealing with its APIs. You would need to convert the callback into a suspend function in order for you to be able to await the result in a coroutine.

Here's a suspend extension function that does this (I discovered a solution it by doing a google search):

suspend fun DatabaseReference.getValue(): DataSnapshot {
    return async(CommonPool) {
        suspendCoroutine<DataSnapshot> { continuation ->
            addListenerForSingleValueEvent(FValueEventListener(
                    onDataChange = { continuation.resume(it) },
                    onError = { continuation.resumeWithException(it.toException()) }
            ))
        }
    }.await()
}

class FValueEventListener(val onDataChange: (DataSnapshot) -> Unit, val onError: (DatabaseError) -> Unit) : ValueEventListener {
    override fun onDataChange(data: DataSnapshot) = onDataChange.invoke(data)
    override fun onCancelled(error: DatabaseError) = onError.invoke(error)
}

With this, you now how a getValue() suspect method on DatabaseReference that can be awaited in a coroutine.

like image 62
Doug Stevenson Avatar answered Oct 25 '22 07:10

Doug Stevenson


The @Doug example for singleValueEvent if you want to keep listing you can use coroutine flow like below:

@ExperimentalCoroutinesApi
inline fun <reified T> DatabaseReference.listen(): Flow<DataResult<T?>> =
  callbackFlow {
    val valueListener = object : ValueEventListener {
      override fun onCancelled(databaseError: DatabaseError) {
        close(databaseError.toException())
      }

      override fun onDataChange(dataSnapshot: DataSnapshot) {
        try {
          val value = dataSnapshot.getValue(T::class.java)
          offer(DataResult.Success(value))
        } catch (exp: Exception) {
          Timber.e(exp)
          if (!isClosedForSend) offer(DataResult.Error(exp))
        }
      }
    }
    addValueEventListener(valueListener)

    awaitClose { removeEventListener(valueListener) }
  }
like image 35
Omar Abdan Avatar answered Oct 25 '22 05:10

Omar Abdan