Android Kotlin

Android Firebase With Kotlin Coroutines And Flow API For Realtime Update

Google+ Pinterest LinkedIn Tumblr

Recently, I had to work with firebase in one of my Android apps. Being an Android developer with the kotlin programming language, I love to use kotlin-coroutines for all the asynchronous task handling that is easy to readable and maintainable. So, I want to use the same approach for all firebase SDK calls and luckily found out that there are a couple of extension functions wrote by the kotlin team for Google Play Service Task API.

Before showing you those extension functions let take a quick recap with the following example.

In an instance, I want to fetch a user from a collection of firebase database, we would have something similar to this, I’ll be using Firebase Firestore database for this.

fun fetchUser(id: String,listener: UserFetchListener) {
    FirebaseFirestore.getInstance()  // 1
           .document("users/$id")   // 2
           .get()   // 3
           .addOnSuccessListener { snapshots ->    //  4
                  val user = snapshots.toObjects(User::class.java)    // 5
                  listener.handleUser(user)  
            }.addOnFailureListener { e ->    //  6
                  listener.handleException(e)   
            }
}

In order to understand how the above code works let’s break it down in detail.

  1. Retrieving the instance of FirebaseFirestore.
  2. Gets a DocumentReference that refers to a document at the users/id path.
  3. Read the document referred by the users.
  4. Installing an OnSuccessListener and it’ll be called later when the result has been found.
  5. Convert the found DocumentSnapshot into the User object.
  6. Installing an OnFailureListener if the result gets canceled or fail with an exception.

Looking at the code above, you might think there’s nothing wrong with the listener because everything makes sense and even keeps the code organized.

Wait! before you make some conclusion in your mind that you’re going with the above method. Let’s just see the same example with coroutines.

First, create a new file with Task.kt name and add the following extension methods in that file.

Note: You need to name the file Task.kt else it’ll not work. JUST KIDDING!

/**
 * Converts this task to an instance of [Deferred].
 * If task is cancelled then resulting deferred will be cancelled as well.
 */
public fun <T> Task<T>.asDeferred(): Deferred<T> {
    if (isComplete) {
        val e = exception
        return if (e == null) {
            @Suppress("UNCHECKED_CAST")
            CompletableDeferred<T>().apply { if (isCanceled) cancel() else complete(result as T) }
        } else {
            CompletableDeferred<T>().apply { completeExceptionally(e) }
        }
    }

    val result = CompletableDeferred<T>()
    addOnCompleteListener {
        val e = it.exception
        if (e == null) {
            @Suppress("UNCHECKED_CAST")
            if (isCanceled) result.cancel() else result.complete(it.result as T)
        } else {
            result.completeExceptionally(e)
        }
    }
    return result
}

/**
 * Awaits for completion of the task without blocking a thread.
 *
 * This suspending function is cancellable.
 * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
 * stops waiting for the completion stage and immediately resumes with [CancellationException].
 */
public suspend fun <T> Task<T>.await(): T {
    // fast path
    if (isComplete) {
        val e = exception
        return if (e == null) {
            if (isCanceled) {
                throw CancellationException("Task $this was cancelled normally.")
            } else {
                @Suppress("UNCHECKED_CAST")
                result as T
            }
        } else {
            throw e
        }
    }

    return suspendCancellableCoroutine { cont ->
        addOnCompleteListener {
            val e = exception
            if (e == null) {
                @Suppress("UNCHECKED_CAST")
                if (isCanceled) cont.cancel() else cont.resume(result as T)
            } else {
                cont.resumeWithException(e)
            }
        }
    }
}

Now, if we used coroutines in our first example it would become:

suspend fun getUser(id: String) : User? {
    val snapshot = try {
          FirebaseFirestore.getInstance().document("user/$id").get().await()
    } catch(e: Exception) {
         null
    }
    return snapshot?.toObject(User::class.java)
}

The code now looks synchronous, with no callbacks, and easier to read, Right?

I wrote an article on how to authenticate a user with Google, Twitter, Facebook, and GitHub with firebase SDK using kotlin coroutines extension function which we just saw above.

Note: These extension functions are only working for a one-time request such as callbacks like addOnSuccessListener, addOnFailureListener, addOnCompleteListener, etc..

Realtime Updates For Firebase Database With Kotlin Coroutines & Flow API

Another problem which I faced while working on this app to listen to the realtime updates. Now we all know for realtime updates Firebase SDK has these ValueEventListener and ChildEventListener. There’s nothing wrong with these methods but like I said you before I kinda like coroutine style. So, how can we achieve this realtime update values with kotlin coroutines?

Well, for realtime updates in the firebase database we can happily use the kotlin flow API comes with kotlin coroutines.

If you didn’t know what Kotlin Flow API check out the following link.

Show me the code!

class UserObserver {
   
     private val scope = CoroutineScope(Dispatchers.IO)
     private firebaseDatabase = FirebaseDatabase.getInstance()   // 1
    
     @ExperimentalCoroutinesApi
     private val flow = callbackFlow<User> {   // 2
        val databaseReference = firebaseDatabase.getReference("users")    // 3
        val eventListener = databaseReference.addValueEventListener(object : ValueEventListener {

               override fun onCancelled(p0: DatabaseError?) {
                       [email protected](p0?.toException())   // 4
                }

               override fun onDataChange(p0: DataSnapshot?) {
                      p0?.let {
                            val user = p0.getValue(User::class.java)
                            [email protected](user)   // 5
                      }
                }
            })

            awaitClose {   // 6
                databaseReference.removeEventListener(eventListener)  
           }
    }

    fun observe() {
          scope.launch {  
               flow.collect { user ->   // 7
                    // handle user
               }
          }
    }
}

Let’s go back to the step-by-step breakdown.

  1. Gets the default FirebaseDatabase instance.
  2. Creating an instance of the cold flow using the callbackFlow function to convert a multi-shot callback into a flow.
  3. Retrieving the DatabaseReference from the database root node.
  4. Close the flow stream if an exception occurs while reading data.
  5. Sends the updated values of a user using the sendBlocking function.
  6. The awaitClose lambda will be called when the parent coroutine is canceled. After the invoking of this lambda, no code after this will be executed. So, here we simply call the removeEventListenr to detach any active listener from DatabaseReference.
  7. Collects the updated user from the flow.

Note: The code inside the callbackFlow{...} block is not called until this flow is collected by a caller. Also, the collect method is a suspended method so we need to call this from a suspend method or coroutine builder.

Now if you need to cancel the on-going flow operation you can simply cancel the parent coroutine and as a result, the underlying flow subscription will be canceled automatically.

fun stop() {
     scope.cancel()
}

And that’s all, I hope you find these kotlin extension function useful and they’ll improve your productivity when developing firebase apps with kotlin coroutines. If you like the post please share it with the community and don’t forget to hit the ♥️ button below.

Thank you for being here and keep reading…

Write A Comment