Most Android bugs in real apps come from threading and state, so this lesson matters. It covers the Kotlin features that make code short, then the whole coroutine toolkit with the rules that keep it safe.
Extension, inline and scope functions
An extension function adds a function to an existing type without changing it: fun String.isEmail() = contains("@"). A higher-order function takes or returns a function. inline copies the function body and lambda into the call site, which avoids creating objects. The five scope functions run a block on an object and differ in how you refer to it and what they return.
let: object isit, returns the block result. Used for null checks:name?.let { save(it) }.run: object isthis, returns the block result.with(obj) { ... }: like run, but obj is passed in.apply: object isthis, returns the object. Used for setting up:Intent().apply { action = "x" }.also: object isit, returns the object. Used for side effects such as logging.
Coroutines: the model
A coroutine is a light task that can suspend (pause) without blocking a thread. A suspend function can only be called from a coroutine or another suspend function. Every coroutine lives in a CoroutineScope. In Android you rarely create scopes: viewModelScope ends when the ViewModel is cleared, and lifecycleScope ends with the lifecycle. This is structured concurrency: children cannot outlive the parent, so work is cancelled with its screen and does not leak.
class DetailViewModel(private val repo: Repo) : ViewModel() {
fun load(id: Int) {
viewModelScope.launch {
val user = repo.user(id) // suspend, runs off the main thread inside
val posts = repo.posts(id)
// update state here
}
}
}
Dispatchers, async and cancellation
Dispatchers.Main: UI work.Dispatchers.IO: disk and network.Dispatchers.Default: heavy calculation.- Use
withContext(Dispatchers.IO) { ... }inside a suspend function to switch thread and come back.
launch starts a job and returns it. async starts work that returns a value and you collect it with await(). Use async to run independent calls in parallel.
suspend fun loadScreen(): Screen = coroutineScope {
val user = async { api.user() }
val feed = async { api.feed() }
Screen(user.await(), feed.await()) // both calls run at the same time
}
Cancelling a job sets a flag. Suspending functions in the standard libraries check it, but a long loop of your own must call ensureActive() or yield(). Never swallow CancellationException in a catch block. For errors, use try/catch inside the coroutine. An exception in a launch child cancels its parent; supervisorScope or SupervisorJob keeps siblings alive.
Flow, StateFlow and SharedFlow
A Flow is a stream of values over time: flow { emit(1); emit(2) }. It is *cold*: it starts when someone collects it. Operators such as map, filter, debounce, distinctUntilChanged, combine and flatMapLatest transform it. StateFlow is a *hot* flow that always holds one current value, the right tool for UI state. SharedFlow has no current value and sends events to all collectors, but one-off events are often better modelled as state. A Channel passes values between coroutines, one receiver per value.
val query = MutableStateFlow("")
val results: StateFlow<List<Item>> = query
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { text -> repo.search(text) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
stateIn turns a cold flow into StateFlow. WhileSubscribed(5000) keeps it running for five seconds after the last collector goes away, which survives a rotation and stops work in the background. In Compose, read it with collectAsStateWithLifecycle().
Thread safety
- Share data between coroutines through immutable objects, StateFlow or a Mutex.
- Avoid mutable shared variables across threads. If you must, use
Mutexor atomic types. - Never call blocking code (
Thread.sleep, blocking network) on the main dispatcher.
Your task
- Write an extension function
fun Int.toRupees(): Stringand use it. - Use
asyncto run two delay calls in parallel and measure that the total is about the longer one. - Build a search flow with debounce and flatMapLatest against a fake repository.
- Cancel a long loop of your own with
ensureActive().
How to check it worked
- You can explain why
viewModelScopeprevents leaks. - You can choose the right dispatcher for a network call and for a sort.
- You know when to use StateFlow and when SharedFlow.