collecting-flows-safely — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited collecting-flows-safely (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
collectAsState() keeps collecting whenever the composable is in composition, including while the app is backgrounded — that wastes CPU and battery. collectAsStateWithLifecycle() ties collection to a Lifecycle.State (default STARTED) so backgrounding pauses upstream work. For chatty flows, pair the consumer with .conflate() and .distinctUntilChanged() so Compose only sees meaningful changes. Skydoves hot take #4: a Flow<T> parameter on a composable is unstable and blocks skipping for the whole composable — collect at the caller and pass the resolved value.
ViewModel or Repository exposes StateFlow<T>, SharedFlow<T>, or a cold Flow<T> to a Compose screen.fun MyScreen(state: Flow<State>) (Flow-as-parameter antipattern).collectAsState vs collectAsStateWithLifecycle, or about Lifecycle.State.STARTED / RESUMED semantics.snapshotFlow.remember { ... } block — that is composition-internal state, prefer mutableStateOf directly. See ../using-efficient-effects/SKILL.md for choosing the right effect API.State<T> (e.g. mutableStateOf, Animatable.asState()) — do not wrap it in a flow just to call collectAsStateWithLifecycle().STOPPED. Move that work to a Service / WorkManager / repeatOnLifecycle in the Activity, not into Compose.androidx.lifecycle:lifecycle-runtime-compose (2.6+; the artifact that exposes collectAsStateWithLifecycle). Maven coordinates: androidx.lifecycle:lifecycle-runtime-compose:<version>.StateFlow, SharedFlow, conflate, distinctUntilChanged).../../recomposition/deferring-state-reads/SKILL.md if the high-frequency emissions are driving animation values — phase deferral may be a better fix than .conflate().\.collectAsState\(. Replace each call with collectAsStateWithLifecycle() unless the producing flow is created inside the same composable scope.StateFlow (cold Flow<T> or SharedFlow<T>). For StateFlow<T>, the overload reads .value — no initial value needed.STARTED matches the framework's repeatOnLifecycle default. Use RESUMED for widgets that should only collect while the Activity owns input focus (e.g. always-visible foreground HUD with an aggressive sensor source). Do not use CREATED — that defeats the purpose..conflate() upstream of collectAsStateWithLifecycle() so the consumer keeps only the latest value across a frame. If consecutive emissions can be value-equal, also add .distinctUntilChanged() — and ensure the emitted type has a correct equals().fun Foo(prices: Flow<Price>) with fun Foo(price: Price). Collect at the caller. If the producer must stay private to the parent, expose a () -> Price lambda provider rather than the raw Flow.snapshotFlow { ... } inside a LaunchedEffect. That is the supported bridge from Compose's snapshot system to coroutine flows; combine with .distinctUntilChanged() to avoid spurious emissions.@TraceRecomposition (see ../../measurement/tracing-recompositions-at-runtime/SKILL.md) and a logcat sanity check with the app backgrounded — upstream emissions should stop.collectAsState with collectAsStateWithLifecycle// WRONG
import androidx.compose.runtime.collectAsState
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
val state by viewModel.uiState.collectAsState()
HomeContent(state)
}
// WRONG because: collection continues while the app is backgrounded -> wasted CPU and battery,
// and any upstream operators (network polling, db queries) keep running.// RIGHT
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
HomeContent(state)
}minActiveState for an always-visible widget// RIGHT
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun TopBar(viewModel: HomeViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle(
minActiveState = Lifecycle.State.RESUMED,
)
TopBarContent(state)
}Flow<T> as a composable parameter (skydoves hot take #4)// WRONG
@Composable
fun PriceTicker(prices: Flow<Price>) {
val price by prices.collectAsState(initial = Price.ZERO)
Text(price.formatted)
}
// WRONG because: Flow is unstable -> blocks skipping for the whole composable; the collection
// lifecycle is also unclear (recreated on every recomposition unless the caller remembers it).// RIGHT
@Composable
fun PriceTicker(price: Price) {
Text(price.formatted)
}
// Caller collects once and passes the value:
@Composable
fun TickerScreen(viewModel: TickerViewModel) {
val price by viewModel.price.collectAsStateWithLifecycle()
PriceTicker(price)
}.conflate() + .distinctUntilChanged()// WRONG
@Composable
fun TiltIndicator(repository: SensorRepository) {
val tilt by repository.tilt.collectAsStateWithLifecycle(initialValue = 0f)
TiltUi(tilt)
}
// WRONG because: hundreds of emissions per second invalidate the consuming composable per emission;
// most are dropped frames worth of work.// RIGHT
@Composable
fun TiltIndicator(repository: SensorRepository) {
val flow = remember(repository) {
repository.tilt.conflate().distinctUntilChanged()
}
val tilt by flow.collectAsStateWithLifecycle(initialValue = 0f)
TiltUi(tilt)
}snapshotFlow// RIGHT
@Composable
fun FeedAnalytics(listState: LazyListState, analytics: Analytics) {
LaunchedEffect(listState, analytics) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> analytics.logFirstVisibleIndex(index) }
}
}
// snapshotFlow bridges snapshot State to a cold Flow without paying the cost of recomposing
// every time firstVisibleItemIndex changes — the read happens inside the LaunchedEffect's coroutine,
// not in the composition phase.snapshotFlow over derivedStateOf for fire-and-forget reactions// LESS PREFERRED for an effect that only emits side effects
val isAtTop by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
LaunchedEffect(isAtTop) { if (isAtTop) reportTop() }// PREFERRED — no derived state slot in composition; reads happen in the coroutine
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex == 0 }
.distinctUntilChanged()
.filter { it }
.collect { reportTop() }
}collectAsStateWithLifecycle() (from androidx.lifecycle:lifecycle-runtime-compose) for any Flow that originates outside the composition (ViewModel, repository, sensor, network, websocket, location).Flow<T> as a composable parameter. Collect at the caller, pass the resolved T (or a () -> T lambda provider for very hot producers). Flow is unstable and blocks skipping for the entire composable..conflate() and/or .distinctUntilChanged() upstream of collectAsStateWithLifecycle() for any flow emitting more often than ~once per 100 ms. Ensure equals() is correct on the emitted type when using distinctUntilChanged.remember(key) when applying .conflate() / .distinctUntilChanged() inline, so a new chain is not created on every recomposition.Lifecycle.State.CREATED for minActiveState — it leaves collection running while the activity is invisible, defeating the migration.State<T> into a flow just to call collectAsStateWithLifecycle(). Keep the State direct.snapshotFlow { ... } over derivedStateOf { ... } when the only consumer is a fire-and-forget effect (analytics, logging, side-channel emit) instead of UI.grep -R "collectAsState(" src/ returns 0 hits, or each remaining hit collects a flow created inside the same composable.Repository log lines, sensor callbacks) stop within one frame and resume on foregrounding.@TraceRecomposition(traceStates = true) on the consuming composable shows one recomposition per meaningful emission, not per raw upstream emission. See ../../measurement/tracing-recompositions-at-runtime/SKILL.md.Flow<*> (grep -R ": Flow<" src/).minActiveState = RESUMED is used, the rationale (focus-required widget) is documented in code.repeatOnLifecycle): https://developer.android.com/topic/libraries/architecture/coroutinesandroidx.lifecycle:lifecycle-runtime-compose release notes: https://developer.android.com/jetpack/androidx/releases/lifecyclesnapshotFlow, LaunchedEffect): https://developer.android.com/develop/ui/compose/side-effects../using-efficient-effects/SKILL.md for LaunchedEffect / DisposableEffect / RememberedEffect selection.../../measurement/tracing-recompositions-at-runtime/SKILL.md for verifying emission counts at runtime.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.