How to Make the Firebase Database SDK Work Better With Kotlin

In this article, we are going straight to the point. 🚀

I want to share some extension functions to make your experience with the Firebase Database a little more comfortable with Kotlin.

We are going to make reusable code that is also going to avoid the need for callbacks and take advantage of suspend functions.

Mapping operations

Here is a function to cast the value of every node in the database to whatever object you need. Thanks to the reified and inline keywords, you can make this transformation in one line with clear looking code.

// This would be called like so, snapshot.toDomain<String>()

inline fun <reified T> DataSnapshot.toDomain(): T? = getValue(T::class.java)

Read operations

For one-shot read operations, you can use the following function. In all the methods that you are going to see in this post, an exception is going to be thrown if the operation fails, so take that into account when using them.

suspend fun DatabaseReference.read(): DataSnapshot = suspendCoroutine { continuation ->
    val valueEventListener = object : ValueEventListener {
        override fun onCancelled(error: DatabaseError) {
            continuation.resumeWithException(error.toException())
        }

        override fun onDataChange(snapshot: DataSnapshot) {
            continuation.resume(snapshot)
        }
    }
    addListenerForSingleValueEvent(valueEventListener)
}

To subscribe to changes in the database, we have the following function that listen to node children, returns a Flow than be collected every time a node creation happens.

To make it a little more interesting, I made it so that you can pass it a mapper function instead of doing the mapping outside.

/* This function can be called like 
 * subscribeModifiedChildren(DataSnapshot::toBoolean)
 * where toBoolean is 
 * fun toBoolean() = toDomain<Boolean>() ?: false
 */

suspend fun <T> DatabaseReference.subscribeNewChildren(
    mapper: DataSnapshot.() -> T
): Flow<T> = callbackFlow {
    val childEventListener = object : ChildEventListener {
        override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
            trySend(snapshot.mapper())
        }

        override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {}

        override fun onChildRemoved(snapshot: DataSnapshot) {}

        override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}

        override fun onCancelled(error: DatabaseError) {}
    }
    addChildEventListener(childEventListener)
    awaitClose {
        removeEventListener(childEventListener)
    }
}

In this next method, we have the same idea as above, but now we are listening changes to a specific node.

@OptIn(InternalCoroutinesApi::class)
suspend fun <T> DatabaseReference.subscribeModifiedChildren(
    mapper: DataSnapshot.() -> T
): Flow<T> = callbackFlow {
    val valueEventListener = object : ValueEventListener {
        override fun onCancelled(error: DatabaseError) {
            handleCoroutineException(coroutineContext, error.toException())
        }

        override fun onDataChange(snapshot: DataSnapshot) {
            trySend(snapshot.mapper())
        }
    }
    addValueEventListener(valueEventListener)
    awaitClose {
        removeEventListener(valueEventListener)
    }
}

For queries, we do exactly the same as the read operation, but we extend from the Query object.

suspend fun Query.read(): DataSnapshot = suspendCoroutine { continuation ->
    val valueEventListener = object : ValueEventListener {
        override fun onCancelled(error: DatabaseError) {
            continuation.resumeWithException(error.toException())
        }

        override fun onDataChange(snapshot: DataSnapshot) {
            continuation.resume(snapshot)
        }
    }
    addListenerForSingleValueEvent(valueEventListener)
}

Write operations

The write operation uses the suspendCoroutine again, but now we called the setValue method provided by the Firebase SDK.

suspend fun <T> DatabaseReference.write(data: T) {
    suspendCoroutine { continuation ->
        setValue(data)
            .addOnSuccessListener {
                continuation.resume(Unit)
            }
            .addOnFailureListener {
                continuation.resumeWithException(it)
            }
    }
}

In case you want to get a node or create it if it doesn’t exist, you have the following functions. This is pretty useful, for example, if you need to add a new node when a user is trying to register in your app, but he did sign up before and didn’t remember.

suspend inline fun <reified T> DatabaseReference.getOrCreate(data: T): DataSnapshot =
    suspendCoroutine { continuation ->
        val transactionHandler = object : Transaction.Handler {
            override fun doTransaction(currentData: MutableData): Transaction.Result {
                return if (currentData.getValue<Any>() != null) {
                    Transaction.success(currentData)
                } else {
                    currentData.value = data
                    Transaction.success(currentData)
                }
            }

            override fun onComplete(
                error: DatabaseError?,
                committed: Boolean,
                currentData: DataSnapshot?
            ) {
                if (committed && currentData != null) {
                    continuation.resume(currentData)
                } else {
                    error?.let { throw error.toException() }
                }
            }
        }
        runTransaction(transactionHandler)
    }

Delete operations

To finish, here is a delete operation. We are going to follow the same technique used in the write function, so that we can know if the operation was completed successfully or if we need to manage the error.

suspend fun DatabaseReference.delete() {
    suspendCoroutine { continuation ->
        removeValue()
            .addOnSuccessListener {
                continuation.resume(Unit)
            }
            .addOnFailureListener {
                continuation.resumeWithException(it)
            }
    }
}

If you want to read more content like this, please go check out my profile.

Mansi Vaghela LinkedIn Twitter Github Youtube