Irbisa · Cheatsheet

Kotlin Coroutines

Suspending functions, scopes, dispatchers, structured concurrency, cancellation, exception handling

Middle Developer · 17 questions

  1. 01

    What are coroutines, how do they differ from threads, and why are they called lightweight?

    A coroutine is a computation that can suspend: at a suspension point it releases its thread and resumes later, possibly on another thread.

    fun main() = runBlocking {  repeat(100_000) {           // 100k threads would run out of memory    launch {      delay(1_000)            // suspends: the thread serves other coroutines      print(".")    }…
  2. 02

    What do runBlocking and GlobalScope do, and why don't they belong in Android app code?

    runBlocking starts a coroutine and blocks the current thread until it finishes; GlobalScope launches coroutines with no parent that live as long as the process.

  3. 03

    What is the difference between launch and async, and how do they handle exceptions differently?

    launch starts fire-and-forget work and returns a Job; async computes a value and returns a Deferred<T> that you await().

    class ProfileVM(private val api: Api) : ViewModel() {  fun save(p: Profile) {    viewModelScope.launch { api.save(p) }          // Job, no result  }   fun load() = viewModelScope.launch {…
  4. 04

    What is structured concurrency, and why does it matter on Android?

    Structured concurrency means every coroutine starts inside a scope and becomes a child of that scope's Job: the parent does not complete until its children do, cancelling the parent cancels them, and a child's failure is reported to the parent.

  5. 05

    What is the difference between CoroutineScope and CoroutineContext, and which elements does a context hold?

    CoroutineContext is an indexed set of elements that configure a coroutine; CoroutineScope is an interface that holds one context and is the receiver of launch and async, so new coroutines inherit it.

  6. 06

    Which dispatcher do you pick for CPU work, blocking I/O and UI updates, and how do Dispatchers.Default and Dispatchers.IO differ?

    Dispatchers.Main for the UI, Dispatchers.Default for CPU-bound work (as many threads as cores), Dispatchers.IO for blocking calls (up to 64 threads by default).

    class ReportRepo(  private val dao: ReportDao,  private val io: CoroutineDispatcher = Dispatchers.IO,   // injectable for tests) {  private val writer = io.limitedParallelism(1)           // one writer at a time…
  7. 07

    What does the suspend keyword actually do, and what does the compiler turn a suspend function into?

    suspend marks a function that may pause without blocking its thread.

    // What you writesuspend fun showProfile(id: Long) {  val user = api.user(id)          // suspension point 1  val posts = api.posts(user.id)   // suspension point 2  render(user, posts)}…
  8. 08

    When do you use withContext and when async/await, and how do you run three independent network calls in parallel?

    withContext runs one block in another context and suspends until it returns, so it is sequential.

    suspend fun loadDashboard(): Dashboard = coroutineScope {  val user = async { api.user() }  val feed = async { api.feed() }  val badges = async { api.badges() }  Dashboard(user.await(), feed.await(), badges.await())   // all three overlap}…
  9. 09

    Will the try/catch in this code catch the exception, and how do you handle exceptions in coroutines correctly?

    fun load() {  try {    viewModelScope.launch {      val feed = api.feed()       // throws IOException      _state.value = FeedState.Ready(feed)    }  } catch (e: IOException) {    _state.value = FeedState.Error(e)  }}

    No. launch only schedules the coroutine and returns at once; the exception is thrown later, inside the coroutine, after the try block has exited.

  10. 10

    How does coroutine cancellation work, why is it cooperative, and why must you never swallow CancellationException?

    cancel() only marks the Job as cancelling; the code stops when it next reaches a suspension point such as delay or withContext, which checks the job and throws CancellationException.

    suspend fun hashAll(files: List<File>): List<String> = withContext(Dispatchers.Default) {  files.map { file ->    ensureActive()                     // CPU loop: stop here if cancelled    sha256(file)  }}…
  11. 11

    How do viewModelScope, lifecycleScope and repeatOnLifecycle differ, and why was launchWhenStarted deprecated?

    viewModelScope lives until onCleared(), lifecycleScope until the Activity or Fragment is destroyed, and repeatOnLifecycle(STARTED) starts its block at STARTED and cancels it at STOPPED.

    class ChatFragment : Fragment(R.layout.fragment_chat) {  private val vm: ChatViewModel by viewModels()   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {    viewLifecycleOwner.lifecycleScope.launch {      viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {…
  12. 12

    How do you wrap a callback-based API in a suspend function or a Flow?

    For one result use suspendCancellableCoroutine: start the call, resume the continuation from the callback, and cancel the underlying call in invokeOnCancellation.

    suspend fun Call.await(): Response = suspendCancellableCoroutine { cont ->  enqueue(object : Callback {    override fun onResponse(call: Call, response: Response) = cont.resume(response)    override fun onFailure(call: Call, e: IOException) = cont.resumeWithException(e)  })  cont.invokeOnCancellation { cancel() }       // cancel the HTTP call too…
  13. 13

    A thousand coroutines each increment the same counter a thousand times and the total comes out below a million - why, and how do you fix it without blocking a thread?

    counter++ is a read-modify-write, and Dispatchers.Default runs those coroutines on several threads at once, so updates overwrite each other.

    suspend fun broken(): Int {  var counter = 0  withContext(Dispatchers.Default) {    repeat(1_000) { launch { repeat(1_000) { counter++ } } }   // races: usually less than 1_000_000  }  return counter…
  14. 14

    Coroutines and Flow vs RxJava: what are the trade-offs, and how do their thread pools differ?

    Coroutines give sequential-looking code, cancellation tied to scopes and plain try/catch.

  15. 15

    What do SupervisorJob and supervisorScope change, and why does scope.launch(SupervisorJob()) { } break cancellation?

    Under a SupervisorJob or supervisorScope, a failing child does not cancel its parent or its siblings.

    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) // wrong: the passed Job becomes the parent, this is not a child of scopeval orphan = scope.launch(SupervisorJob()) { longRunningSync() }// scope.cancel() leaves it running; only orphan.cancel() stops it…
  16. 16

    api.promo() throws and promo.await() is never called. Does this code crash, and what happens to the feed?

    fun refresh() {  viewModelScope.launch {    val promo = async { api.promo() }   // throws IOException    val feed = api.feed()    _state.value = FeedState.Ready(feed)    // promo.await() is never called  }}

    Yes, it crashes.

  17. 17

    Find the bug: the first API call that needs a token hangs forever, and so does every call after it.

    class TokenProvider(private val auth: AuthApi) {  private val mutex = Mutex()  private var token: Token? = null   suspend fun validToken(): Token = mutex.withLock {    val current = token    if (current != null && !current.isExpired()) current else refresh()  }   suspend fun refresh(): Token = mutex.withLock {    auth.refresh().also { token = it }  }}

    validToken() holds the Mutex and calls refresh(), which tries to lock it again.