fix: propagate composable menu DSL state changes to the native menu - #435
Merged
Conversation
rememberRecordedMenuContent recorded the user's @composable menu lambda into a plain mutableListOf. When state read inside the lambda changed, Compose restarted only the lambda's own recompose scope: it re-recorded into the abandoned RecordingComposableScope while the outer function (fingerprint, replay block, the underlying Tray's LaunchedEffect) never re-ran, so label/enabled/checked changes never reached the native menu unless callers disposed the tray via key(). Backing the recorded ops with mutableStateListOf makes the lambda-only restart write snapshot state that the Tray wrapper reads via snapshot(); the wrapper is invalidated, re-records the whole menu with a fresh scope, computes a new fingerprint, and the native menu rebuilds. Verified with a timed repro on Windows: each state flip now produces a menu update with the new label, with no recomposition loop. Fixes #434
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #434
Root cause
The report blamed the label pipeline, but the plain
TrayMenuBuilderpath is fine — the bug is specific to the composable-DSL overloads (ComposableTray.kt), which is what trailing-lambda callers actually resolve to.rememberRecordedMenuContentinvokes the user's@Composable ComposableTrayMenuScope.() -> Unitlambda and records it into a plainmutableListOf. When state read inside that lambda changes (e.g.label = if (dashboardOpen) … else …), Compose restarts only the lambda's own recompose scope: the DSL re-records into the old, abandonedRecordingComposableScope, while the outer function —snapshot(),structuralFingerprint(),remember(fingerprint), and the underlyingTray'sLaunchedEffect— never re-runs. The native menu keeps the stale text until something else forces a full rebuild, which is exactly thekey(...)workaround from the issue.Diagnosed with a timed repro (label flip every 2 s + stack traces): the menu lambda re-ran on every flip via
RecomposeScopeImpl.compose(lambda-only restart), whileTraynever recomposed and no native update was queued.Fix
Back the recorded ops with
mutableStateListOf. Now a lambda-only restart writes snapshot state that the Tray wrapper reads throughsnapshot(), so the wrapper is invalidated, re-records the whole menu with a fresh scope, computes a new fingerprint, and the native menu rebuilds. Submenus chain the same way (child list read by the parent scope).One line of behavior, verified on Windows 11 with the issue's repro shape:
menuHash→LaunchedEffectrelaunch →WindowsTrayManagerprocesses the updatelabel,isEnabled, andcheckedalike (all in the fingerprint)Testing
compileKotlinJvm(lib, tray-app, demo),ktlintCheck,detekt,jvmTestall pass.