Blog Infos
Author
Published
Topics
, , , ,
Published

Learn to love the Main thread by utilizing the official Coroutine conventions and best practices for Android developers.

Android Rabbit not hopping! by Copilot.

 

Misconceptions

The story begins, as all stories, through the things that we see. It can be during an interview for an Android position at the company where I work, or seen in an article on proandroiddev.com. The following statements are the ones that stood out in the recent months:

-We should run business logic off the main thread, in a background thread!

-Oh that looks like it makes a backend call, we should really switch to the IO dispatcher

-Using a LaunchedEffect is async, therefore it will not block the main thread

Or a classic example of doing work in a ViewModel as part of some example:

class MyViewModel(
  private val myRepository: MyRepository,
): ViewModel() {

  fun doWork() {
    viewModelScope.launch {
      val myData = withContext(Dispatchers.IO) {
        myRepository.getMyData()
      }
      // Do something with myData
    }
  }

}
Kotlin to the rescue!

As always, Kotlin has your back. It is the year 2025 and if you aren’t using Coroutines yet, I seriously advice you to drop everything and start implementing them. The quotes are basically misconceptions, created when modern technology meets old standards. In the past, we needed to be mindful of not blocking the main thread. But now there is Kotlin.

Coroutine Main thread conventions

When we understand Android coroutine threading conventions, we will directly understand why 3 out of 4 of these examples are bad in regards to when to switch threads (don’t worry, the 4th one about LaunchedEffect is briefly mentioned down below). The title of this article may already give the biggest clue. The trick is: if a library offers an API with Coroutines, you should assume that the library will internally handle any required threading! That is right, the convention is, and I quote:

“Suspend functions should be safe to call from the main thread”

Here is the link to the Coroutine Best Practices on the official Android site. This convention is extremely important to know, as it is very different from how it was before. In the past, it was mandatory to be mindful about threading while you were waiting for resources. You were not allowed to place network calls and you were not allowed to access a database.

The “good” old days

Let us have a look at the following old-school Retrofit code:

interface GitHubService {

  @GET("users/{user}/repos")
  fun listRepos(@Path("user") user: String): Call<List<Repo>>

}

class GithubNetworkSource(
  private val gitHubService: GitHubService = getGithubService(),
) {

  fun fetchUserRepos(user: String) =
    gitHubService.listRepos(user).execute().body() ?: emptyList()

}

As you can see: there is no suspending going on here, just a simple call to retrieve a list of repositories for a certain user on Retrofit. Let us call this code in our ViewModel:

class MainViewModel(
  private val githubNetworkSource: GithubNetworkSource = GithubNetworkSource(),
): ViewModel() {

  init {
    println(githubNetworkSource.fetchUserRepos("joost-klitsie"))
  }

}

If we run this, we are supposed to see the following exception:

android.os.NetworkOnMainThreadException

To fix this, we can do two things: We either switch threads, which we could do by creating a suspend function instead of a regular function and then switch context to Dispatchers.IO, OR we could simply use the Coroutines API that Retrofit supports out of the box! And yes, we are going with the second option, as you can see in the updated version:

interface GitHubService {

  @GET("users/{user}/repos")
  suspend fun listRepos(@Path("user") user: String): Response<List<Repo>>

}

You can see there is a suspend modifier added, and we changed the return type from Call<> to Response<>. This means, that we no longer have to execute the call ourselves, and we can directly deal with the response. I strongly urge you to take in mind that this is example code and it doesn’t catch any errors or do proper error handling, as this code is only meant to show how to tackle threading and Corouting Scopes. Obviously, now we need to launch a Coroutine to run this code. To avoid any confusion, we can have a look at the same guidelines page as earlier to decide how to do that:

“The ViewModel should create coroutines”

So knowing that, we will now add some boilerplate code and after that we can change our ViewModel to launch the actual work:

class GithubNetworkSource(
  private val gitHubService: GitHubService = getGithubService(),
) {

  suspend fun fetchUserRepos(user: String) =
    gitHubService.listRepos(user).body() ?: emptyList()

}

class GithubRepository(
  private val githubNetworkSource: GithubNetworkSource = GithubNetworkSource(),
) {

  suspend fun fetchUserRepos(user: String) =
    githubNetworkSource.fetchUserRepos(user)

}

class FetchRepositoriesForUserUseCase(
  private val githubRepository: GithubRepository = GithubRepository(),
) {

  suspend fun run(user: String) = githubRepository.fetchUserRepos(user)

}

class MainViewModel(
  private val fetchRepositoriesForUserUseCase: FetchRepositoriesForUserUseCase = FetchRepositoriesForUserUseCase(),
): ViewModel() {

  init {
    viewModelScope.launch {
      println(fetchRepositoriesForUserUseCase.run("joost-klitsie"))
    }
  }

}

And we should get the following response:

 

[
  Repo(id=322302851, name=compose-progress-button), 
  Repo(id=834030119, name=DataLoadingExample), 
  Repo(id=315288699, name=kotlin-styled-material-ui), 
  Repo(id=206812053, name=ScopedObserver), 
  Repo(id=857373384, name=Translatable)
]

 

You can see that we launched the Coroutine from the ViewModel. The ViewModel uses a Dispatchers.Main.immediate under the hood, so the code definitely runs on the Main thread. After that, the journey took us through a use case, a repository and finally a network source which invoked our Retrofit service. Nowhere did we need to switch from the Main thread, because we didn’t violate anything in Android’s StrictMode. Retrofit did all of the heavy lifting for us, yay! 🙂

Good libraries follow Main-thread safe suspend functions

Any proper library does and should follow the Main thread safe suspend function requirements! Luckily, the most used libraries like Room and Retrofit do this. So if you are an author of any library that uses Coroutines, you should definitely implement your API in this way. Also, this can help you to create conventions in any other project you work on.

So, when do we switch threads?

Never say never, but you should know the following:

It is perfectly fine to run the vast majority of your work on the Main thread

I am not making the argument that modern phones are fast enough: I am making the argument you are probably not doing heavy lifting to begin with. I’d say that for 99% of your business needs, your code will run fine on the Main thread! Imagine a case where you have a feature with 10 requirements: most likely you have a bunch of if/else or when statements, and a bunch of suspending calls to a network or database resource. Remember how you answered the question “what are Coroutines” on that job interview? It probably was something similar to “Coroutines are like light-weight threads or something”. The beauty is that when they are suspending, they hardly take any resources (especially compared to Threads). So having a suspending point, or literally 100,000 of them, on the Main thread while waiting for data to load is an operation with little overhead.

Switching threads is expensive

If you think about your work as being some serious heavy lifting, then of course you should go ahead and run it on a Dispatchers.Default . But this should be a conscious decision, for when you actually do heavy work like image or video processing. In all my current projects combined (note the plural), I can count on 1 hand when I felt like I had to actually switch threads to do work:

SharedPreferences

If you still use ye olde SharedPreferences, instead of the DataStore, know that this can be a disk-read and therefore block the main thread. This is (sadly) allowed by default, and it is good to be mindful of it by for example creating a Coroutine-friendly preferences wrapper that switches internally to the IO dispatcher. The particular issue we faced was that we wished to use (the now deprecated!) EncryptedSharedPreferences, which does not have any fancy coroutines API like the DataStore.

Logging

Did you know that logging also can block the thread? If you have are logging large backend responses, this could be an issue. A solution would be to accept the slowness in your debug builds, and remove logging from your production apps altogether! We opted for adjusting our logger to launch a Coroutine using the Dispatchers.IO on every log message, and we never had this lag bothering us again!

Combining legacy code with Coroutines

Obviously, when you combine legacy network calls with Coroutines and you couldn’t be bothered to migrate it properly, you will need to wrap your calls inside Dispatchers.IO.

Actually expensive calculations

On one project we have to check which markers are visible on a rotatable map. We get a map polygon and on every map movement, we calculate whether a markers Lat/Long is inside this visible polygon. As we have thousands of markers, and it runs often, we decided to off-load this task to another thread.

WWGD?

Google actually knows pretty well how to thread. Lets see what tools and libraries they offer and how threading is done:

Android Components

ApplicationsActivitiesFragmentsServices and BroadcastReceivers will by default run their work on the Main thread. For a BroadcastReceiver it is perhaps good to know that you can start asynchronous work on a different thread which is allowed to run a tad longer than the usual 5 seconds.

Jetpack ViewModel

ViewModel expects asynchronous code to be started from the viewModelScope. This uses the Dispatchers.Main.immediate under the hood.

Lifecycle

The lifecycle helper CoroutineScopes like the ProcessLifecycelOwner.lifecycleScopeActivity.lifecycleScopeFragment.lifecycleScopeFragment.viewLifecycleOwner.lifecycleScope , Compose’s LocalLifecycleOwner.current.lifecycleScope and even the lifecycle of a LifecycleService will run the code on the Main thread.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

No results found.

Jetpack Compose

Any code inside a LaunchedEffect orSideEffect runs on the Main thread. Any code launched using the rememberLifecycleScope also runs on the Main thread. It is good to know that these disptachers are more special and may only run at designated times during compositions.

WorkManager

If you run work using the WorkManager and you use a CoroutineWorker, your code will run inside the Dispatchers.Default. If you feel it is necessary, you can still change the context to a Dispatchers.IO if you want to make backend calls. You can see that WorkManager is optimised to run your work in the background.

Conclusion

Stick to the main thread for the bulk of your work and only switch threads as part of a very conscious decision, and not as part of a default way of working. Forget the past and embrace Coroutine friendly APIs! Coroutines makes it easier than ever to run your code, await data and propagate results. I hope this article helps to shed some light on modern day Android developing and threading, and shows how it changed during the last years.

If you like what you saw, put those digital hands together, and as always: let me know what you think in the comments! Joost out.

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
Menu