1. Introduction to Coroutines 

Coroutines are Kotlin's solution for asynchronous programming. They allow you to write asynchronous code that looks and feels like synchronous code, making it easier to read and maintain.

Key Benefits:

  • Lightweight: Thousands of coroutines can run on a few threads
  • Non-blocking: Don't block threads while waiting for operations
  • Sequential: Write async code in a sequential manner
  • Structured Concurrency: Automatic cleanup and cancellation

Basic Example:

import kotlinx.coroutines.* fun main() = runBlocking { println("Start") delay(1000) // Non-blocking delay println("End") }

2. Core Concepts

2.1 Suspend Functions

Functions marked with

suspend
can be paused and resumed without blocking threads.

suspend fun fetchUser(): String { delay(1000) // Simulates network call return "User123" } suspend fun fetchOrders(): List<String> { delay(500) return listOf("Order1", "Order2") }

2.2 Coroutine Context

Every coroutine has a context that includes:

  • Job: Controls lifecycle
  • Dispatcher: Determines which thread(s) to use
  • Exception Handler: Handles uncaught exceptions
fun main() = runBlocking { val job = launch(Dispatchers.IO + CoroutineName("MyCoroutine")) { println("Running on: ${Thread.currentThread().name}") } job.join() }

3. Coroutine Builders

3.1 launch - Fire and Forget

Returns a Job for fire-and-forget operations.

fun main() = runBlocking { val job = launch { repeat(3) { println("Task $it") delay(500) } } println("Main continues...") job.join() // Wait for completion }

3.2 async - Concurrent Execution

Returns >Deferred<T> for operations that return results.

fun main() = runBlocking { val userDeferred = async { fetchUser() } val ordersDeferred = async { fetchOrders() } // Both operations run concurrently val user = userDeferred.await() val orders = ordersDeferred.await() println("User: $user, Orders: $orders") }

3.3 runBlocking - Bridge to Blocking World

Blocks the current thread until completion. Use sparingly.

fun main() = runBlocking { println("Start of main") delay(1000) println("End of main") } // In tests @Test fun testCoroutine() = runBlocking { val result = async { computeResult() } assertEquals("expected", result.await()) }

3.4 withContext - Context Switching

Switch execution context without creating new coroutines.

suspend fun loadData(): String = withContext(Dispatchers.IO) { // This runs on IO thread println("Loading on: ${Thread.currentThread().name}") delay(1000) "Data loaded" } fun main() = runBlocking { println("Main on: ${Thread.currentThread().name}") val data = loadData() println("Back to: ${Thread.currentThread().name}") println(data) }

4. Coroutine Scopes

4.1 GlobalScope

Lives for the entire application lifetime. Use carefully.

fun startBackgroundWork() { GlobalScope.launch { while (true) { performBackgroundTask() delay(60000) // Every minute } } }

4.2 Custom Scope

Create your own scope for better control.

class DataRepository { private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.IO + job) fun fetchDataAsync() { scope.launch { val data = networkCall() processData(data) } } fun cleanup() { job.cancel() // Cancels all child coroutines } }

4.3 Structured Concurrency

Parent-child relationships ensure proper cleanup.

fun main() = runBlocking { println("Parent starts") launch { println("Child 1 starts") delay(1000) println("Child 1 ends") } launch { println("Child 2 starts") delay(500) println("Child 2 ends") } println("Parent waits for children") }

5. Dispatchers

5.1 Types of Dispatchers

fun main() = runBlocking { // Default - CPU intensive work launch(Dispatchers.Default) { println("Default: ${Thread.currentThread().name}") // Heavy computation } // IO - Network/disk operations launch(Dispatchers.IO) { println("IO: ${Thread.currentThread().name}") // File operations, network calls } // Main - UI operations (Android) // launch(Dispatchers.Main) { ... } // Unconfined - Inherits from caller launch(Dispatchers.Unconfined) { println("Unconfined: ${Thread.currentThread().name}") } }

5.2 Custom Dispatcher

val customDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() fun main() = runBlocking { launch(customDispatcher) { println("Custom: ${Thread.currentThread().name}") } }

6. Exception Handling

6.1 Try-Catch in Suspend Functions

suspend fun riskyOperation(): String { return try { delay(1000) if (Random.nextBoolean()) { throw Exception("Something went wrong") } "Success" } catch (e: Exception) { "Error: ${e.message}" } }

6.2 CoroutineExceptionHandler

val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception") } fun main() = runBlocking { val job = launch(handler) { throw Exception("Coroutine failed") } job.join() println("Main continues") }

6.3 SupervisorJob

Prevents child failures from cancelling siblings.

fun main() = runBlocking { val supervisor = SupervisorJob() with(CoroutineScope(coroutineContext + supervisor)) { val child1 = launch { delay(100) throw Exception("Child 1 failed") } val child2 = launch { delay(200) println("Child 2 completed") } joinAll(child1, child2) } }

7. Advanced Topics

7.1 Channels - Communication Between Coroutines

fun main() = runBlocking { val channel = Channel<Int>() // Producer launch { for (x in 1..5) { channel.send(x * x) delay(100) } channel.close() } // Consumer for (y in channel) { println("Received: $y") } }

7.2 Flow - Asynchronous Streams

fun numbers(): Flow<Int> = flow { for (i in 1..3) { delay(1000) emit(i) } } fun main() = runBlocking { numbers() .map { it * it } .collect { println("Collected: $it") } }

7.3 StateFlow and SharedFlow

class Repository { private val _state = MutableStateFlow("Initial") val state: StateFlow<String> = _state fun updateState(newState: String) { _state.value = newState } } fun main() = runBlocking { val repo = Repository() launch { repo.state.collect { println("State: $it") } } delay(100) repo.updateState("Updated") delay(100) }

7.4 Select Expression

suspend fun selectExample() { val channel1 = Channel<String>() val channel2 = Channel<String>() launch { delay(100) channel1.send("from channel1") } launch { delay(200) channel2.send("from channel2") } val result = select<String> { channel1.onReceive { it } channel2.onReceive { it } } println("Selected: $result") }

8. Android-Specific Usage

8.1 ViewModel with Coroutines

class UserViewModel(private val repository: UserRepository) : ViewModel() { private val _users = MutableLiveData<List<User>>() val users: LiveData<List<User>> = _users fun loadUsers() { viewModelScope.launch { try { val userList = repository.getUsers() _users.value = userList } catch (e: Exception) { // Handle error } } } }

8.2 Activity/Fragment Lifecycle

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> updateUI(state) } } } } }

8.3 Repository Pattern

class UserRepository(private val apiService: ApiService) { suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) { apiService.fetchUsers() } suspend fun createUser(user: User): User = withContext(Dispatchers.IO) { apiService.createUser(user) } }

9. Best Practices

9.1 Do's and Don'ts

DO:

  • Use structured concurrency
  • Choose appropriate dispatchers
  • Handle exceptions properly
  • Use appropriate scopes (lifecycleScope, viewModelScope)
  • Cancel coroutines when not needed

DON'T:

  • Use GlobalScope in UI code
  • Block main thread with runBlocking
  • Ignore exception handling
  • Create too many coroutines
  • Forget to handle cancellation

9.2 Performance Tips

// Good: Concurrent execution suspend fun loadDataConcurrently() { val user = async { fetchUser() } val orders = async { fetchOrders() } val profile = async { fetchProfile() } updateUI(user.await(), orders.await(), profile.await()) } // Bad: Sequential execution suspend fun loadDataSequentially() { val user = fetchUser() val orders = fetchOrders() val profile = fetchProfile() updateUI(user, orders, profile) }

10. Interview Questions

Basic Level


Q1: What are coroutines in Kotlin? A: Coroutines are lightweight threads that allow writing asynchronous code in a sequential manner. They can suspend and resume without blocking threads, making them more efficient than traditional threads.


Q2: What's the difference between launch and async? A:

  • launch
    returns a Job and is used for fire-and-forget operations
  • async
    returns Deferred<T> and is used when you need a result from the coroutine

Q3: What is a suspend function? A: A suspend function is a function marked with the suspend keyword that can be paused and resumed. It can only be called from other suspend functions or coroutines.

Intermediate Level


Q4: Explain different types of Dispatchers. A:

  • Dispatchers.Main
    : UI thread (Android)
  • Dispatchers.IO
    : I/O operations (network, file)
  • Dispatchers.Default
    : CPU-intensive work
  • Dispatchers.Unconfined
    : Inherits caller's context

Q5: What is structured concurrency? A: Structured concurrency ensures that coroutines follow a parent-child relationship where parent coroutines wait for their children to complete and can cancel all children when cancelled.


Q6: How do you handle exceptions in coroutines? A: Using try-catch blocks, CoroutineExceptionHandler, or SupervisorJob to prevent child failures from affecting siblings.

Advanced Level


Q7: What's the difference between Flow and Channel? A:

  • Flow: Cold stream, declarative, lazy evaluation
  • Channel: Hot stream, imperative, immediate execution

Q8: Explain the difference between SupervisorJob and regular Job. A: SupervisorJob allows child coroutines to fail independently without cancelling siblings, while regular Job cancels all children if one fails.


Q9: How would you implement timeout in coroutines?

suspend fun fetchWithTimeout(): String { return withTimeout(5000) { // 5 seconds fetchDataFromNetwork() } }

Q10: What are the best practices for using coroutines in Android? A:

  • Use lifecycleScope for UI components
  • Use viewModelScope for ViewModels
  • Avoid GlobalScope in UI code
  • Handle cancellation properly
  • Use appropriate dispatchers
  • Don't block main thread with runBlocking

Practical Scenarios


Q11: How would you make multiple network calls concurrently?

suspend fun loadUserData(userId: String): UserData = coroutineScope { val user = async { userService.getUser(userId) } val posts = async { postService.getUserPosts(userId) } val friends = async { friendService.getUserFriends(userId) } UserData(user.await(), posts.await(), friends.await()) }

Q12: How do you cancel a coroutine?

val job = launch { try { repeat(1000) { i -> if (!isActive) return@launch // Check cancellation delay(100) println("Working $i") } } finally { println("Cleaning up...") } } delay(500) job.cancel() // Cancel the coroutine

Q13: Implement a retry mechanism with coroutines.

suspend fun <T> retry( times: Int, delay: Long = 1000, block: suspend () -> T ): T { repeat(times - 1) { try { return block() } catch (e: Exception) { delay(delay) } } return block() // Last attempt } // Usage val result = retry(3, 2000) { fetchDataFromNetwork() }

Common Mistakes to Avoid

  1. Using runBlocking in Android UI code
  2. Not handling exceptions in coroutines
  3. Using GlobalScope for UI-related operations
  4. Creating too many coroutines unnecessarily
  5. Not understanding cancellation and cleanup
  6. Mixing blocking and non-blocking code incorrectly

This comprehensive tutorial covers all aspects of Kotlin coroutines from basics to advanced concepts, providing practical examples and common interview questions to help you master asynchronous programming in Kotlin.

Related Tutorials & Resources