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.
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.
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
| Claim | Direct evidence | Status |
|---|---|---|
| Restart groups store update scopes; compiler emits the restart protocol | Pinned ComposableFunctionBodyTransformer.kt and RecomposeScopeImpl.kt | Durable
Version-specific |
Runtime gates control shouldExecute and skipToGroupEnd | Pinned GapComposer.kt and Composer.kt | Version-specific |
| Strong skipping widens unstable-parameter eligibility and is enabled by default at the compiler pin | ComposeFeatureFlags.kt, release notes, and history | Version-specific |
| Non-skippable lowering omits the skip branch | Pinned testNonSkippableComposable.txt | Version-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.