· 6 min · Kotlin, Coroutines, Architecture
Kotlin Flows in Production: Patterns I Keep, Patterns I Dropped
After 1.5 years of shipping Flow-based Android apps, some patterns survived contact with production and some quietly caused the worst bugs of my career. A field report.
Flows look elegant in blog posts and behave differently under real lifecycles, process death, and flaky networks. These are the patterns that survived production at BytXen.
Keep: stateIn with WhileSubscribed(5000)
val uiState: StateFlow<ProductUiState> = repository.observeProducts()
.map { ProductUiState.Loaded(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ProductUiState.Loading
)
The 5-second timeout is not a magic number — it is exactly long enough to survive a configuration change without restarting upstream work, and short enough to stop collecting when the user actually leaves.
Keep: callbackFlow for legacy listeners
Wrapping location callbacks, sensor listeners, and Play Billing into callbackFlow with awaitClose gives one consistent cancellation story. The moment everything is a Flow, structured concurrency does the cleanup you used to forget.
Dropped: shared flows as event buses
SharedFlow-as-event-bus reintroduces every problem LocalBroadcastManager had: invisible producers, dropped events during process death, impossible debugging.
- Sealed UI state for anything the screen renders
- One-shot Channel for navigation / snackbar events
- No shared "god" event bus crossing feature boundaries
Dropped: flatMapLatest chains three levels deep
Clever operator chains become unreadable the moment a bug appears at 2 AM. If a chain needs a comment to explain, I now split it into named intermediate flows. Boring code ships.