Kotlin Coroutines in Android: Understanding suspend functions and main-safety by example

What are suspend functions and how to use them to achieve background work, and main safety.

Benny Pivnic
5 min readApr 20, 2020

When I have started learning about coroutines in Android in order to replace my executors and callbacks, I had a learning curve about how to actually use the coroutines, what runs on the main thread and what actually runs in a background thread. In this post I will demonstrate by example how to use coroutines to do some heavy calculations on a background thread using coroutines and providing main-safety.

Resources:

The first rule of Android development: Don’t block the main thread!

Starting with coroutines one might think that adding a function the keyword suspend will run the function in a background thread. This is not the case, a suspend function unless stated otherwise, will run on the main thread. What is a suspend function then?

From code lab:

The keyword suspend is Kotlin’s way of marking a function, or function type, available to coroutines. When a coroutine calls a function marked suspend, instead of blocking until that function returns like a normal function call, it suspends execution until the result is ready then it resumes where it left off with the result. While it’s suspended waiting for a result, it unblocks the thread that it’s running on so other functions or coroutines can run.

Let’s make some order in that paragraph:

  • A suspend function will be called from a coroutine
  • In case the function is a long running task (blocking), it will suspend and let the calling thread resume
  • When the blocking function is suspended, other functions on the calling thread can run
  • When the result of the blocking function is ready then it resumes

It all sounds nice, however some clarifications are needed:

  • Declaring a function suspend does not do anything more then it could do if the function was not suspending.
  • This means adding suspend to a blocking function called from the main thread, will still run on the main thread
  • Thus the blocking function should be run explicitly on a background thread

How do we solve it then?

Let’s Consider the following example:

  • MainActivity with two TextViews
  • First textView will display a calculation of a large number (result from blocking function)
  • The second will display “Hello World!”
  • MainViewModel with a function countToOneBillion() that will count to one billion (1,000,000,000) and set the number in MainActivity
  • If countToOneBillion() is blocking, “Hello World!” won’t be displayed until countToOneBillion() finishes
notice the second TextView with text: “Hello World!”
The activity updating the text view, Hello World! is default text and won’t be changed

the view model:

In this example countToOneBillion() is blocking and will block “Hello World!” from displaying

Test case 1:
Wrap countToOneBillion() in a coroutine with viewModelScope.launch
We will run the blocking function in the view model scope

Result: “Hello World!” isn’t displayed before the calculation is complete, thus countToOneBillion() is still blocking

but why isn’t it enough? from android developer site:

viewModelScope.launch will start a coroutine in the viewModelScope. This means when the job that we passed to viewModelScope gets canceled, all coroutines in this job/scope will be cancelled. If the user left the Activity before delay returned, this coroutine will automatically be cancelled when onCleared is called upon destruction of the ViewModel.

Since viewModelScope has a default dispatcher of Dispatchers.Main, this coroutine will be launched in the main thread.

This means countToOneBillion() is still blocking, and just running a code in a coroutine or launching the function in the correct scope (viewModelScope) doesn’t run it in the background. countToOneBillion() still runs on the main thread, since it is the default dispatcher which is Dispatchers.Main.

Test case 2:
Add suspend to countToOneBillion(). For this example let’s wrap the calculation with the function: countToOneBillionInternal(), for better readability and for adding the suspend keyword (suspend function can be called from a coroutine scope or from another suspend function)

Result:

  • “Hello World!” isn’t displayed before the calculation is complete, thus countToOneBillion() is still blocking
  • Android studio will grey out the suspend keyword stating this is redundant
  • This means adding suspend doesn’t do anything to the blocking function

Why is it still blocking?
Running a suspend function, unless specified otherwise will run on Dispatcher.Main which means it will run on main thread, if you want to achieve background work, you will HAVE to specify which Dispatcher should the function run on.

Dispatchers.Main — Use this dispatcher to run a coroutine on the main Android thread. This should be used only for interacting with the UI and performing quick work. Examples include calling suspend functions, running Android UI framework operations, and updating LiveData objects.

Dispatchers.IO — This dispatcher is optimized to perform disk or network I/O outside of the main thread. Examples include using the Room component, reading from or writing to files, and running any network operations.

Dispatchers.Default — This dispatcher is optimized to perform CPU-intensive work outside of the main thread. Example use cases include sorting a list and parsing JSON.

This means we will need to run our function in Dispatchers.Default since we are performing a heavy calculation and not an IO operation. For this we will use the function withContext().

Test case 3:

add withContext(Dispatechers.Default) to countToOneBillion()

Result:

  • MainActivity will display “0” for the count TextView (default value)
  • MainActivity will display “Hello world!” for the second textView (Nothing is blocking the main thread)
  • When countToOneBillion() finishes, it will set the correct value to main thread
  • We have achieved main-safety

Conclusion

  • Suspend functions will run by default on main thread
  • In order to achieve background work and main-safety, it is needed to run blocking functions on a correct dispatcher
  • Available dispatchers for background work are Dispatcher.IO (optimized for IO) and Dispatcher.Default (for long calculations)
  • Some third party libraries (room, retrofit) come with main safety build in and it is enough to use the suspend keyword when using them
  • In order to achieve main-safety in your project, you have to take care of the background work by yourself

Extras: gradle configuration

// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

--

--

Benny Pivnic

Android Tech lead, Researcher, Innovation enthusiastic. Interested in mobile product development and love great UX.