Firebase Realtime Database and Kotlin Coroutines

Viewed 2460

I am a beginner and currently working on an android app in Kotlin that gets data from TMDb API and uses Firebase Realtime Database for storing and retrieving data.

I use Kotlin Coroutines to get data from TMDb API but I am not sure if I should use coroutines for storing or retrieving data from Firebase Realtime Database.

I am hoping that Firebase automatically does the work done by Kotlin coroutines.

Here is one of the Firebase operations that I want to perform : (retrieve object from database)

firebaseDatabase = Firebase.database
        dbReference = firebaseDatabase.getReference("users/$uid")

val dbListener = object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                // Get Post object and use the values to update the UI
                val fobject = dataSnapshot.getValue<TvFirebase>()
                Log.v("utk", "tv show name is " + fobject!!.name)
                // ...
            }

            override fun onCancelled(databaseError: DatabaseError) {
                // Getting Post failed, log a message
                Log.w("utk", "onCancelled", databaseError.toException())
                // ...
            }
        }

val tvReference = firebaseDatabase.getReference("users/$uid/tv/236")
            tvReference.addValueEventListener(dbListener)

So do I need to use Kotlin Coroutines with Firebase Realtime Database?

4 Answers

I'm adding some new features in my old project using both "Realtime database" and "Firestore".

Since firestore supports "await" suspend function on Coroutines I searched the same for Realtime database. Coroutines are great if you don't mix suspend and callback function..

End up to do it myself. I didn't try yet the code below but it should work.

Note: Some features are still marked as ExperimentalCoroutinesApi since 1.2.0, so be careful if it changes in futur releases.

    sealed class RealtimeDatabaseValueResult {
        class Success(val dataSnapshot: DataSnapshot): RealtimeDatabaseValueResult()
        class Error(val error: DatabaseError): RealtimeDatabaseValueResult()
    }

    /**
     * Perform a addListenerForSingleValueEvent call on a databaseReference in a suspend function way
     * @param onCancellation action to perform if there is a cancellation
     */
    @ExperimentalCoroutinesApi
    suspend fun DatabaseReference.awaitSingleValue(onCancellation: ((cause: Throwable) -> Unit)? = null) = suspendCancellableCoroutine<RealtimeDatabaseValueResult> { continuation ->

        val valueEventListener = object: ValueEventListener{
            override fun onCancelled(error: DatabaseError) {
                continuation.resume(RealtimeDatabaseValueResult.Error(error = error), onCancellation)
            }

            override fun onDataChange(snapshot: DataSnapshot) {
                continuation.resume(RealtimeDatabaseValueResult.Success(snapshot), onCancellation)
            }
        }

        // add listener like you normally do
        addListenerForSingleValueEvent(valueEventListener)

        // in case the job, coroutine, etc. is cancelled, we remove the current event listener
        continuation.invokeOnCancellation { removeEventListener(valueEventListener) }
    }

Usage :

 fun fetchUser(firebaseDatabase: FirebaseDatabase, userId: String){
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).launch{
            when(val result = firebaseDatabase.getReference("users").child(userId).awaitSingleValue()){
                is RealtimeDatabaseValueResult.Success -> {
                    val dataSnapshot: DataSnapshot = result.dataSnapshot
                    // proceed action with dataSnapshot

                }
                is RealtimeDatabaseValueResult.Error -> {
                    val error: DatabaseError = result.error
                    // proceed action with error
                }
            }
        }
    }

It's not required to use coroutines with any Firebase API, but that can certainly make your development easier, if you do it correctly. However, coroutines are not compatible with Firebase listeners that report changed values over time. For those, you might want to use a LiveData or Flow.

Just add a minor change to your code.

From this...

firebaseDatabase = Firebase.database
        dbReference = firebaseDatabase.getReference("users/$uid")

val dbListener = object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                // Get Post object and use the values to update the UI
                val fobject = dataSnapshot.getValue<TvFirebase>()
                Log.v("utk", "tv show name is " + fobject!!.name)
                // ...
            }

            override fun onCancelled(databaseError: DatabaseError) {
                // Getting Post failed, log a message
                Log.w("utk", "onCancelled", databaseError.toException())
                // ...
            }
        }

val tvReference = firebaseDatabase.getReference("users/$uid/tv/236")
            tvReference.addValueEventListener(dbListener)

To this...

    val firebaseDatabase = FirebaseDatabase.getInstance().getReference("users/$uid")
    
    firebaseDatabse.addChildEventListener(object: ChildEventListener {
    
    override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
    Log.d("MainFragment", "Children $snapshot")
    }
    override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {}
    override fun onChildRemoved(snapshot: DataSnapshot) {}
    override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
    override fun onCancelled(error: DatabaseError) {}
    })

Dont forget to check this if its correct man ;)

You can use callBackFlow API to remove all of firebase callbacks. Simple ex:

override suspend fun getSomething(): Flow<Long> = callbackFlow {
        val valueListener = object: ValueEventListener{

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

           override fun onDataChange(snapshot: DataSnapshot) {
              offer(snapshot.childrenCount)
            }
        }
        currentUserId?.let { userId ->
            firebaseRef?.child(userId)?.addValueEventListener(valueListener)
                    ?: offer(0)        }

        awaitClose {
            currentUserId?.let { userId ->
                firebaseRef?.child(userId)?.removeEventListener(valueListener)
            }
        }
    }
Related