Documentation
State Preservation
One of the biggest frustrations with full rebuilds is losing your app state: the screen you navigated to, the form you were filling out, the scroll position in a long list. HotSwan preserves your state across reloads through targeted recomposition that tries the least disruptive approach first.
This page explains how each strategy works, what state survives at each level, and how you can structure your code to maximize preservation.
Recomposition strategy
After the runtime swaps the changed classes in memory, HotSwan needs to trigger a Compose recomposition so the UI picks up the new code. The question is how to trigger recomposition in a way that preserves as much state as possible.
HotSwan uses a three tier strategy. It tries the first tier, and only falls back to the next tier if the previous one is unavailable or fails:
- Targeted recomposition: recomposes only the affected composable scopes in place.
- Composition reset: disposes and recreates all compositions from scratch.
- Restart Activity: recreates the entire Activity.
In practice, targeted recomposition succeeds for the vast majority of changes. You will rarely see the fallback tiers trigger during normal development.
Primary: Targeted recomposition
HotSwan identifies which composable scopes are affected by the code change and invalidates only those scopes. On the next frame, the Compose runtime re-executes the affected composable functions with the new code. The key property is that compositions are not disposed or recreated. The slot table remains intact.
Because the slot table is untouched, all state stored in it survives: plain remember{} values, rememberSaveable{} values, navigation state, scroll position, lazy layout items, and dialog or bottom sheet state. Nothing is reset.
This works even when composables are reordered. The compiler plugin ensures that each composable's state is tracked by its identity rather than its position in the source code, so the runtime preserves state correctly regardless of ordering.
When a group's shape changed: scoped replace
Targeted recomposition assumes the shape of the composition is the same and only the code inside it moved. Some edits change the shape, for example wrapping a block of UI in a new container. Re-running the old scopes would then walk a slot table that no longer matches.
For those, HotSwan replaces the affected groups rather than re-running them. Compose disposes each matched group and builds it again, so remember{} values inside those groups start fresh. Everything outside them is untouched, which is the point of doing it by group instead of by composition.
State that does not live in the slot table is unaffected. rememberSaveable{} is restored from the registry that holds it, a ViewModel is scoped to its store owner rather than to a group, and your navigation back stack is held above the screen being replaced.
Last resort: process restart
One edit cannot be applied in place at all: changing the superclass of an already instrumented class, or the interfaces it implements. Instances of the old shape already exist, and no amount of scoping makes them compatible with the new one.
HotSwan restarts the app and your edit is there when it comes back. This is a genuine process restart, not an Activity.recreate(). HotSwan deliberately refuses the in process version, because recreating an Activity inside a process that still holds the old class definitions is how you get an app that is half updated and hard to reason about.
A restart is a cold start, so treat in memory state as gone. It is still a different order of cost from the alternative: your app comes back with the change already applied, with no rebuild and no reinstall.
What survives each level
| State type | Targeted | Group replace | Process restart |
|---|---|---|---|
| remember{} | |||
| rememberSaveable{} | |||
| ViewModel | |||
| Navigation back stack | |||
| Scroll position | |||
| Lazy layout items | |||
| Dialog state |
Tips for maximizing state preservation
Since targeted recomposition is the primary path and preserves all state, most of the time you do not need to worry about state loss. But if you want to be defensive:
- Use rememberSaveable instead of remember for state that matters. It survives all three tiers.
- Keep important state in ViewModels. ViewModel instances survive all tiers, so state stored there is always preserved.
- Avoid structural changes during iteration. Adding new functions or changing data classes triggers a full build, which restarts the app. Save structural changes for when you are done iterating on the UI.