Suspending Firebase Realtime Database with Kotlin Coroutines

Kaustubh Patange
ProAndroidDev
Published in
4 min readJan 3, 2021

--

Photo by Marc Reichelt on Unsplash

This guide is a short explanation on simplifying the use of Firebase real-time database in Kotlin via Coroutines. As far as you know, for every database operation, there is an associated callback that will notify you about the success or failure of the operation.

What I really don’t love about callbacks is they break the tendency of executing code in a uniform order. With Java, we can use RxJava but it would still add some boilerplate code & makes the code kind of messy to read. With Kotlin, we have Coroutines which executes code asynchronously in a lightweight thread without blocking the UI following a convenient paradigm. Personally, I feel this approach is quite good as it makes the code easy to read.

Ok, enough talking let’s dive into it but before we do I assume you have at least a basic knowledge of Coroutines & how they are used in Android.

Also, all of the code I’ve written & explained is packed into a library ready to use in your project.

Suspending the operations

There are a couple of terms you need to get familiar with (or you already had… Skip),

  • Dispatchers: It determines the thread on which the coroutine should be executed.
  • CoroutineContext: It determines the context in which the coroutine is executed.
  • CoroutineScope: It determines the scope in which the coroutine is launched. Each scope is backed by the CoroutineContext & is responsible for the sustainability of the jobs. If the scope is canceled all of its child jobs are also canceled.

So let’s see how a callback listener is converted into a suspending function (by suspending I mean they can be used in a coroutine). For example a SingleValueEvent operation on a DatabaseReference should look like this.

database.getReference("ref").addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
...
}

override fun onCancelled(error: DatabaseError) {
...
}
})

From this, we can understand that the listener calls one of these two states OnDataChange, onCancelled . Hence, we need a holder for these state changes.

Sealed Classes

A sealed class is basically an extension over an enum class. We can define the states as classes & parameters associated with that state which will be covariantly referred to as the parent class.

For example, we can create a state holder for the above problem as below,

sealed class EventResponse {
data class Changed(val snapshot: DataSnapshot): ValueEventResponse()
data class Cancelled(val error: DatabaseError): ValueEventResponse()
}

SuspendableCoroutine

Ok, now what about the actual callback? Since the example I took is a one-shot event for completion or failure, we will use suspendableCoroutine .

Here I’ve created an extension function on DatabaseReference . Inside the suspendCoroutine we have a continuation block where all the code is executed on a different thread. It suspends the current thread until the execution is complete & transfers back the control flow to the calling thread by using Continuation.resume along with the information passed as the parameter.

We can then use it in a suspend function or in a CoroutineScope we want.

This was cool, but how about streaming of data something like ValueEventListener or ChildEventListener . Now the problem with them is they are not one-shot events, they will continue to emit new values as long as they are subscribed to the callbacks. In this case, we can use Flow.

CallbackFlow

A flow is an asynchronous data stream which emits values that can be collected by a consumer when launched in a CoroutineScope.

We are going to construct a flow using a callbackFlow which will keep emitting the values unless they are canceled or an exception is thrown.

The following example demonstrates ValueEventListener but the same can be modified to work on ChildEventListener .

Similar to Continuation.resume we use sendBlocking method to synchronously emit the data by blocking the caller.

Now to listen for the changes i.e collecting the results we can do something like below,

A SupervisorJob is similar to a regular job the only difference is canceling will affect its child jobs (in this case ValueEventListener ). Hence at the last, you will see we have job.cancel() which will close the flow for collecting, resulting it to call the method in awaitClose (from the callback flow) i.e removeEventListener . Similar to suspendCoroutine, callbackFlow more likely behaves the same everything inside the block runs asynchronously.

Conclusion

Using the powerful features of Kotlin we have made our job easier. You can apply these concepts to any callback listeners you came across in Android or in a 3rd party library. It’s all about simplifying the task & that's one of the many things I liked about Kotlin is its expressive nature.

All of this code is available in my Github repository in a form of a library so you can readily use them in your application.

--

--