Agent skill

Collecting Flows Safely

by skydoves in skydoves/compose-performance-skills

A skill your agent uses to migrate Compose UI from collectAsState() to collectAsStateWithLifecycle(), hoist Flow<T parameters out of composables, and apply .conflate() / .distinctUntilChanged() /…

Apache-2.0Auto-check passed

Install Collecting Flows Safely

skills CLI
$ npx skills add skydoves/compose-performance-skills --skill collecting-flows-safely -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install skydoves/compose-performance-skills collecting-flows-safely --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/skydoves/compose-performance-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/side-effects/collecting-flows-safely .claude/skills/collecting-flows-safely && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
collecting-flows-safely
GitHub stars
513
Token cost
~3.1k tokens
SKILL.md length
913 words
Files
1
Skills in repo
2
Repo updated
First seen
Licence
Apache-2.0

At a glance

A skill your agent uses to migrate Compose UI from collectAsState() to collectAsStateWithLifecycle(), hoist Flow<T parameters out of composables, and apply .conflate() / .distinctUntilChanged() /…

  • Works in 7 steps: Audit every collectAsState() call.… → Provide an initialValue when the flow is… → Pick the right minActiveState. Default… → …
  • Migrate Compose UI from collectAsState() to collectAsStateWithLifecycle()
  • SKILL.md covers When to use this skill, When NOT to use this skill, Prerequisites and Workflow, plus 4 more sections
  • Instructions only: no scripts, shell commands, URLs or credentials in SKILL.md

What it does

Collecting Flows Safely is an agent skill from skydoves/compose-performance-skills. Use this skill to migrate Compose UI from collectAsState() to collectAsStateWithLifecycle(), hoist Flow<T parameters out of composables, and apply .conflate() / .distinctUntilChanged() / snapshotFlow so background CPU and battery stop draining and chatty flows stop invalidating the UI per emission. Covers ViewModel StateFlow/SharedFlow consumers, sensor and location streams, and the "Flow as composable parameter" antipattern. Trigger when the user mentions collectAsState, collectAsStateWithLifecycle…

Its SKILL.md is about 3.1k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

The repository describes itself as: ⚡️ A curated library of Agent Skills focused on Jetpack Compose performance. The licence is Apache-2.0.

When your agent uses it

  • Migrate Compose UI from collectAsState() to collectAsStateWithLifecycle()
  • Hoist Flow<T parameters out of composables
  • The user mentions collectAsState
  • CollectAsStateWithLifecycle

Example prompts

  • “Flow as composable parameter”
  • “/collecting-flows-safely”

Workflow steps

7 steps, taken from the first numbered list in SKILL.md.

  1. Audit every collectAsState() call. Search the module for .collectAsState(. Replace each call with collectAsStateWithLifecycle() unless the…
  2. Provide an initialValue when the flow is not a StateFlow (cold Flow or SharedFlow). For StateFlow, the overload reads .value — no initial…
  3. Pick the right minActiveState. Default STARTED matches the framework's repeatOnLifecycle default. Use RESUMED for widgets that should only…
  4. For high-frequency producers (>1 emission per ~100 ms), add .conflate() upstream of collectAsStateWithLifecycle() so the consumer keeps…
  5. Hoist Flow parameters out of composables. Replace fun Foo(prices: Flow) with fun Foo(price: Price). Collect at the caller. If the producer…
  6. For State → Flow direction, use snapshotFlow { ... } inside a LaunchedEffect. That is the supported bridge from Compose's snapshot system…
  7. Verify with @TraceRecomposition (see ../../measurement/tracing-recompositions-at-runtime/SKILL.md) and a logcat sanity check with the app…

What it can do on your machine

Read from SKILL.md and the folder at commit 1b32f81. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are kotlin).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Links to these hosts (documentation or services it may open):

    • medium.com
    • developer.android.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Collecting Flows Safely loads about 3.1k tokens when it runs. Until then it costs about 186 tokens; SKILL.md has 913 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~186
When it runs · the whole SKILL.md, loaded when a task matches
~3.1k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from skydoves/compose-performance-skills at commit 1b32f81, republished under its Apache-2.0 licence (© skydoves). 913 words, ~3,099 tokens.

Download SKILL.mdSave it as .claude/skills/collecting-flows-safely/SKILL.md (or your agent's skills folder).
name
collecting-flows-safely
description
Use this skill to migrate Compose UI from `collectAsState()` to `collectAsStateWithLifecycle()`, hoist `Flow<T>` parameters out of composables, and apply `.conflate()` / `.distinctUntilChanged()` / `snapshotFlow` so background CPU and battery stop draining and chatty flows stop invalidating the UI per emission. Covers ViewModel `StateFlow`/`SharedFlow` consumers, sensor and location streams, and the "Flow as composable parameter" antipattern. Trigger when the user mentions `collectAsState`, `collectAsStateWithLifecycle`, lifecycle-aware flow collection, `Lifecycle.State.STARTED`, background battery drain from a Compose screen, `snapshotFlow`, `Flow` parameter on a composable, conflate, or distinctUntilChanged.
license
Apache-2.0. See LICENSE for complete terms.
metadata.author
Jaewoong Eum (skydoves)
metadata.keywords
jetpack-compose, performance, collectasstatewithlifecycle, flow, lifecycle, battery, snapshotflow, side-effects

Collecting Flows Safely — keep upstream work tied to the UI lifecycle

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.

When to use this skill

  • A ViewModel or Repository exposes StateFlow<T>, SharedFlow<T>, or a cold Flow<T> to a Compose screen.
  • The user reports background battery drain, "the screen keeps working when the app is in the recents tray", or wakelock noise on logcat.
  • A composable consumes sensor data, location, GPS, websocket, or animation frames coming from outside Compose.
  • A composable's signature is fun MyScreen(state: Flow<State>) (Flow-as-parameter antipattern).
  • The user asks about collectAsState vs collectAsStateWithLifecycle, or about Lifecycle.State.STARTED / RESUMED semantics.
  • The user wants Compose state to drive a non-Compose subscriber (analytics on visible item index, etc.) — that is the State→Flow direction served by snapshotFlow.

When NOT to use this skill

  • The flow is constructed and consumed entirely inside one composable's remember { ... } block — that is composition-internal state, prefer mutableStateOf directly. See ../using-efficient-effects/SKILL.md for choosing the right effect API.
  • The data source is already State<T> (e.g. mutableStateOf, Animatable.asState()) — do not wrap it in a flow just to call collectAsStateWithLifecycle().
  • A truly always-on background listener that must run while the Activity is STOPPED. Move that work to a Service / WorkManager / repeatOnLifecycle in the Activity, not into Compose.

Prerequisites

  • Compose UI 1.4+ (any modern release).
  • Add androidx.lifecycle:lifecycle-runtime-compose (2.6+; the artifact that exposes collectAsStateWithLifecycle). Maven coordinates: androidx.lifecycle:lifecycle-runtime-compose:<version>.
  • Kotlin coroutines basics (StateFlow, SharedFlow, conflate, distinctUntilChanged).
  • Read ../../recomposition/deferring-state-reads/SKILL.md if the high-frequency emissions are driving animation values — phase deferral may be a better fix than .conflate().

Workflow

  1. Audit every collectAsState() call. Search the module for \.collectAsState\(. Replace each call with collectAsStateWithLifecycle() unless the producing flow is created inside the same composable scope.
  2. Provide an initialValue when the flow is not a StateFlow (cold Flow<T> or SharedFlow<T>). For StateFlow<T>, the overload reads .value — no initial value needed.
  3. Pick the right minActiveState. Default 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.
  4. For high-frequency producers (>1 emission per ~100 ms), add .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().
  5. Hoist Flow<T> parameters out of composables. Replace 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.
  6. For State → Flow direction, use snapshotFlow { ... } inside a LaunchedEffect. That is the supported bridge from Compose's snapshot system to coroutine flows; combine with .distinctUntilChanged() to avoid spurious emissions.
  7. Verify with @TraceRecomposition (see ../../measurement/tracing-recompositions-at-runtime/SKILL.md) and a logcat sanity check with the app backgrounded — upstream emissions should stop.

Patterns

Pattern: replace collectAsState with collectAsStateWithLifecycle
kotlin
// 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.
kotlin
// RIGHT
import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
fun HomeScreen(viewModel: HomeViewModel) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    HomeContent(state)
}
Show full SKILL.md (368 more words)Show less
Pattern: custom minActiveState for an always-visible widget
kotlin
// 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)
}
Pattern: do not pass Flow<T> as a composable parameter (skydoves hot take #4)
kotlin
// 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).
kotlin
// 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)
}
Pattern: high-frequency flow needs .conflate() + .distinctUntilChanged()
kotlin
// 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.
kotlin
// RIGHT
@Composable
fun TiltIndicator(repository: SensorRepository) {
    val flow = remember(repository) {
        repository.tilt.conflate().distinctUntilChanged()
    }
    val tilt by flow.collectAsStateWithLifecycle(initialValue = 0f)
    TiltUi(tilt)
}
Pattern: State → Flow with snapshotFlow
kotlin
// 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.
Pattern: prefer snapshotFlow over derivedStateOf for fire-and-forget reactions
kotlin
// LESS PREFERRED for an effect that only emits side effects
val isAtTop by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
LaunchedEffect(isAtTop) { if (isAtTop) reportTop() }
kotlin
// PREFERRED — no derived state slot in composition; reads happen in the coroutine
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex == 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect { reportTop() }
}

Mandatory rules

  • MUST call collectAsStateWithLifecycle() (from androidx.lifecycle:lifecycle-runtime-compose) for any Flow that originates outside the composition (ViewModel, repository, sensor, network, websocket, location).
  • MUST NOT pass 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.
  • MUST add .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.
  • MUST wrap the operator chain in remember(key) when applying .conflate() / .distinctUntilChanged() inline, so a new chain is not created on every recomposition.
  • MUST NOT use Lifecycle.State.CREATED for minActiveState — it leaves collection running while the activity is invisible, defeating the migration.
  • MUST NOT rewrap an existing State<T> into a flow just to call collectAsStateWithLifecycle(). Keep the State direct.
  • PREFERRED: snapshotFlow { ... } over derivedStateOf { ... } when the only consumer is a fire-and-forget effect (analytics, logging, side-channel emit) instead of UI.

Verification

  • grep -R "collectAsState(" src/ returns 0 hits, or each remaining hit collects a flow created inside the same composable.
  • Background the app and watch logcat: upstream operators (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.
  • Battery Historian / Studio Energy Profiler shows no foreground-only work attributed to the screen while the app is backgrounded.
  • No composable in the module declares a parameter of type Flow<*> (grep -R ": Flow<" src/).
  • When minActiveState = RESUMED is used, the rationale (focus-required widget) is documented in code.

References

© skydoves, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in side-effects/collecting-flows-safely of skydoves/compose-performance-skills.

Open the folder on GitHubat commit 1b32f81

Compare with similar skills

Collecting Flows Safely next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Collecting Flows Safely compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Collecting Flows Safely this skillskydoves/compose-performance-skills513—~3.1kAutomated safety check: PassApache-2.0
Safe Database Migration Patternsaffaan-m/ECC276k—~3.3kAutomated safety check: PassMIT
Database Migrationsaffaan-m/ECC276k4 repos~3kAutomated safety check: PassMIT
Reversible MigrationJuliusBrussee/caveman111k1 repos~196Automated safety check: PassApache-2.0
Database Migrationsaffaan-m/ECC276k1 repos~2.4kAutomated safety check: PassMIT
DB Migratesimstudioai/sim30k—~2kAutomated safety check: PassApache-2.0

Similar skills

  • Rules and examples for safe, reversible schema changes in production: zero-downtime column and index changes, large data backfills and ORM migration workflows.

    276k GitHub stars~3.3k tokensUpdated 4 days ago
    DatabasesAuto-check passed
  • Safe, reversible database migration patterns: forward-only production changes, expand-contract zero-downtime renames, concurrent indexes, batched backfills, and per-tool workflows for PostgreSQL…

    276k GitHub starsUsed in 4 repos~3k tokens
    DatabasesAuto-check passed
  • Reversible Migration

    JuliusBrussee/caveman

    Implement reversible compatibility-safe transitions. Use for schema, data, API, protocol, configuration, or dependency migrations requiring rollback and…

    111k GitHub starsUsed in 1 repo~196 tokens
    DevelopmentAuto-check passed
  • Şema değişiklikleri, veri migration'ları, rollback'ler ve PostgreSQL, MySQL ve yaygın ORM'ler (Prisma, Drizzle, Django, TypeORM, golang-migrate) arasında sıfır kesinti deployment'ları için…

    276k GitHub starsUsed in 1 repo~2.4k tokens
    DatabasesAuto-check passed
  • DB Migrate

    simstudioai/sim

    Author or review a Drizzle DB migration for zero-downtime safety — expand/contract phasing, backward-compatibility with the deployed app version, and writing the -- migration-safe acknowledgment the…

    30k GitHub stars~2k tokensUpdated today
    DatabasesAuto-check passed
  • Covers schema conventions and safe rollout for Drizzle database migrations: online indexes, backfills, idempotent SQL and fixing regeneration or rebase conflicts.

    83k GitHub stars~3.5k tokensUpdated today
    DatabasesAuto-check: notes

More from skydoves/compose-performance-skills

  • Choosing Derivedstateof

    skydoves/compose-performance-skills

    A skill your agent uses to decide when Jetpack Compose derivedStateOf is the right tool and when it is pure overhead.

    513 GitHub stars~2.9k tokensUpdated 3 mo ago
    Auto-check passed

Questions about Collecting Flows Safely

What does Collecting Flows Safely do?

A skill your agent uses to migrate Compose UI from collectAsState() to collectAsStateWithLifecycle(), hoist Flow<T parameters out of composables, and apply .conflate() / .distinctUntilChanged() /…. Collecting Flows Safely is an agent skill from skydoves/compose-performance-skills.distinctUntilChanged() / snapshotFlow so background CPU and battery stop draining and chatty flows stop invalidating the UI per emission.

When should I use Collecting Flows Safely?

Collecting Flows Safely fits situations like: migrate Compose UI from collectAsState() to collectAsStateWithLifecycle(); hoist Flow<T parameters out of composables; the user mentions collectAsState; collectAsStateWithLifecycle.

How do I install Collecting Flows Safely in Claude Code?

Run `npx skills add skydoves/compose-performance-skills --skill collecting-flows-safely -a claude-code`. Or copy the skill folder (side-effects/collecting-flows-safely in skydoves/compose-performance-skills) into .claude/skills/collecting-flows-safely in your project. Claude Code loads it when a task matches its description.

How do I install Collecting Flows Safely in Codex?

Run `npx skills add skydoves/compose-performance-skills --skill collecting-flows-safely -a codex`. Or copy the skill folder (side-effects/collecting-flows-safely in skydoves/compose-performance-skills) into .agents/skills/collecting-flows-safely in your project. Codex loads it when a task matches its description.

Can I use Collecting Flows Safely in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add skydoves/compose-performance-skills --skill collecting-flows-safely -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/collecting-flows-safely, .gemini/skills/collecting-flows-safely, .github/skills/collecting-flows-safely and .opencode/skills/collecting-flows-safely in your project.

What does Collecting Flows Safely need to run?

SKILL.md names no scripts, command-line tools or credentials: Collecting Flows Safely is instructions for the agent only.

Does Collecting Flows Safely access the network?

SKILL.md names 2 domains. As links in the text: medium.com and developer.android.com. This is read from the text; nothing was executed.

Is Collecting Flows Safely safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Collecting Flows Safely use?

Collecting Flows Safely is published under the Apache-2.0 licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Collecting Flows Safely use?

About 3.1k tokens (SKILL.md is roughly 12k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Collecting Flows Safely?

Skills that share tags, products or a category with Collecting Flows Safely: Safe Database Migration Patterns (affaan-m/ECC, 276k stars), Database Migrations (affaan-m/ECC, 276k stars), Reversible Migration (JuliusBrussee/caveman, 111k stars) and Database Migrations (affaan-m/ECC, 276k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Collecting Flows Safely?

skydoves (a GitHub user) maintains it in skydoves/compose-performance-skills, which has 513 GitHub stars. The repository holds 2 skills in this directory. The repository was last updated on June 16, 2026.

Source: skydoves/compose-performance-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.