Converting Firebase API Callbacks to Kotlin Flows
Converting Firebase API Callbacks to Kotlin Flows

Photo by Michal Matlon on Unsplash

A short guide on when and why to use the callbackFlow builder function in Android.

Click on the handsome man below for the full code.

The key to understanding when callbackFlow is appropriate is to be able to recognise a “callback” in the first place.

Here are some “callbacks” that you use all the time:

setOnClickListener

addOnCompleteListener

addOnScrollListener

addUpdateListener

If you see “listener” in the code, chances are you are looking at a “callback”. callbackFlow is a nifty little way to convert these listeners into Kotlin Flows.

But why would you want to do this?

Well, sometimes you don’t! A good rule of thumb is to ask yourself: am I going to receive a bunch of things (multi-shot), or am I just after one thing (single-shot)?

Let's look at our use cases from above:

setOnClickListener — multi (can be clicked more than once)

— (can be clicked more than once) addOnCompleteListener — single (things should complete once)

— (things should complete once) addOnScrollListener — multi (we get a bunch of scroll events)

— (we get a bunch of scroll events) addUpdateListener — multi (whenever something updates, we get a ‘callback’)

We can use callbackFlow on all of these, but should we?

Button.clickFlow()

Let’s do something crazy. Let’s turn our everyday Button’s onClickListener callback into a Flow. Just for fun.

Note that we use the function offer instead of emit inside callbackFlow . In English, offer is a softer verb than emit — almost a question. An offer can be accepted… or not. This becomes important when dealing with backpressure, but for the purposes of this article, you can think of offer and emit as synonyms.

Moving on, let’s create a little extension function to collect our clicks:

Now we can hook this up in the collector like so:

buttonDownload.onClick { viewModel.download() }

… which is not much different from:

buttonDownload.setOnClickListener { viewModel.download() }

🤔 Hmmm, these look pretty similar!

So turning our button clicks into a Flow doesn’t make a whole lot of sense…unless, of course, we want to debounce!

😎

I’m being a bit facetious, of course. This kind of code is, in my experience, probably over-engineering things. However, the exercise itself is fun and provides a digestible way of exploring and understanding how callbackFlow works and what it is designed for. If you want to see some useful examples where coroutines are used to simplify View related code check out https://chris.banes.dev/suspending-views/ and https://github.com/JakeWharton/RxBinding for some inspiration.

Let’s take a look at a few “callbacks” from the Firebase SDKs and create something more useful.

Querying a Record from Firestore

Let’s say we just want to get a single record from your remote database:

CollectionReference.get()

.addOnCompleteListener { snapshot ->

// Do something with snapshot

}

Since we only want a single snapshot, a suspend function makes more sense in this case. Lucky for us, Google created a neat extension function ( fun suspend Task.await() ) that wraps another awesome function from the kotlin library ( suspendCancellableCoroutine ), which does exactly this.

You’ll need:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:0"

so you can do this:

import kotlinx.coroutines.tasks.await val snapshot = CollectionReference.get().await()

But you can’t just call a suspend function from anywhere, you’ll need some coroutine scope:

implementation “androidx.lifecycle:lifecycle-runtime-ktx:0”

This gives us lifecycleScope and viewModelScope extension properties which we can use to create coroutines from our Views or ViewModels respectively, and automatically connects the coroutine lifecycle to the View or Viewmodel lifecycle.

💡 In Android, the View or ViewModel is typically where our scopes should be launched and where our Flows should be collected. If you find yourself writing this all the time:

GlobalScope.launch { } // Naughty, naughty!

…you’re probably doing it wrong.

Back to the example:

Neat right? Simple, easy to read code and all without the use of a Flow.

📎 Don’t use Flow if a suspend function will do the job.

Watching for changes to a Firestore Database

When registering for changes to a certain part of a Firestore Database, we are expecting multiple “callbacks”, and so, we should find ourselves reaching for Flow, not a suspend function.

Not much different to our Button click Flow from above. Notice, however, how we just panic and bail if we get an error, and if we get a null or empty snapshot, we offer an emptyList ? This is poor programming and will almost certainly confuse the collector — even if you write the collector yourself! It’s better to propagate these signals to the collector. This makes your code more complex, yes, but also smarter. The client code can decide to do something different when an error or empty response is received: call another API instead, log an error, display an error, crash with an error, give the user a different option… whatever.

So, how can we encapsulate and propagate these signals to the caller/client code?

Let’s look at a different scenario and explore this idea.

Downloading a File from Firebase Storage

Downloading a file is a good mix of the above situations. It can take some time to download the file, but one way or another, it will complete. There are several ways that it can complete and there are several things we would like to know about during the download too. Firebase Storage has a nice set of “callbacks” to keep you informed about your download.

How do we convert all this to a Flow?

The first, second and third step is to think about how to model the information we are going to offer in the Flow, since a Flow must encapsulate a single type, let’s call it DownloadResult and model all the relevant data we need in the client code.

Here’s what I came up with:

📎 Agonise over the structures that you put into your Flows. Modelling your data efficiently is the key to writing clean functional code.

InProgress and Paused contain information that enables the collector to pause, resume and cancel the task, as well as determine the progress.

and contain information that enables the collector to pause, resume and cancel the task, as well as determine the progress. Success contains the File which the collector can then do with as they please.

contains the which the collector can then do with as they please. Failed contains the reason for the failure and allows the collector to react accordingly.

Now let’s have a look at how we would use it:

Note, this is a slimmed-down version of the real code to make it easier to digest the first time around, be sure to download the full repository for all the finer details.

We can now use a Redux-like pattern — whereby our UI state flows from the state property:

Bonus Material

Two observations about offer()

It is not a suspend function. It returns a Boolean.

When and how the underlying system (in this case Firebase) “calls back” is out of our control. This is why offer() is not a suspend function, it is designed to operate outside the callbackFlow coroutine scope (as is close() ).

offer ’s return value indicates whether it has succeeded in “offering” the value to the Flow or not. This is important in the Download File scenario because the final values offered just before the Flow is closed ( DownloadResult.Complete.XXX ), are very important. They tell the collector how the download completed — was it a success or a failure? However, the flood of DownloadResult.InProgress offerings could potentially clog the Flow and prevent the collector from receiving the last offering! Not good!

How do we get around this?

The good news is that you normally don’t have to worry about it. Flow covers the main use cases out-of-the-box and it’s only when you are doing “something fancy” that you’ll come across such an issue.

So, in the next article I’m going to do “something fancy” and dive deeper into this problem…
['Nick Skelton']
['AndroidDev', 'Kotlin', 'Software Development', 'Firebase', 'Kotlin Flow']