min
06/23/2026, 8:25 AMCompositionLocalProvider. I know that it remembers its mappings for comparison on recompose. Mappings of instances by…
• compositionLocalOf are backed by a mutable snapshot state, reused across recompositions, that invalidates readers when changed
• staticCompositionLocalOf are not, and instead use a wrapper that’s replaced with a new one when mapped to a different value.
But how does remembering a different value (because local provides 1 during one composition and local provides 2 during the next, for example) invalidate the content: @Composable () -> Unit recompose scope?min
06/23/2026, 11:30 AM@Composable
fun LocalDemo(modifier: Modifier = Modifier) {
val state = remember { mutableIntStateOf(0) }
Column(modifier = modifier) {
val key = staticCompositionLocalOf { "Not tapped yet." }
CompositionLocalProvider(key provides "Tapped ${state.intValue} times.") {
Text(key.current)
}
Button(onClick = { state.intValue += 1 }) {
Text("Tap")
}
}
}
How does Compose know to recompose the lambda passed to CompositionLocalProvider?shikasd
06/23/2026, 7:47 PMprovidersInvalid flag in Composer)min
06/24/2026, 10:53 AMstartProvider call conditionally leaves the Composer in the state where it runs the body of the content: @Composable () -> Unit argument which it’s also injected into.
@Composable
@OptIn(InternalComposeApi::class)
@NonSkippableComposable
public fun CompositionLocalProvider(value: ProvidedValue<*>, content: @Composable () -> Unit) {
currentComposer.startProvider(value)
content()
currentComposer.endProvider()
}
And unless a local by staticCompositionLocalOf has been remapped to a different value, the execution of the content is skipped.
override fun startProvider(value: ProvidedValue<*>) {
// ...
providersInvalid = invalid
providerCache = providers
start(compositionLocalMapKey, compositionLocalMap, GroupKind.Group, providers)
}
But I can’t anything in the start method that skips the content unless the local has been remapped. Could you point me to the code please?min
06/24/2026, 11:28 AMshikasd
06/24/2026, 11:31 AMmin
06/24/2026, 11:31 AMshikasd
06/24/2026, 11:32 AMmin
06/24/2026, 12:33 PMCompositionLocalProvider?shikasd
06/24/2026, 12:46 PMChuck Jazdzewski [G]
06/24/2026, 3:55 PMskipping state. If skipping is false the function will not be skipped regardless of which parameters are passed to it. When a static composition local is changed then skipping will return false for all children of the CompositionLocalProvider call. The recompose states are not invalidated as there is no need to. They will execute regardless as skipping is disabled until CompositionLocalProvider returns (technically, until endProvider() is called).min
06/25/2026, 10:32 AMval untracked = staticCompositionLocalOf { 0 }
CompositionLocalProvider(untracked provides state.intValue) {
Log.d("LOCALDEMO", "provider where `untracked provides state.intValue`")
}
This becomes…
ProvidableCompositionLocal untracked = CompositionLocalKt.staticCompositionLocalOf((Function0) objRememberedValue4);
CompositionLocalKt.CompositionLocalProvider(
(ProvidedValue<?>) untracked.provides(Integer.valueOf(state.getIntValue())),
ComposableSingletons$MainActivityKt.INSTANCE.m9057getLambda$441102124$app(),
$composer2,
ProvidedValue.$stable | 48,
);
Where the m9057getLambda$441102124$app() returns…
/* JADX INFO: renamed from: lambda$-441102124, reason: not valid java name */
private static Function2<Composer, Integer, Unit> f149lambda$441102124 = ComposableLambdaKt.composableLambdaInstance(-441102124, false, new Function2() { // from class: com.example.composedemo.ComposableSingletons$MainActivityKt$$ExternalSyntheticLambda5
@Override // kotlin.jvm.functions.Function2
public final Object invoke(Object obj, Object obj2) {
return ComposableSingletons$MainActivityKt.lambda__441102124$lambda$5((Composer) obj, ((Integer) obj2).intValue());
}
});
Which, on invoke, calls…
static final Unit lambda__441102124$lambda$5(Composer $composer, int $changed) {
ComposerKt.sourceInformation($composer, "C:MainActivity.kt#ffoge4");
if (!$composer.shouldExecute(($changed & 3) != 2, $changed & 1)) {
$composer.skipToGroupEnd();
} else {
if (ComposerKt.isTraceInProgress()) {
ComposerKt.traceEventStart(-441102124, $changed, -1, "com.example.composedemo.ComposableSingletons$MainActivityKt.lambda$-441102124.<anonymous> (MainActivity.kt:65)");
}
Log.d("LOCALDEMO", "provider where `untracked provides state.intValue`");
if (ComposerKt.isTraceInProgress()) {
ComposerKt.traceEventEnd();
}
}
return Unit.INSTANCE;
}
So as part of transforming every @Composable at compile time, the provider content: @Composable () -> Unit is wrapped in a conditional shouldExecute check. The check seems to come down to parametersChanged || !skipping where…
override val skipping: Boolean
get() {
return !inserting &&
!reusing &&
!providersInvalid &&
currentRecomposeScope?.requiresRecompose == false &&
!forciblyRecompose
}
Of which providersInvalid is set in the composer start{Provider, Providers} methods. Is this correct?Chuck Jazdzewski [G]
06/25/2026, 3:52 PMshouldExecuted to determine whether it should execute its content or skip. Non-skippable functions (e.g. inline composable functions or function explicitly marked non-skippable with @NonSkippableComposable) do not; they never skip.
Changing the value of a static composition local will cause the map storing the composition locals to change. When the map changes providersInvalid is set to true until the corresponding end is called (i.e. the function returns). While providersInvalid is true shouldExecute will return true.
This means that changing the value of a static composition local is possible but is costly. The intent is that static composition locals never (or nearly never) change. However, if a value changes rarely then using a static local may be less costly overall than a dynamic local as the cost of tracking reads of a dynamic local has a cost too which statics do not pay.min
06/26/2026, 12:27 PM@Composable at compile time
◦ A Kotlin compiler plugin; a compile time dependency that manipulates the IR
◦ A generated `Composer` parameter reads from and writes to a tree in a Composition
◦ Unless inline or opted out of, the body mapped if Unit to a ‘recompose scope’
• The Compose runtime is a framework used by code generated by the Compose compiler
◦ A Composition is to broadcast its evolution for Applier implementors to derive from
◦ Drives compositions in a CompositionContext tree rooted at a Recomposer node
◦ Composes in a snapshot; observers record reads & writes by recompose scopes
◦ remember computes + caches → reads while valid at the current Composer position
• ComponentActivity::setContent populates + sets the activity content to a ComposeView
◦ A Composition in an AndroidComposeView child to derive a `LayoutNode` tree from
◦ Recomposed if invalidated as driven by a Recomposer on global snapshot advance
◦ The global snapshot advanced in a suspending loop on resume if writes observed
◦ The derived tree to draw measures nodes and places their children postorder on layout
• A CompositionLocal<T> is a key to map to and by which to look up T values
◦ Abstract; subtype ProvidableCompositionLocal<T> instantiated by factories
◦ A mapping brought into scope by a CompositionLocalProvider for its subtree
◦ .provides(T): ProvidedValue<T> and the like on the subtype return a mapping
◦ .current: T by a @Composable getter that searches up the composition
• A CompositionLocalProvider remembers its mappings to compare to args on recompose
◦ The content: @Composable () -> Unit recompose scope checks to skip if identical
◦ Updated in place if by `compositionLocalOf`; backed by a snapshot state to write to
◦ Replaced if by staticCompositionLocalOf which sets a `Composer` flag not to skip
Thank you for helping me put it together! Please let me know if I’ve got anything wrongmin
06/27/2026, 6:54 AM@NonSkippableComposable) do not; they never skip.
CompositionLocalProvider(tracked provides state.intValue) @NonRestartableComposable {
Log.d("LOCALDEMO", "provider where `tracked provides state.intValue`; not restartable")
}
CompositionLocalProvider(tracked provides state.intValue) {
Log.d("LOCALDEMO", "provider where `tracked provides state.intValue`; restartable")
}
CompositionLocalProvider(untracked provides state.intValue) {
Log.d("LOCALDEMO", "provider where `untracked provides state.intValue`")
}
I expected the first and last providers to recompose their contents every time, but only the last one does:
--------- beginning of main
--------- beginning of system
LOCALDEMO com.example.composedemo D provider where `tracked provides state.intValue`; not restartable
LOCALDEMO com.example.composedemo D provider where `tracked provides state.intValue`; restartable
LOCALDEMO com.example.composedemo D provider where `untracked provides state.intValue`
LOCALDEMO com.example.composedemo D provider where `untracked provides state.intValue`
LOCALDEMO com.example.composedemo D provider where `untracked provides state.intValue`
LOCALDEMO com.example.composedemo D provider where `untracked provides state.intValue`
The decompiled bytecode shows that the @NonRestartableComposable annotation has had no effect:
static final Unit lambda_1641123563$lambda$4(Composer $composer, int $changed) {
ComposerKt.sourceInformation($composer, "C:MainActivity.kt#ffoge4");
if (!$composer.shouldExecute(($changed & 3) != 2, $changed & 1)) {
$composer.skipToGroupEnd();
} else {
if (ComposerKt.isTraceInProgress()) {
ComposerKt.traceEventStart(1641123563, $changed, -1, "com.example.composedemo.ComposableSingletons$MainActivityKt.lambda$1641123563.<anonymous> (MainActivity.kt:63)");
}
Log.d("LOCALDEMO", "provider where `tracked provides state.intValue`; not restartable");
if (ComposerKt.isTraceInProgress()) {
ComposerKt.traceEventEnd();
}
}
return Unit.INSTANCE;
}
Is this a bug?
CompositionLocalProvider(tracked provides state.intValue,
@Composable @NonRestartableComposable fun () {
Log.d("LOCALDEMO", "provider where `tracked provides state.intValue`; NOT restartable")
}
)
CompositionLocalProvider(tracked provides state.intValue) {
Log.d("LOCALDEMO", "provider where `tracked provides state.intValue`; restartable")
}
CompositionLocalProvider(untracked provides state.intValue) {
Log.d("LOCALDEMO", "provider where `untracked provides state.intValue`")
}
I suspect that it is, because replacing the {} lambda with an anonymous fun one achieves the expected behaviour:
LOCALDEMO com.example.composedemo D provider where `tracked provides state.intValue`; NOT restartable
LOCALDEMO com.example.composedemo D provider where `tracked provides state.intValue`; restartable
LOCALDEMO com.example.composedemo D provider where `untracked provides state.intValue`
LOCALDEMO com.example.composedemo D provider where `tracked provides state.intValue`; NOT restartable
LOCALDEMO com.example.composedemo D provider where `untracked provides state.intValue`
LOCALDEMO com.example.composedemo D provider where `tracked provides state.intValue`; NOT restartable
LOCALDEMO com.example.composedemo D provider where `untracked provides state.intValue`
The @NonRestartableComposable runs every time now, and the bytecode also forgoes the shouldExecute check as expected:
static final Unit lambda__522512317$lambda$4(Composer $composer, int $changed) {
ComposerKt.sourceInformationMarkerStart($composer, -522512317, "C(<no name provided>):MainActivity.kt#ffoge4");
if (ComposerKt.isTraceInProgress()) {
ComposerKt.traceEventStart(-522512317, $changed, -1, "com.example.composedemo.ComposableSingletons$MainActivityKt.lambda$-522512317.<no name provided> (MainActivity.kt:63)");
}
Log.d("LOCALDEMO", "provider where `tracked provides state.intValue`; NOT restartable");
if (ComposerKt.isTraceInProgress()) {
ComposerKt.traceEventEnd();
}
ComposerKt.sourceInformationMarkerEnd($composer);
return Unit.INSTANCE;
}
The documentation for the annotation doesn’t say it doesn’t work and isn’t to be used on {} lambdas either:
> This annotation can be applied to Composable functions in order to prevent code from being generated which allow this function's execution to be skipped or restarted. This may be desirable for small functions which just directly call another composable function and have very little machinery in them directly, and are unlikely to be invalidated themselves.shikasd
06/27/2026, 8:00 PMmin
06/28/2026, 8:13 AMshikasd
06/28/2026, 8:57 AM