Coroutine patterns in Android, and why they work

Tom Colvin
ProAndroidDev
Published in
6 min readJan 21, 2024

--

I know many Android developers who learn coroutines through code patterns, and that is usually enough to get by. But doing so misses the beauty behind them — and the fact that, at the heart of it, it’s all quite simple really. So, what makes those patterns work?

Grab your toolkit, let’s prise open some common coroutine patterns you’ve probably seen a hundred times, and marvel at the clockwork behind.

And of course, if you’re new to all this then welcome! Here’s a good set of patterns that are really worth learning as an Android developer.

Pattern 1: The suspending function

Here’s how you make toast. You may already know this:

  1. Put bread in the toaster
  2. Wait around
  3. Take bread — now toast — from the toaster

Here’s a Kotlin version of this:

suspend fun makeToast() {
println("Putting bread in toaster")
delay(2000)
println("Bread is now toasted")
}

If you review your actions throughout this process, you’re mostly just hanging around, waiting for bread to become toast. Only a very small proportion of the time are you actually active.

So what do you do whilst you’re waiting? Well, anything you like. You can tick off another item on your to-do list. As long as you’re back in time to deal with your newly toasted bread once it’s ready, you’re good.

And that’s what a suspending function does. During the delay, your coroutine is said to be suspended, which flags to the coroutines library (specifically the dispatcher) that it can do something else.

So — and here’s the key part — when you call this suspend function, the underlying thread is not blocked. The coroutines library uses the delay efficiently, and the thread is put to work.

Of course, to the code calling the makeToast() function above, none of this detail matters. You call makeToast() and the function returns a bit later once the toast is ready. Whether it sat and waited around for the toast, or did other jobs, is irrelevant.

Pattern 2: Calling a suspending function from the main thread

This is why it’s often safe to call a suspend function from the main/UI thread. Given that it doesn’t block the calling thread, the calling thread is free to carry on doing UI things.

Here’s an example of this pattern. On click of a button, we reveal a PIN number for 10 seconds, then hide it again:

This is perfectly safe because it doesn’t block the UI thread. The UI will continue responding throughout the 10 second delay.

Pattern 3: Switching contexts

Many suspend functions spend most of their time suspended. A good example is grabbing data from the internet: it takes little effort to set up a connection, but waiting for the data to download takes most of the time.

So is it safe to perform suspended networking tasks on the UI thread? No! Not at all.

The calling thread is only unblocked for the length of time that the suspended task is actually suspended (i.e. waiting around).

Networking tasks involve all sorts of work outside of the waiting: setup, encryption, parsing responses, etc. They may only take milliseconds — but that’s milliseconds of time where the UI thread is blocked.

For performance reasons, you need your UI thread to be updating the UI constantly. Don’t interrupt it or your app’s performance will suffer.

So, that’s why we have the “switching contexts” pattern:

The withContext above ensures that this suspend function is run on an IO thread pool. With this in place, the saveNote function can be safely called from the UI thread.

As a general rule: ensure your suspend functions switch contexts when they need to, so that they can be called from the UI thread.

Pattern 4: Running coroutines in a scope

This isn’t so much a pattern, since all coroutines need a context in which to run.

But take the example pattern below. What does code like this really mean?

viewModelScope.launch {
// Do something
}

Let’s start with this simplified view: The scope of a coroutine represents the extent of its lifetime. (Actually there’s a bit more to it than that, and I will write more on this subject in a future article, but this is a good starting point).

So by saying viewModelScope.launch you are saying: launch a coroutine whose lifetime is limited by viewModelScope.

So “viewModelScope” here is like a bucket which holds coroutines for the View Model, including the one above. When the bucket is emptied — that is, when viewModelScope is cancelled — its contents will also be cancelled. Practically speaking, that means you can write code without worrying when it needs to be shut down.

Pattern 5: Multiple operations in a suspend function

We came across viewModelScope above. There are many others, for example:

  • rememberCoroutineScope() in Compose, which provides a scope that lasts as long as the @Composable is on the screen. (Pattern 1 above has an example of this)
  • viewLifecycleOwner.lifecycleScope in Android Views, which lasts as long as the Activity/Fragment
  • GlobalScope, which lasts forever (and so is usually, but not always, A Bad Idea™)

Or, you can create your own, like in this pattern:

Now why would you want to do that? Well, coroutineScope is a special function which creates a new coroutine scope and suspends until any/all child coroutines in it have completed.

So the pattern above means “do these things in parallel, then return when they’re all done”.

This is helpful in repository classes that have local and remote data sources, for example, because you often want to do something to both the data sources at the same time. The operation is only considered complete when both actions complete.

Pattern 6: Infinite loops (apparently)

Now that we understand coroutine scopes, we can see why a pattern like this actually works:

The while(true) — which would have been a massive red flag 5 years ago — is actually perfectly safe here. Once the viewModelScope is cancelled, the launched coroutine will be cancelled, and so the ‘infinite’ loop stops.

But the reason why it stops is quite interesting…

The call to delay() yields the thread to the coroutine dispatcher. That means it allows the coroutine dispatcher to check to see if anything else needs doing, and it can go and do it.

But it also means the coroutine dispatcher checks to see if the coroutine has been cancelled, and if so throws a CancellationException. You don’t need to handle this exception, but the result is that stack unwinds and the while(true) gets discarded.

Anti-pattern 1: A suspend function that doesn’t suspend

Giving way to the coroutine dispatcher is therefore essential. It’s perfectly safe to use libraries like Room, Retrofit and Coil, because they defer to the dispatcher when needed.

But this is why you shouldn’t ever write a coroutine that does this:

This takes an appreciable time to run. And once started it can’t be stopped.

A coroutine-safe version of the above would use the yield() function. yield() is a bit like running delay() without the actual delay: it yields to the dispatcher and will receive a CancellationException if it needs to stop.

Here’s a safe version of the above function:

So there we go. Six patterns using coroutines and one anti-pattern — and most importantly, why they work and what’s behind them.

In a future blog I’ll go into more depth into, for example, the difference between coroutine scope and context, what a Job is and what happens when you use launch. For now, though, ask any questions you have below!

Update 20 Feb 2024: the next article in the series is now available.

Tom Colvin has been architecting software for two decades and is particularly partial to working with Android. He’s co-founder of Apptaura, the mobile app specialists, and available on a consultancy basis.

--

--

Google Developer Expert in Android and CTO of Apptaura, the app development specialists. Available on consultancy basis. All articles 100% me, no AI.