Lesson 4

Skipping is a decision, not a guarantee

Connect shouldExecute and skipToGroupEnd to strong skipping, current defaults, and the reasons a restartable function still executes.

3 min readUpdated Jul 12, 2026

Status: Restartability gives the runtime a callable scope to revisit. Execution is a separate Durable decision; gates and strong-skipping defaults are Version-specific .

Outcome

Explain why a restartable composable can execute, skip, or be deliberately non-skippable.

Restartable is a return path

A restart group records a RecomposeScopeImpl and a callback that can re-enter it. The pinned compiler emits startRestartGroup, a body, and endRestartGroup()?.updateScope (ComposableFunctionBodyTransformer.kt). That answers “can this region restart?” not “will this invocation skip?”

A skippable body emits shouldExecute(...); false leads to skipToGroupEnd(), which advances over the group without running its body (Composer.kt).

Runtime gates can force execution

At the pinned gap-buffer implementation, skipping is true only when the composer is not inserting or reusing, providers are valid, the current scope does not require recomposition, and forced recomposition is off (GapComposer.kt). Its shouldExecute implementation then returns parametersChanged || !skipping, apart from its pausing path (same source).

An invocation executes when a parameter differs, content is inserted or reused, a provider change disables skipping, or the scope was invalidated and requiresRecompose is true. A state read can therefore force a rerun even with unchanged function parameters. Restartability makes that local rerun possible; it does not make it skippable.

Diagram
flowchart TD
    Restart[Restartable scope] --> Params{Parameters changed?}
    Params -->|Yes| Execute[Execute body]
    Params -->|No| Gates{Runtime skipping gates open?}
    Gates -->|No| Execute
    Gates -->|Yes| Skip[skipToGroupEnd]

Strong skipping widens eligibility, not certainty

The pinned compiler flag documentation says strong skipping makes unstable-parameter restartable composables skippable and memoizes lambdas with unstable captures; it is enabled by default (ComposeFeatureFlags.kt). Its unstable-parameter golden captures changedInstance, shouldExecute, and skipToGroupEnd (golden).

The release history records the transition: Compiler 1.5.13 calls it production-ready and Runtime 1.7.0 lists enabling it (compiler notes, runtime notes; Android Review 2694106, 3040172). Verify the pair you ship.

@NonSkippableComposable remains an explicit escape hatch. The pinned golden testNonSkippableComposable.txt has a restart group but no shouldExecute/skipToGroupEnd pair.

Reproduce the distinction

These are pinned runtime/compiler-test procedures, not experiments run for this lesson. Run the runtime controls at the pin:

./gradlew :compose:runtime:runtime:desktopTest \
  --tests 'androidx.compose.runtime.CompositionTests.testSimpleSkipping' \
  --tests 'androidx.compose.runtime.RestartTests.restart_and_skip'

Control A: testSimpleSkipping invalidates equal Point inputs and expects no changes. Control B: restart_and_skip changes count, then only data; the nested count stays unchanged. Compiler control: run:

./gradlew :plugins:compose-compiler-plugin:compiler-hosted:integration-tests:test \
  --tests 'androidx.compose.compiler.plugins.kotlin.FunctionBodySkippingTransformTestsNoSource.testNonSkippableComposable'

These are not a universal recomposition metric, frame result, or restartability guarantee.

Check yourself

A restartable child receives the same stable parameter, but its parent re-enters. Which component decides whether to skip it?

Evidence ledger

ClaimDirect evidenceStatus
Restart groups store update scopes; compiler emits the restart protocolPinned ComposableFunctionBodyTransformer.kt and RecomposeScopeImpl.ktDurable Version-specific
Runtime gates control shouldExecute and skipToGroupEndPinned GapComposer.kt and Composer.ktVersion-specific
Strong skipping widens unstable-parameter eligibility and is enabled by default at the compiler pinComposeFeatureFlags.kt, release notes, and historyVersion-specific
Non-skippable lowering omits the skip branchPinned testNonSkippableComposable.txtVersion-specific

Freshness

Refresh after changes to shouldExecute, the skipping gates, strong-skipping feature defaults, @NonSkippableComposable, or the release-note transition. Do not turn “the body was skipped in this fixture” into an application-wide performance claim.

Finished this lesson?

Your progress stays only in this browser.