If you’re focused on building a great mobile experience, there’s nothing more frustrating than running into an ANR.

ANRs, or Application Not Responding errors, occur when an app becomes unresponsive. This is technically due to excessive work on the main thread causing new work to take longer than five seconds to run, often freezing the UI in the process.

Google hates ANRs and will sink an app’s visibility on the Play Store if developers can’t keep them under control. Users aren’t any more tolerant of ANRs, as they lead to a poor user experience and, ultimately, app abandonment.

Avoiding ANRs and providing a better user experience is contingent on your ability to minimize main thread work in your Android app. The idea here is to avoid work on the main thread that could be done on other threads — after all, CPU time on the main thread is as precious a resource as a developer has.

So while things like UI updates can’t be offloaded, anything that can (without causing poor user experience) should. Ideally, things like network calls, CPU-intensive tasks like image encoding, or disk access, for example, should never be done on the main thread.

Below, our team of in-house engineers have pulled together four different tips to help you improve the performance of your app, provide a better user experience, and ultimately drive more engagement and revenue. So, let’s get started!

Tips for minimizing main thread work (with snippets!)

1. Use Kotlin Coroutines: Coroutines are the go-to technique for doing work on background threads in Kotlin while the app is running. It is built into the language and can be configured in a number of ways to suit your needs. For more information, refer to the official documentation.

// Create a coroutine scope to run jobs in a background thread
val myScope = CoroutineScope(Job() + Dispatchers.IO)

// Create a function that runs in a coroutine but does so in the main thread
suspend fun displayResult(result: String) = withContext(Dispatchers.Main) {
    // Update UI with result

fun runMe() {
    myScope.launch {
        // Do slow network read on background thread
        val result = doNetworkRead()
        // Send result back to main thread for consumption

2. Use WorkManager: WorkManager is great for background tasks that take a long time to complete, and that are perhaps running even when you are not actively using the app. It allows you to optimize when these tasks are triggered and under what conditions (e.g. you might want to require that a network connection exists and the device is charging, if your background task is downloading something big).

// set constraints of when the work manager job can run
val constraints = Constraints.Builder()

// create one time job that reads from the network and defined contraints
val networkReadWorker = OneTimeWorkRequestBuilder<NetworkReadWorker>()

val workManager = WorkManager.getInstance(context)

// enqueue to request to run

// observe the result of the job and display the result
    .observe(lifecycleOwner) { workInfo ->
        if (workInfo.state == WorkInfo.State.SUCCEEDED) {

3. Use RxJava: RxJava is a general purpose library that allows you to do “reactive” programming. Unlike imperative programming, reactive programming simplifies more complex systems by separating the parts of the program that change data from the parts that make updates due that change. With that in mind, you can use RxJava to update UI, as well as to run tasks on background threads, propagate updates, and manage work in a reactive way. And unlike coroutines, you can use this in Java code as well, as the name implies.

    .fromCallable {
        doNetworkRead() // read from network and produce emission
    .subscribeOn(Schedulers.io()) // produce emission on IO thread
    .observeOn(AndroidSchedulers.mainThread()) // run subscriber on main thread
    .subscribe { result ->

4. Use Executors directly: Before RxJava and Coroutines with all their bells and whistles, developers used threads directly to run arbitrary jobs and move work off the main thread of their Android apps. One of the simplest ways to do it is with Java Executors, and it’s still an effective technique to reach for today when optimizing for responsiveness. For example, you can create an Executor backed by a thread pool to run a set of long running jobs in the background.

val threadPoolExecutor = Executors.newFixedThreadPool(1)
val jobs: List<Runnable> = networkReadAndDisplayJobs()
// submit a list of jobs to run in this thread pool to read and display data
jobs.forEach {
    threadPoolExecutor.submit {

Build better Android experiences with intelligent ANR reporting

Minimizing the work on your main thread will help you build a more responsive — and therefore more engaging — Android app. But it won’t completely eliminate the risk of these user-churning errors.

While Google Play Console helps flag ANRs, they won’t always help you quickly find the root cause of the issue. In fact, even popular crash reporters struggle to get developers the information they need to quickly address an Application Not Responding error.

That’s why mobile developers that care about their Android user experience have been turning to Embrace. With Embrace, mobile developers can take advantage of unparalleled ANR reporting capabilities to get data on what is happening in their apps before an ANR is triggered.

Embrace gives you a high-fidelity view into any ANR, through sampled stack traces and flame graphs, making issue resolution faster and easier. Don’t struggle to reproduce ANRs locally – capture them in production instead.

Experience the difference of a more holistic view of ANRs, and eliminate them from your user experience with Embrace. Get started for free today.

A screenshot of Embrace’s sign-up screen next to text that reads "get started today and get 1 million free user sessions."