Coroutines in Android
⚙️ Coroutines in Android
🔬 viewModelScope — What It Actually Is
viewModelScope is a CoroutineScope (Kotlin Intermediate Chapter 1) automatically provided to every ViewModel, tied precisely to that ViewModel's own lifetime:
The ViewModel base class calls viewModelScope.cancel() internally inside its own onCleared() — a coroutine started with viewModelScope.launch { } gets structured-concurrency cancellation (Kotlin Intermediate Chapter 2) for free, tied to exactly the right lifetime: the ViewModel's, which (per Chapter 2 of this course) already survives rotation correctly.
🖼️ lifecycleScope — Tied to the Activity/Fragment Instead
Outside a ViewModel — directly in an Activity or Fragment, most often in code interoperating with the older View system from Course 1 — lifecycleScope plays the equivalent role, tied to that specific Activity/Fragment instance's lifecycle (Course 1, Chapters 2 and 6):
viewModelScope vs lifecycleScope
| viewModelScope | lifecycleScope | |
|---|---|---|
| Owned by | A ViewModel | An Activity or Fragment |
| Cancelled when... | ViewModel is cleared (screen genuinely gone) | The Activity/Fragment is destroyed |
| Survives rotation? | Yes (the ViewModel itself survives) | No — cancelled on every rotation, like the Activity itself |
| Typical use | Business logic, data loading | UI-adjacent, one-off work tied to a specific screen instance |
🔁 repeatOnLifecycle — Safely Collecting a Flow
Collecting a StateFlow naively inside lifecycleScope.launch { } keeps collecting even while the screen is fully backgrounded — wasted work, and potentially wasted battery/network. repeatOnLifecycle automatically starts and stops collection based on lifecycle state:
repeatOnLifecycle(Lifecycle.State.STARTED) automatically cancels the collection when the Activity drops below STARTED (backgrounded — Course 1, Chapter 2's onStop) and restarts it when it returns to STARTED or above — no manual pause/resume bookkeeping required.
Every Compose example so far in this course used collectAsStateWithLifecycle(), which does exactly this repeatOnLifecycle dance internally — it's why Chapter 2 through 5 never needed to think about this manually. repeatOnLifecycle matters directly when collecting a Flow inside a traditional Activity/Fragment (View system code, Course 1), which is genuinely the more manual and more error-prone path.
🧵 Dispatchers, In Practice
Room's suspend DAO functions (Chapter 3) and Retrofit's suspend API calls (Chapter 4) already run their actual I/O off the main thread automatically — the libraries handle their own dispatcher switching internally. Manually reaching for withContext(Dispatchers.IO) or Dispatchers.Default (Kotlin Intermediate Chapter 1) is mainly needed for work those libraries don't already cover:
Already Handled Automatically
Room queries, Retrofit calls, delay() — any library function that's genuinely suspend and does I/O has typically already dispatched itself correctly. Calling these from viewModelScope.launch { } with no dispatcher argument is already correct.
Needs Manual withContext
Your own CPU-heavy pure-Kotlin computation, or interop with a blocking Java/Android API that isn't itself suspend-aware — these need an explicit withContext(Dispatchers.Default) or Dispatchers.IO to avoid blocking the caller's thread.
💻 Coding Challenges
Challenge 1: viewModelScope Cleanup
Write a ViewModel with a function startPolling() that launches a viewModelScope coroutine looping with delay(2000) and logging a message each time. In a comment, explain precisely when this loop stops, and why no explicit cancel() call is needed anywhere in the ViewModel itself.
Goal: Reinforce that viewModelScope cancellation is automatic and tied to ViewModel lifetime, not something to hand-manage.
Challenge 2: repeatOnLifecycle in an Activity
Write MainActivity.onCreate collecting a ViewModel's uiState StateFlow using lifecycleScope.launch { repeatOnLifecycle(...) { ... } }, updating a plain (non-Compose) TextView's text with each new state's value. Explain in a comment what would happen (in terms of wasted work) if repeatOnLifecycle were removed and the Flow collected directly instead.
Goal: Practice the manual Flow-collection pattern needed outside Compose.
Challenge 3: Choosing a Dispatcher
Write a suspend function calculatePrimesUpTo(limit: Int): List<Int> that computes prime numbers (reusing Kotlin Fundamentals Chapter 8's isPrime() extension function idea) up to limit, wrapped in the correct withContext dispatcher for CPU-bound work. Add a comment explaining why this needs an explicit dispatcher while a Room/Retrofit call in this chapter's other examples didn't.
Goal: Practice recognizing genuinely CPU-bound work and dispatching it correctly.
Nothing in this chapter was a new coroutines concept — Kotlin Intermediate Chapters 1-2 already covered suspend functions, structured concurrency, and Dispatchers in full. What's new here is purely Android's specific scopes (viewModelScope, lifecycleScope) and the repeatOnLifecycle pattern, which only exist because Android screens have a lifecycle at all — a constraint that doesn't apply to Kotlin running anywhere else.
🎯 What's Next
Next chapter: DataStore & Preferences — replacing SharedPreferences, and the difference between proto and preferences DataStore.