preserving-state-across-reloads — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited preserving-state-across-reloads (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Compose HotSwan applies every hot reload through three escalating tiers. Tier 1 (targeted recomposition) preserves the most: scroll position, navigation back stack, remember and rememberSaveable values, ViewModel state, lazy layout items, dialog and bottom sheet state. Tier 2 (composition reset) disposes and recreates compositions, dropping per-composable remember. Tier 3 (Activity.recreate()) restarts the Activity and only state held by ViewModel or SavedInstanceState survives.
The iteration loop is fastest and least surprising when every reload stays inside tier 1. This skill covers which edits trigger which tier, which state holders survive each tier, and how to hoist transient UI state when an edit must escalate.
Activity.recreate() versus a composition reset.../understanding-hot-reload-limits/SKILL.md.../setting-up-compose-hotswan/SKILL.md.../iterating-with-ai-and-mcp/SKILL.md.../../recomposition/debugging-recompositions/SKILL.md first.WATCHING against the running app. Setup lives in ../setting-up-compose-hotswan/SKILL.md.../../recomposition/debugging-recompositions/SKILL.md for the underlying mechanics.rememberSaveable for state that must survive process death and (in this context) composition reset.| Tier | Mechanism | Preserves | Loses | Triggered when |
|---|---|---|---|---|
| 1. Targeted recomposition | recomposes only the affected scopes in place | navigation back stack, scroll position, remember, rememberSaveable, ViewModel, lazy layout items, dialog and bottom sheet state | nothing | simple body change inside one composable scope |
| 2. Composition reset | dispose and recreate all compositions from scratch | Activity, ViewModel, navigation (via NavController), rememberSaveable (depends on retention) | per-composable remember values not retained by a Saveable, scroll position not held by a saveable state holder | tier 1 unavailable (theme change, root-scope structural change) |
3. Activity.recreate() | recreate the entire Activity | ViewModel, SavedInstanceState | scroll, transient dialog state, anything not saved | composition fails or schema mismatch detected |
The tier that ran is reported in the HotSwan tool window status after every reload. Read it after each edit to confirm the loop stayed where it was supposed to.
The HotSwan tool window prints the tier (1, 2, or 3) for each reload. If the developer expected tier 1 and the status reports tier 2 or 3, the edit touched a wider scope than intended. Read the tier before deciding whether the lost state is a configuration problem or expected behaviour.
Tier 2 disposes per-composable remember blocks. Walk the composable that lost state and convert its local remember to rememberSaveable for any value that the developer wants to keep across reloads that may escalate. This is the single highest-leverage change for a HotSwan-driven iteration loop.
ViewModel for long iterationsWhen the developer is iterating on a screen for a long stretch and individual edits keep escalating to tier 2 or tier 3, hoist transient UI state (selected tab, expanded item, scroll position, dialog open) into a ViewModel. ViewModel survives all three tiers, so the iteration loop never loses the workbench state.
CompositionLocal mutations during a fast loopEditing a value used by MaterialTheme, or by any staticCompositionLocalOf, invalidates the root content lambda of the corresponding CompositionLocalProvider. HotSwan cannot scope that to a single recomposition target and escalates to tier 2. Move the colour, dimension, or typography under iteration into a local override on the composable being tuned, then move it back into the theme once the value is final.
staticCompositionLocalOf for any value the developer is editingEven outside theme, any value provided through staticCompositionLocalOf invalidates the entire content of the provider on change. Use compositionLocalOf (which tracks reads) for values that may change during a hot-reload session.
remember lost on tier 2// WRONG (for tier 2 reloads)
@Composable
fun TabScreen() {
var selected by remember { mutableIntStateOf(0) }
Tabs(selected = selected, onSelected = { selected = it })
}
// WRONG because: a tier 2 composition reset disposes remember; the selected tab snaps back to 0 after a reload that escalates.// RIGHT
@Composable
fun TabScreen() {
var selected by rememberSaveable { mutableIntStateOf(0) }
Tabs(selected = selected, onSelected = { selected = it })
}// WRONG (forces tier 2)
val LightColors = lightColorScheme(primary = Color(0xFFEE0044))
// WRONG because: editing a top-level colour used by MaterialTheme invalidates the root CompositionLocal; the reload escalates to tier 2 and disposes per-composable remember.// RIGHT (stays in tier 1)
@Composable
fun PrimaryButton(text: String) {
Button(
onClick = {},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEE0044)),
) { Text(text) }
}Move the literal back into the theme once the visual value is final. The escalation only matters during the fast iteration loop.
// RIGHT
@Composable
fun Feed(items: List<Item>) {
val state = rememberLazyListState()
LazyColumn(state = state) {
items(items, key = { it.id }) { Item(it) }
}
}rememberLazyListState is backed by a Saveable, so the scroll position survives composition reset and Activity recreation. Combined with stable keys, the visible items stay rendered after a reload that escalates.
ViewModel// RIGHT
class FeedViewModel : ViewModel() {
private val _uiState = MutableStateFlow(FeedState())
val uiState: StateFlow<FeedState> = _uiState.asStateFlow()
}
@Composable
fun FeedScreen(viewModel: FeedViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
Feed(state.items)
}ViewModel outlives every tier of HotSwan reload (and configuration changes generally), so screen-level workbench state stays untouched even on tier 3.
staticCompositionLocalOf for values under iteration// WRONG (every edit forces tier 2)
val LocalAccent = staticCompositionLocalOf { Color.Red }
// WRONG because: changing the provided value invalidates the entire content of CompositionLocalProvider; HotSwan cannot scope that and escalates.// RIGHT
val LocalAccent = compositionLocalOf { Color.Red }compositionLocalOf tracks reads and only invalidates the actual readers; HotSwan can keep the reload inside tier 1.
rememberSaveable over remember for any UI state the developer wants to keep across hot reloads that may escalate.CompositionLocal edits out of a fast iteration session; they force tier 2.staticCompositionLocalOf for values the developer is actively editing during a hot reload session.ViewModel when the iteration loop is long.rememberLazyListState (and the matching grid / pager state holders) for scroll and visible-item state because they are saveable by default.MaterialTheme colour escalates to tier 2 (status confirms the tier)rememberSaveable survives a tier 2 composition resetViewModel survives a tier 3 Activity.recreate()rememberLazyListState keeps scroll position after a tier 2 reload/docs/state-preservation)rememberSaveable: https://developer.android.com/develop/ui/compose/state-savingViewModel overview: https://developer.android.com/topic/libraries/architecture/viewmodelCompositionLocal: https://developer.android.com/develop/ui/compose/compositionlocal~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.