CS 3180 Mobile Application Development

Week 10: Background Tasks & Coroutines

CS 3180 — Mobile Application Development

Kotlin Coroutines · Dispatchers · viewModelScope · Flow

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Why Async Programming?

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Android's Main Thread Rule

The UI thread must stay responsive at all times

  • Render a frame every 16ms (60 fps)
  • Handle touch events immediately
  • Update the interface on user input

Any slow operation on the main thread breaks this

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

What Counts as "Slow"?

  • Network requests (100ms – 5+ seconds)
  • Database queries (10ms – 1+ second for large reads)
  • File I/O (variable, can be hundreds of ms)
  • Image decoding (50ms+)
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

What Counts as "Slow"?

  • Network requests (100ms – 5+ seconds)
  • Database queries (10ms – 1+ second for large reads)
  • File I/O (variable, can be hundreds of ms)
  • Image decoding (50ms+)

→ These must happen on a background thread

→ Results must come back to the main thread to update UI

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Kotlin Coroutines

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

What is a Coroutine?

A suspendable computation — it can pause without blocking its thread

// Looks synchronous:
val data = fetchFromNetwork()   // suspend point — thread is free here
processData(data)               // resumes after fetch completes

The thread is released while waiting. Other work can use it.

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Coroutines vs Threads

Threads Coroutines
Memory ~1 MB each Kilobytes
Creation cost Expensive (OS) Cheap
Max practical count Hundreds Thousands+
Cancellation Complex Built-in
Code style Callbacks Sequential
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Coroutine Builders

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

launch — Fire and Forget

viewModelScope.launch {
    val scores = repository.getScores()   // suspend call
    _uiState.value = scores
}
  • Returns a Job — handle for the coroutine's lifecycle
  • Use when you don't need a return value
  • Most common builder in Android
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

async — Parallel Work with a Result

viewModelScope.launch {
    val scoresDeferred = async { repository.getScores() }
    val teamsDeferred  = async { repository.getTeams() }

    // Both run simultaneously ↑

    val scores = scoresDeferred.await()
    val teams  = teamsDeferred.await()
}
  • Returns Deferred<T> — a promise of a value
  • await() suspends until the result is ready
  • Use when you need parallel work that returns a value
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

suspend — Mark a Pauseable Function

suspend fun getScores(): List<Score> {
    return withContext(Dispatchers.IO) {
        api.fetchScores()   // blocking network call
    }
}
  • Can only be called from a coroutine or another suspend function
  • Compiler enforces this — calling from regular code is a compile error
  • Signals: "this function may take a while"
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Dispatchers

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

The Three Dispatchers

// Main thread — UI updates only
withContext(Dispatchers.Main) {
    binding.textView.text = "Done"
}
// IO thread pool — network, database, files
withContext(Dispatchers.IO) {
    val response = api.fetchData()
}
// CPU thread pool — sorting, parsing, computation
withContext(Dispatchers.Default) {
    val sorted = list.sortedBy { it.score }
}
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Switching Dispatchers

suspend fun loadAndShow() {
    val data = withContext(Dispatchers.IO) {
        database.query()        // IO thread — safe to block
    }
    withContext(Dispatchers.Main) {
        updateUI(data)          // Main thread — UI update
    }
}

Compare to the callback equivalent:

// Traditional Java threading:
new Thread(() -> {
    Data data = database.query();
    runOnUiThread(() -> updateUI(data));
}).start();
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

CoroutineScope in Android

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

viewModelScope — For ViewModels

class ScoreViewModel : ViewModel() {

    fun loadScores() {
        viewModelScope.launch {
            val scores = repository.getScores()
            _uiState.value = scores
        }
    }
}
  • Tied to the ViewModel lifecycle
  • Cancelled automatically when ViewModel is cleared
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

ViewModel Lifecycle + Coroutines

Screen opens  → ViewModel created → viewModelScope active
Device rotates → Activity destroyed → ViewModel SURVIVES
                                    → viewModelScope SURVIVES
                                    → Coroutine keeps running ✅

User presses Back → ViewModel cleared
                  → viewModelScope CANCELLED
                  → All coroutines cancelled ✅
                  → No memory leaks
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

LaunchedEffect — For Compose Composables

@Composable
fun ScoreScreen(viewModel: ScoreViewModel = viewModel()) {

    LaunchedEffect(Unit) {
        viewModel.loadScores()   // Runs once when composable enters
    }

    // UI here
}
  • Launches a coroutine tied to the composable's lifecycle
  • Re-runs if the key changes (Unit = run once)
  • Cancelled when composable leaves the composition
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

rememberCoroutineScope — For Event Handlers

@Composable
fun ScoreCard() {
    val scope = rememberCoroutineScope()

    Button(onClick = {
        scope.launch {              // Launch from a click handler
            repository.saveScore()
        }
    }) {
        Text("Submit Score")
    }
}
  • Returns a scope that survives recomposition
  • Use inside event handlers (clicks, gestures)
  • LaunchedEffect can't be called inside a lambda
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Which Scope to Use?

Situation Use
Coroutine lives in ViewModel viewModelScope.launch {}
Trigger on composable appearing LaunchedEffect(key) {}
Launch from button click rememberCoroutineScope()
Unit test coroutines runTest {}
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Loading States

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Modeling UI State

data class ScoreUiState(
    val scores: List<Score> = emptyList(),
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)
  • One class — all possible UI states
  • Eliminates impossible state combinations
  • Easy to test each state independently
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

ViewModel: Load with Error Handling

fun loadScores() {
    viewModelScope.launch {
        _uiState.update { it.copy(isLoading = true, errorMessage = null) }

        try {
            val scores = withContext(Dispatchers.IO) {
                repository.getScores()
            }
            _uiState.update { it.copy(scores = scores, isLoading = false) }
        } catch (e: Exception) {
            _uiState.update {
                it.copy(isLoading = false, errorMessage = e.message)
            }
        }
    }
}
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

UI: React to State

@Composable
fun ScoreScreen(vm: ScoreViewModel = viewModel()) {
    val state by vm.uiState.collectAsState()

    when {
        state.isLoading          -> CircularProgressIndicator()
        state.errorMessage != null -> ErrorMessage(state.errorMessage!!)
        else                     -> ScoreList(state.scores)
    }
}

The UI knows nothing about coroutines — it just renders state.

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Flow: Reactive Streams

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Coroutine vs Flow

// suspend: one value, one time
suspend fun getScore(): Score { ... }

// Flow: many values, over time
fun observeScores(): Flow<List<Score>> { ... }

Flow emits values continuously — like a live feed

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Flow in Practice

// DAO (Room returns Flow automatically):
@Query("SELECT * FROM scores ORDER BY holeNumber")
fun observeScores(): Flow<List<Score>>

// ViewModel converts to StateFlow for UI:
val scores: StateFlow<List<Score>> = repository
    .observeScores()
    .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())

// UI collects:
val scores by viewModel.scores.collectAsState()

Insert a score → Room emits → ViewModel passes it → UI recomposes. Automatic.

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Live Code: Async Score Loader

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

What We'll Build

[Loading spinner]
       ↓  (1.5 seconds)
[List of scores]

Using: viewModelScope.launch · delay() · StateFlow · LaunchedEffect

Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

ViewModel

class ScoreViewModel : ViewModel() {
    data class UiState(
        val scores: List<String> = emptyList(),
        val isLoading: Boolean = false,
        val error: String? = null
    )

    private val _state = MutableStateFlow(UiState())
    val state: StateFlow<UiState> = _state.asStateFlow()

    fun loadScores() {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true) }
            delay(1500)                          // swap for real API call
            _state.update {
                it.copy(
                    isLoading = false,
                    scores = listOf("Hole 1: 4", "Hole 2: 3", "Hole 3: 5")
                )
            }
        }
    }
}
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Composable

@Composable
fun ScoreScreen(vm: ScoreViewModel = viewModel()) {
    val state by vm.state.collectAsState()

    LaunchedEffect(Unit) {
        vm.loadScores()
    }

    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        when {
            state.isLoading -> CircularProgressIndicator()
            state.error != null -> Text(state.error!!)
            else -> LazyColumn {
                items(state.scores) { Text(it, Modifier.padding(16.dp)) }
            }
        }
    }
}
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Summary

  • launch — fire-and-forget background work
  • async / await — parallel work that returns a value
  • suspend — marks a function as potentially pauseable
  • Dispatchers.IO — network, database, file operations
  • viewModelScope — coroutines tied to ViewModel lifecycle
  • LaunchedEffect — trigger side effects when composable appears
  • rememberCoroutineScope — launch from UI event handlers
  • Flow — reactive streams for continuous data updates
Week 10 Monday: Coroutines & Async Programming
CS 3180 Mobile Application Development

Due This Week

Item Due
Lab 7: Get Data from the Internet Friday 11:59 PM
Chapter 8 Quiz Sunday 11:59 PM
zyBook Ch 8 activities Sunday 11:59 PM
Week 10 Monday: Coroutines & Async Programming

SPEAKER NOTES: Welcome back from the midterm. Today we start the second half of the course with one of the most important Kotlin features in Android development. The concepts from here on build directly on everything covered in weeks 1-7.

SPEAKER NOTES: Before any code, let's establish the problem. Students have all experienced laggy apps — today they learn why that happens and how to prevent it.

SPEAKER NOTES: 16ms per frame = 60 frames per second. Anything that takes longer causes a dropped frame (jank). Ask: "Have you ever seen an app freeze or stutter? What was probably happening?" Almost always: someone blocked the main thread.

SPEAKER NOTES: All of these are common operations in real apps. Every one of them can exceed 16ms. If you call any of these on the main thread, you get jank at best and an ANR (Application Not Responding) dialog at worst — Android kills the app after 5 seconds.

SPEAKER NOTES: The challenge isn't just moving work off the main thread — it's getting results back. Traditional approach: callbacks and threads. That gets messy fast. Kotlin coroutines solve this elegantly.

SPEAKER NOTES: Coroutines are Kotlin's answer to async programming. Let's build a mental model before writing any syntax.

SPEAKER NOTES: Key mental model: blocking vs suspending. A blocked thread sits idle, doing nothing, burning resources. A suspended coroutine releases the thread entirely — other coroutines can use that thread while this one waits. The code still reads top-to-bottom, which is why coroutines are so much cleaner than callbacks.

SPEAKER NOTES: You can run 100,000 coroutines where hundreds of threads would exhaust system memory. In Android apps you'll rarely need more than a handful at once, but the lightweight nature means you never have to think about it. The sequential code style is the real win — no callback hell.

SPEAKER NOTES: Three ways to start a coroutine. Each has a distinct purpose.

SPEAKER NOTES: launch says "go do this, I don't need you to come back with anything." Common use: load data on screen open, save a record, trigger a sync. The Job lets you cancel the coroutine if needed.

SPEAKER NOTES: async is like launch but gives you a handle to a future value. The critical insight here: both async blocks START at the same time. If each takes 1 second, running them in parallel takes 1 second total instead of 2. Sequential await() calls collapse the parallelism — you get results in order but work runs concurrently.

SPEAKER NOTES: The suspend keyword is a contract. It says: "I might pause. You must be inside a coroutine to call me." The compiler enforces this at compile time — you can't accidentally call a suspend function on the main thread without wrapping it in a coroutine. This is safety by design.

SPEAKER NOTES: Dispatchers control which thread (or thread pool) a coroutine runs on. In Android, this matters a lot — some operations are only safe on specific threads.

SPEAKER NOTES: Three dispatchers in practice: Main: The only thread that can touch Views/Compose. Never block here. IO: A pool of threads optimized for waiting. Can safely block here (network timeouts, file reads). Default: A pool optimized for CPU work. Sorting a 10,000-item list, parsing a large JSON document. The framework makes switching between them trivial: just withContext().

SPEAKER NOTES: The coroutine version reads top-to-bottom like synchronous code. The Java version has nested callbacks, manual thread creation, and no structured lifecycle. In real apps the callback version gets much worse — multiple async steps create deeply nested "callback hell." Coroutines eliminate this entirely.

SPEAKER NOTES: Every coroutine lives inside a scope. The scope controls lifetime — when the scope is cancelled, all its coroutines are cancelled automatically.

SPEAKER NOTES: viewModelScope is the primary scope in Android MVVM. When the user navigates away permanently, the ViewModel is cleared and all its coroutines are cancelled — no memory leaks, no background work for a dead screen. The framework handles all of this. You never write cleanup code.

SPEAKER NOTES: Draw this on the board. Rotation is a configuration change — the Activity dies and is recreated, but the ViewModel outlives it. This is why we put coroutines in the ViewModel: they survive rotation. When the user genuinely leaves, everything is cleaned up automatically.

SPEAKER NOTES: LaunchedEffect is the Compose equivalent of "do this side effect when I appear." The key parameter controls when it re-runs: Unit means "only on first composition." If you passed a userId, it would re-run whenever userId changes. This is how you trigger data loads when a screen appears.

SPEAKER NOTES: rememberCoroutineScope solves a specific problem: you need to launch a coroutine from a click handler, but click handlers are lambdas — not composable functions. LaunchedEffect can't go there. rememberCoroutineScope gives you a scope you can call .launch() on inside any callback. The scope is tied to the composable's lifecycle and cancelled when it leaves composition.

SPEAKER NOTES: Quick reference. In practice: 90% of your coroutines go in viewModelScope. LaunchedEffect for triggering loads when a screen appears. rememberCoroutineScope for the rare case you need to launch from a UI event handler directly in a composable (vs delegating to ViewModel).

SPEAKER NOTES: Real apps show loading indicators, handle errors, and display results. The loading state pattern is how we model all three in one place.

SPEAKER NOTES: Why one class? Imagine three separate StateFlows: scoresFlow, isLoadingFlow, errorFlow. What stops the UI from showing loading=true AND error != null simultaneously? Nothing. Bundling them in one class lets you enforce that only valid combinations exist.

SPEAKER NOTES: Walk through each step: 1. Set loading=true before starting (so the spinner appears) 2. Do actual work on IO thread 3. Success: update scores, clear loading 4. Failure: set error message, clear loading try/catch works exactly as you'd expect inside a coroutine — no special handling needed.

SPEAKER NOTES: This is the separation of concerns: ViewModel manages async work and state, composable renders state. The composable can't even tell that a coroutine is running — it just sees a StateFlow it observes. This also makes testing trivial: you can set uiState to any value in a test and verify the composable renders correctly.

SPEAKER NOTES: One more concept — Flow. Where suspend functions handle one-shot operations, Flow handles streams of values over time.

SPEAKER NOTES: Think of it like a river vs a bucket. A suspend function fills the bucket once. A Flow is the river — values keep arriving. When does this matter? Database queries: instead of "get scores once," you can observe them — every time a score is inserted or updated, the Flow emits the new list automatically.

SPEAKER NOTES: This is the full reactive chain. No polling, no manual refresh buttons. We'll use this heavily in Week 12 with Room. For now, the key insight: you declare what you want to observe and the updates flow through automatically. The UI never has to ask "is there new data?" — it just gets told.

SPEAKER NOTES: Let's build a working example together. Open Android Studio. We'll simulate network latency with delay() — the pattern is identical to a real API call.

SPEAKER NOTES: The goal is to see the full lifecycle: composable appears → LaunchedEffect triggers load → ViewModel launches coroutine → spinner shows → data arrives → UI updates. Every piece maps directly to real network code — just swap delay() for an API call.

SPEAKER NOTES: Type this live. Emphasize: delay() is a suspend function, just like an API call. The pattern is identical. Ask students to predict what the UI shows at t=0 (spinner) and t=1.5s (list). Then run the app to confirm.

SPEAKER NOTES: Point out LaunchedEffect(Unit) — this fires once when the composable enters composition and calls loadScores(). The when block handles all three states exhaustively. Run the app: students see the spinner for 1.5 seconds, then the list appears. No button tap required.

SPEAKER NOTES: Read through quickly — these are the vocabulary students need for the rest of the course. Wednesday we extend this with WorkManager for guaranteed background work that survives process death.

SPEAKER NOTES: Architecture diagram, screen wireframes, database schema, timeline. Wednesday's WorkManager content feeds directly into the architecture section.