rcosteira79/android-skills

kotlin-flows

Use when working with Flow, StateFlow, SharedFlow, or Channel in Kotlin — including cold vs hot stream decisions, operator chains, lifecycle-safe collection, UI state management, callback bridging, or Channel migration in Android or KMP projects.

소스 보기

사용 방법

  1. 설치 명령을 복사하고 자신의 에이전트 환경에 설치하기 전에 소스 패키지를 확인하세요.
  2. 필요한 도구, 인증 정보, 지원 환경은 원본 문서에서 확인하세요.
  3. 입력과 원하는 산출물을 설명하고 출처와 결과를 대조하세요.
npx skills add https://github.com/rcosteira79/android-skills --skill kotlin-flows

시작 전 확인

  • 설치에는 모델 구독, API 한도, 파일 접근 권한이 포함되지 않습니다.
  • 별점 수와 설치 수는 활동 지표이며 독립적인 품질·보안 검증이 아닙니다.
원본 Skill 문서

원본 저장소의 제목, 예시, 코드, 표, 링크, 이미지를 유지해 표시합니다.

Kotlin Flows

This reference covers the Flow traps and semantic edge cases — Channel vs SharedFlow, callback bridging, retry/error handling, side effects in transforms, and KMP boundaries — not the basics of cold/hot streams, the operator table, or lifecycle-safe collection.

Channel vs SharedFlow — the semantics that bite

FoundAction
BroadcastChannelMigrate → SharedFlow (deprecated)
ConflatedBroadcastChannelMigrate → StateFlow (deprecated)
Channel for single-consumer fire-once events (nav, snackbars, one-shot effects)Keep — correct. Channel(BUFFERED).receiveAsFlow()
Channel broadcast to multiple collectorsMigrate → SharedFlow (see below)
Channel as producer-consumer queueKeep — correct

`Channel.receiveAsFlow()` is fan-out, NOT broadcast. With multiple collectors, each event reaches one collector (the framework picks which), not all of them. If every collector must see every event, you need SharedFlow. This is the trap that's easy to fall into when reaching for Channel to multicast.

For single-consumer one-shot events, Channel(BUFFERED).receiveAsFlow() is the default because send suspends until consumed (the event is queued, never dropped), whereas SharedFlow(replay = 0) drops the emission if no collector is active at the instant of emission. Expose the Flow, never the Channel, and collect with collect inside LaunchedEffect — not collectAsStateWithLifecycle, which retains the last event as state and re-fires it on every recomposition / config change.

kotlin
private val _events = Channel<UiEvent>(Channel.BUFFERED)
val events: Flow<UiEvent> = _events.receiveAsFlow()

// in the UI
LaunchedEffect(Unit) {
    viewModel.events.collect { event -> /* navigate / snackbar — consumed once */ }
}

Exactly-once is really at-most-once-with-very-high-probability. A receiveAsFlow() collector cancelled after receive() succeeds but before its block processes the element loses that element. Rare (most cancellations land between receives), but for payment/persistence-critical signals, store the outcome in durable state (a pendingResult field on UiState, cleared by the UI after consumption) — see compose/references/state-management.md "Durable state + acknowledgement".

SharedFlow: launch { emit() } over tryEmit

Default to launch { _events.emit(e) } — it suspends until the collector is ready, so the effect is never silently lost. tryEmit() on a default MutableSharedFlow() (no buffer) silently drops when no subscriber is ready; adding extraBufferCapacity = 1 only moves the cliff (a second rapid emission while the buffer is full returns false and is dropped with no error). Use tryEmit only for miss-tolerable effects (tooltip, sound).

Exposing a read-only flow — explicit backing fields (Kotlin 2.4+)

The _x (mutable) / x (read-only) two-property idiom collapses into a single property with explicit backing fields — stable in Kotlin 2.4, experimental in 2.3:

kotlin
val uiState: StateFlow<UiState>
    field = MutableStateFlow(UiState())   // inside the class: uiState.update { … }; callers see StateFlow

Applies wherever a StateFlow / SharedFlow is exposed from a mutable backing (MutableStateFlow is a StateFlow, so the default getter type-checks). It does not fit a Channel exposed as a Flow — a Channel isn't a Flow, so keep the explicit _events / receiveAsFlow() pair above. On Kotlin < 2.4 (check the version catalog), keep the classic private val _x + val x = _x.asStateFlow().

Callback bridging

kotlin
// Single value: suspendCancellableCoroutine — always prefer it (propagates cancellation)
suspend fun authenticate(token: String): User = suspendCancellableCoroutine { cont ->
    val call = authApi.authenticate(token) { user, error ->
        if (user != null) cont.resume(user) else cont.resumeWithException(error ?: Exception())
    }
    cont.invokeOnCancellation { call.cancel() }
}
// suspendCoroutine only when the API has no cancellation concept at all.

// Stream: callbackFlow — awaitClose is MANDATORY (omitting it leaks the callback and never completes)
fun LocationManager.locationUpdates(provider: String): Flow<Location> = callbackFlow {
    val listener = LocationListener { trySend(it) }
    requestLocationUpdates(provider, 1000L, 0f, listener)
    awaitClose { removeUpdates(listener) }
}

For a third-party SDK with no deregistration API, set a flag inside awaitClose to signal shutdown and document the limitation.

retry / retryWhen — always guard the attempt count

kotlin
repository.getItems()
    .retryWhen { cause, attempt ->
        if (cause is IOException && attempt < 3) { delay((attempt + 1) * 1_000L); true } else false
    }
    .collect { ... }

attempt is the 0-based retry index. Without the attempt < count check the flow retries forever on a persistent error. (retry(3) { it is IOException } is the simple form.)

Error handling — .catch scope and the cancellation trap

.catch intercepts upstream errors only (not exceptions thrown inside collect {}), and — unlike try/catch — it does not swallow CancellationException. So prefer the operator form, and for collector-side work catch specific types.

kotlin
// ✓ upstream errors, cancellation-safe
repository.getItems().onEach { process(it) }.catch { logger.error(it) }.collect()

// ✗ catch (e: Exception) inside collect/any suspend fn swallows CancellationException and breaks
//   structured concurrency — the single most common coroutine bug.
//   If a broad catch is unavoidable: if (e is CancellationException) throw e  // before handling

Side effects belong outside combine/map transforms

Transform lambdas are pure and re-execute on every upstream emission and every resubscription (e.g. rotation with WhileSubscribed). A viewModelScope.launch {} or event emission inside one fires repeatedly. Move it to onEach after the transform:

kotlin
combine(userFlow, settingsFlow) { user, settings -> UiState(user, settings) }
    .onEach { analytics.logView(it.user.id) }   // not inside combine
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState.Empty)

`combine` waits for every input to emit at least once before its first emission. If one input is a cold flow that never emits, the combined flow never emits — the classic "screen stuck on Loading" bug. Make each input a StateFlow, give cold inputs onStart { emit(initial) }, or sentinel-prefix them. (combine(a, b) { (x, y) -> … } destructuring doesn't compile.)

KMP

Expose Flow from shared code; collect per-platform (iOS: SKIE or a CoroutineScope wrapper). Avoid reading StateFlow.value from non-coroutine iOS contexts — use a collection wrapper.

Testing

Flow testing lives in android-skills:android-testing — read the two-schedulers trap (runTest + MainDispatcherRule must share one scheduler) before asserting on hot flows.

관련 작업

같은 저장소의 Skills

더 많은 Skills

모든 Skills
rcosteira79
커뮤니티

android-data-layer

Use when implementing the data layer in Android or KMP — the Repository pattern's layered error-propagation model (repository as the error boundary, sealed DataError, Result placement with and without a domain layer) and Room in commonMain (@ConstructedBy, BundledSQLiteDriver, per-target KSP).

설치 수
1
GitHub Stars
155
업데이트
9월 19일
rcosteira79
커뮤니티

android-debugging

Use when debugging Android or KMP issues — Android-specific techniques covering Logcat, ADB, ANR traces, R8 stack trace decoding, memory leaks, Gradle build failures, and Compose recomposition bugs, on a root-cause-first foundation.

설치 수
1
GitHub Stars
155
업데이트
9월 19일
rcosteira79
커뮤니티

android-dev

Use this skill as the baseline for ALL Android and Kotlin Multiplatform (KMP) work — whenever the user mentions Android, Kotlin (in an Android context), KMP, CMP, commonMain, androidMain, iosMain, AndroidManifest, Gradle, build.gradle, Hilt, Dagger, Room, Retrofit, Ktor, ViewModel, LiveData, StateFlow, SharedFlow, Compose, Activity, Fragment, Intent, ADB, Logcat, MVVM, MVI, repository pattern, or any Android SDK / Jetpack / AndroidX API. Always load this skill alongside the more specific skills

설치 수
1
GitHub Stars
155
업데이트
9월 19일
rcosteira79
커뮤니티

android-ux

Use when designing or reviewing Android UI — applies Material Design 3 UX principles covering touch targets, spacing, navigation patterns, accessibility, animation timing, and platform conventions. Includes an M3 compliance audit that scores screens across 10 categories. Complements the compose skill with design-level decisions.

설치 수
1
GitHub Stars
155
업데이트
9월 19일