0
0
0
0
0
0
0
0
0
0
0
0

· 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.

← All entries · Portfolio