Coroutines in Android

Android Development — Architecture & Data
Course 2 · Chapter 6 · Coroutines in Android

⚙️ Coroutines in Android

viewModelScope has appeared in every chapter since Chapter 2, always used without much explanation of what it actually is or why it's safe to launch into. This chapter fills that gap — the Android-specific coroutine scopes, the repeatOnLifecycle pattern for safely collecting a Flow outside Compose, and where dispatchers actually matter in practice.

🔬 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:

class TaskViewModel(private val repository: TaskRepository) : ViewModel() { fun addTask(title: String) { viewModelScope.launch { repository.insert(Task(title = title)) } } // No manual cleanup needed — when this ViewModel is cleared, // every coroutine launched via viewModelScope is cancelled automatically. }

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):

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { // Cancelled automatically when this Activity is destroyed val data = repository.fetchSomething() // ... } } }

viewModelScope vs lifecycleScope

viewModelScopelifecycleScope
Owned byA ViewModelAn 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 useBusiness logic, data loadingUI-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:

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> // Update Views here — this block only runs while STARTED or above } } } }

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.

⚠ Compose Already Handles This For You

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:

suspend fun processLargeDataset(items: List<Item>): List<Result> { return withContext(Dispatchers.Default) { // Genuine CPU-bound work — sorting, filtering, heavy computation — // that would otherwise block whatever thread called this function items.map { expensiveTransform(it) } } }

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.

→ Solution

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.

→ Solution

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.

→ Solution

💡 A Recap Chapter, Made Concrete

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.