跪拜 Guibai
← All articles
Android

Structured Concurrency Isn't Just for Coroutines: Why Android's Data Layer Must Stop Launching, Starting, and Broadcasting

By 潜龙勿用之化骨龙 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

For Western Android developers, this analysis exposes that many "best practices" circulating in the community — like Repositories managing their own coroutine scopes — are actually anti-patterns that undermine structured concurrency. The stakes are real: these patterns lead to resource leaks, untraceable bugs, and architectures that resist change. Understanding the ownership principle gives teams a concrete decision framework for code reviews and architecture design.

Summary

This analysis argues that many seemingly unrelated Android development debates — whether a Repository should `launch` a coroutine, whether a Data layer should expose `start()/stop()` for resource management, and whether EventBus should still be used — are actually manifestations of a single architectural failure: the ownership of tasks, resources, and events is placed in the wrong layer.

The piece systematically deconstructs three major anti-patterns: ViewModels deciding thread models with `Dispatchers.IO`, UI layers manually catching exceptions, and Repositories secretly creating their own coroutine scopes. It then extends the same structural thinking to continuous resources, showing how `callbackFlow + awaitClose` replaces fragile `start/stop` patterns by binding resource lifecycles to subscription relationships.

Finally, it argues that EventBus and LocalBroadcastManager should be retired entirely, replaced by `StateFlow`, `SharedFlow`, and shared ViewModels — not just for better APIs, but because they restore traceable ownership to inter-component communication. The unifying principle: every task, resource, and event must have a clear, structured owner that manages its lifecycle.

Takeaways
Repository should expose `suspend fun` for one-shot operations, not internally `launch` a coroutine — the caller decides the lifecycle.
`withContext` changes the execution thread; `launch` changes the task's ownership — they are not interchangeable.
ViewModel should never specify `Dispatchers.IO` or manually catch exceptions; those responsibilities belong to the Repository layer.
Continuous resources (broadcasts, listeners) should use `callbackFlow + awaitClose` instead of `start()/stop()` patterns.
EventBus and LocalBroadcastManager should be replaced by `StateFlow`, `SharedFlow`, and shared ViewModels for all in-app communication.
`launch` belongs at boundary layers (ViewModel, UseCase, WorkManager); `suspend` stays in composable business capability layers.
Four questions to decide if a Repository should `launch`: Is it one-shot or continuous? Who decides the lifecycle? Does the caller need completion/failure info? Must the task outlive the caller?
Migration from `start/stop` to `callbackFlow` eliminates Receiver leaks, duplicate registration crashes, and shared boolean state.
`WhileSubscribed(5_000)` prevents churn from brief backgrounding or configuration changes.
Cross-module state should use Repository + `StateFlow`, not event buses — even if the bus is implemented with Kotlin Flow.
Exceptions are implementation details; the UI layer should only care about result semantics, not exception types.
Structured concurrency means every concurrent task knows who creates, manages, cancels, awaits, and handles its exceptions.
Conclusions

The most common Android coroutine mistakes are not API errors but boundary violations — layers taking on responsibilities that don't belong to them.

The `start/stop` pattern is structurally identical to the old thread model: it relies on human memory for lifecycle management, which is inherently fragile.

Replacing EventBus with Flow-based solutions without also fixing the ownership problem is just swapping tools, not solving the architecture.

The principle of structured ownership applies far beyond coroutines — it's a universal architectural heuristic for any system with tasks, resources, or events.

The fact that many developers instinctively defend Repository-level `launch` as 'simpler' reveals how deeply the non-structured mindset is embedded.

`callbackFlow` is not just syntactic sugar; it's a paradigm shift from imperative lifecycle management to declarative, subscription-driven lifecycle management.

The four-question decision framework for Repository `launch` is a rare example of a concrete, teachable rule that can be applied in code review without ambiguity.

Modern Android architecture's real value isn't in the specific APIs (StateFlow, SharedFlow) but in the structural discipline they enforce.

The article's central claim — that all these problems are the same problem — is a powerful reframing that can simplify team discussions about architecture.

The migration from EventBus to Flow is not just about avoiding deprecated APIs; it's about making communication relationships explicit and traceable.

Concepts & terms
Structured Concurrency
A programming paradigm where concurrent tasks are organized into a hierarchy with clear parent-child relationships. Every coroutine must belong to a scope, and cancelling a parent scope automatically cancels all its children. This ensures no task outlives its creator, preventing resource leaks and orphaned operations.
Ownership of Tasks, Resources, and Events
The architectural principle that every unit of work, system resource, or communication signal must have a clearly defined owner responsible for its lifecycle — creation, management, cancellation, and exception handling. Violating this principle leads to untraceable bugs and fragile systems.
callbackFlow
A Kotlin Flow builder that bridges callback-based APIs (like Android's BroadcastReceiver or NetworkCallback) into the structured world of coroutines. It uses `awaitClose` to automatically release resources when the Flow collection ends, binding resource lifecycle to subscription lifecycle.
WhileSubscribed(5000)
A `SharingStarted` strategy for `stateIn` that keeps the upstream Flow active for 5 seconds after the last subscriber disappears. This prevents frequent resource registration/release during brief configuration changes or backgrounding, reducing churn and improving performance.
suspend fun vs launch
`suspend fun` declares a function that can be paused and resumed without blocking a thread, but does not create a new coroutine — it runs within the caller's coroutine scope. `launch` creates a new coroutine with its own lifecycle, which may outlive the caller if not properly scoped. The choice determines who controls the task's lifecycle.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗