跪拜 Guibai
← All articles
Android

Modern Android: Stop Designing Callback Interfaces, Use Suspend and Flow Instead

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

Keeping Listener/register/unregister patterns inside a coroutine app creates a second, hidden lifecycle model that bypasses structured concurrency. The result is leaked subscriptions and dangling callbacks when scopes cancel; adopting suspend and Flow as the default keeps cancellation and cleanup where the caller already manages it.

Summary

Kotlin coroutine stacks are the norm in Android, yet data-layer interfaces often still expose Listener/register/unregister patterns. A senior engineer with three years of Flow experience recently proposed a callback interface for a continuous data API, showing that using Flow doesn't guarantee thinking in Flow. The right default is to model one-shot operations as suspend functions and continuous data as Flow; callback is fine for low-level APIs but drags a second lifecycle model into a coroutine-native app.

Flow ties cancellation and cleanup to the calling scope, and callbackFlow adapts legacy listener APIs while keeping unregistration in awaitClose. Data semantics decide the type: Flow for event streams, StateFlow for current state (with conflation caveats), suspend for one-shot queries. A data layer can unify IM SDK, vehicle property, and network callbacks behind Flow contracts without forcing underlying APIs to change; the contract is what unifies, not the implementation.

Takeaways
— One-shot asynchronous operations belong in suspend functions; continuous data belongs in Flow.
— Callback interfaces are not inherently wrong; they fit low-level APIs, but app-internal interfaces should follow the coroutine model.
— Flow naturally associates collection with the calling coroutine scope; cancelling the scope cancels collection and upstream processing.
— callbackFlow converts callback-based APIs to Flow, with awaitClose serving as the place to unregister the listener.
— StateFlow holds the current value and conflates equal consecutive values, so it is not a drop-in for event streams that must emit every change.
— The data layer can expose Flow contracts for IM SDK, vehicle property, and network status callbacks while keeping the underlying implementations unchanged.
— Room already provides Flow for queries; wrapping it again in callbackFlow adds no value.
Conclusions

The anecdote about a senior Flow user proposing a callback interface shows that tool experience does not guarantee design-model alignment; without explicit defaults, habits persist.

The article's rule is scoped to projects already fully on coroutines; a mixed codebase may need different trade-offs.

The core distinction is between the consumer contract and the implementation: Flow at the boundary, callbacks behind the adapter, gives flexibility without leaking lifecycle concerns.

Concepts & terms
callbackFlow
A coroutine builder that adapts callback-based APIs into a Flow. The block runs each time collection starts, and awaitClose is invoked to clean up when the collector cancels.
StateFlow
A Flow that always holds a current value, emits updates to collectors, and conflates equal consecutive values. Suited for state, not for every event.
Structured concurrency
The Kotlin coroutine model where coroutines are tied to a lifecycle scope, and cancellation propagates through the hierarchy, so child work stops when the scope is cancelled.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗