Android Development

Issue Tracker Android Updates Android Home Home Contact

Coroutines in Android

Coroutines is modern way of asynchronous programming using Kotlin by offering a lightweight, efficient alternative to traditional multithreading tools like Thread, Handler, or ThreadPoolExecutor. They require much less manual thread management, suspending and resuming work without blocking underlying threads. They can enable thousands of concurrent operations with minimal overhead, while keeping code readable and maintainable. They can be well integrated with Android lifecycles.

With Coroutines, there is no callback hell and thread synchronization complexities. Their integration with android lifecycles prevents memeory leaks and provide much better error handling. Suspend functions allow coroutines to pause executing on their thread without blocking during a network call and later either resume on same or different thread.

Key Features of Coroutines

  • Lightweight Threading: Suspend and resume execution without blocking threads. Uses dispatchers (e.g., IO, Main) to switch threads efficiently.
  • Structured Concurrency: Run inside Android scopes like CoroutineScope,ViewModelScope, LifecycleScope to handle lifecycle automatically.

Coroutine Scopes in Android

  • GlobalScope: Application-wide lifetime. Rarely used - can cause memory leaks if not properly managed.
  • LifecycleScope: Bound to Activity/Fragment lifecycle - automatically cancels when destroyed.
  • ViewModelScope: Tied to ViewModel's lifecycle - perfect for repository operations and data processing.
  • CoroutineScope: For non-UI components (e.g., Repository, Domain Layer) - requires manual cancellation via job.cancel().

Dispatchers

  • Dispatchers.Main: UI operations on the main thread.
  • Dispatchers.IO: Network and disk I/O operations.
  • Dispatchers.Default: CPU-intensive operations.
  • Dispatchers.Unconfined: Starts in the current thread, but can resume elsewhere.

Coroutine Builders

  • launch: Launches a coroutine without returning a result.
  • async: Returns a result through Deferred and await().
  • runBlocking: Blocks the current thread (used for tests or console).

Suspend Functions

Suspend functions are declared with the suspend modifier and can only be called from within another coroutine or suspend function.


suspend fun fetchData(): String {
    delay(1000)
    return "Data from server"
}
---

Coroutines Launching and Synchronization

Kotlin Coroutines often started using launch and async, each serving different purposes.

Launch vs Async

  • launch is used when there is no need of a result. It runs a job that can be cancelled but does not return a value.
  • async is used when there is need of a result. It returns a Deferred object, and call await() to get the result later.

While launch and async can be used independently, they can also be combined when there is need to perform multiple operations in parallel and then synchronize them — for example, aggregating results before updating the UI.

Coordinating Multiple Coroutines

In many cases, there is need run multiple coroutines concurrently and wait for them to finish before proceeding, especially when updating the UI. This is typically done using async along with awaitAll, or by collecting results manually.


// Inside a ViewModel or lifecycle-aware scope
viewModelScope.launch {
    val deferred1 = async { fetchDataFromNetwork() }
    val deferred2 = async { loadDataFromDb() }

    val result1 = deferred1.await()
    val result2 = deferred2.await()

    updateUI(result1, result2)
}
---

In summary:

  • launch is for fire-and-forget tasks; async is for concurrent operations needing results.
  • Multiple coroutines can be synchronized using await() or awaitAll().
  • CoroutineWorker integrates coroutines into WorkManager for long-running background tasks.

Using Coroutines with WorkManager

Run coroutines inside a custom Worker by using CoroutineWorker from WorkManager. This allows you to run suspend functions in the background, even if the app is killed.


class MyCoroutineWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val success = performBackgroundTask()
        return if (success) Result.success() else Result.retry()
    }

    private suspend fun performBackgroundTask(): Boolean {
        // Example: fetch from network
        delay(1000)
        return true
    }
}

When enqueuing this worker, no need to manually manage threads or callbacks — coroutine context handles it efficiently.

Coroutine Error Handling

Coroutines provide multiple ways to handle exceptions, depending on where and how they are launched:

1. Try-Catch Inside Coroutine


lifecycleScope.launch {
    try {
        val result = fetchData()
        println(result)
    } catch (e: Exception) {
        println("Error: ${e.message}")
    }
}

2. Using CoroutineExceptionHandler


val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught exception: ${exception.message}")
}

lifecycleScope.launch(handler) {
    throw RuntimeException("Test error")
}

Note: This works only for launch coroutines, not async. Exceptions from async must be caught via await().

3. Handling async Exceptions


val deferred = lifecycleScope.async {
    throw IllegalArgumentException("Something went wrong")
}

lifecycleScope.launch {
    try {
        deferred.await()
    } catch (e: Exception) {
        println("Handled: ${e.message}")
    }
}

4. SupervisorScope (isolate failure)

Use supervisorScope to ensure that failure of one child coroutine doesn’t cancel the others:


lifecycleScope.launch {
    supervisorScope {
        launch {
            throw RuntimeException("Child failed")
        }

        launch {
            delay(1000)
            println("Other child continues")
        }
    }
}
---

Example: Coroutine in Android Activity


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycleScope.launch {
            try {
                val result = fetchData()
                // update UI
            } catch (e: Exception) {
                // show error to user
            }
        }
    }

    suspend fun fetchData(): String {
        delay(1500)
        return "Success"
    }
}
---

Quiz Questions

  • Are coroutines true threads managed by the OS?
  • What happens if an exception occurs in a coroutine launched using async?
  • How does supervisorScope differ from a regular coroutine scope?
  • What happens if you forget to handle exceptions in a coroutine?

For full coroutine usage in Android, see this example project: Coroutine Fetch JSON (GitHub)