Blog Infos
Author
Published
Topics
, ,
Published
Image by iconicbestiary

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:

@Composable
fun PlanetsScreen(...) {
val revealPIN by viewModel.isShowingPin.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
Column {
Button(
onClick = {
scope.launch {
// Here we call a function which takes at least 10 seconds to run,
// directly from the main thread. Safe because the thread isn't blocked.
viewModel.revealPinBriefly()
}
}
) {
Text("Reveal PIN")
}
if (revealPIN) {
Text(text = "Your PIN is 1234")
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub
val isShowingPin = MutableStateFlow(false)
// This function suspends the coroutine for a long time, but
// doesn't block the calling thread. So it can be called from
// the main/UI thread safely.
suspend fun revealPinBriefly() {
isShowingPin.value = true
delay(10_000)
isShowingPin.value = false
}
view raw MyViewModel.kt hosted with ❤ by GitHub

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:

suspend fun saveNote(note: Note) {
withContext(Dispatchers.IO) {
notesRemoteDataSource.saveNote(note)
}
}

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.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

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:

suspend fun deleteAllNotes() = withContext(...) {
// Create a scope. The suspend function will return when *all* the
// scope's child coroutines finish.
coroutineScope {
launch { remoteDataSource.deleteAllNotes() }
launch { localDataSource.deleteAllNotes() }
}
}

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:

fun flashTheLights() {
viewModelScope.launch {
// This seems like an unsafe infinite loop, but in fact
// it'll shut down when the viewModelScope is cancelled.
while(true) {
delay(1_000)
lightState = !lightState
}
}
}
view raw MyViewModel.kt hosted with ❤ by GitHub

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:

// !!!!! DON'T DO THIS !!!!!
suspend fun countToAHundredBillion_unsafe() {
var count = 0L
// This suspend fun won't be cancelled if the coroutine
// that's running it gets cancelled, because it doesn't
// ever yield.
while(count < 100_000_000_000) {
count++
}
}
view raw main.kt hosted with ❤ by GitHub

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:

suspend fun countToAHundredBillion() {
var count = 0L
while(count < 100_000_000_000) {
count++
// Every 10,000 we yield to the coroutine
// dispatcher, allowing this loop to be
// cancelled if needed.
if (count % 10_000 == 0) {
yield()
}
}
}
view raw main.kt hosted with ❤ by GitHub

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!

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.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu