Documentation
How It Works
When you save a Kotlin file in your IDE, HotSwan compiles only the changed code, extracts the modified classes, and applies them to the running app on your device. The app swaps the updated classes directly in memory, and Compose recomposes the UI to reflect your changes instantly.
This page walks through each step of the pipeline, from file detection to UI update, so you understand what happens under the hood every time you hit save.
Pipeline overview
Each step is designed to minimize the amount of work. Rather than recompiling your entire project and reinstalling the APK, HotSwan targets only the classes that actually changed. The first reload after a code change takes a few seconds due to incremental compilation, but subsequent reloads are often much faster. On large projects where a full build can take tens of minutes, this difference is transformative.
File detection and module resolution
HotSwan listens for file save events in your IDE. When you save a .kt or resource file, the plugin identifies which Gradle module the file belongs to by matching the file path against your project's module structure.
This module resolution step is important because multi module projects may have dozens of Gradle modules. HotSwan only runs the incremental build on the module that contains the changed file, not the entire project. If you edit a file in :feature:home, only that module gets compiled.
For resource files (res/ directory), HotSwan takes a different path. Instead of compiling Kotlin, it runs processDebugResources to produce a compiled resource APK, which is then applied to the device for live patching.
Incremental Kotlin compilation
HotSwan invokes Gradle's incremental build tasks on the target module. Only the files that actually changed (and their dependents) get recompiled.
During compilation, the HotSwan compiler plugin applies optimizations to your code:
- Independent compilation: Each function body is compiled independently, so changing one function does not affect its siblings. This minimizes the scope of each change.
- State preservation: The compiler analyzes changed code and ensures that only the affected scopes are recomposed. All existing state is preserved across reloads, even when composables are reordered.
- Parameter refresh: After a hot reload, the runtime re-evaluates all parameters to ensure everything reflects the latest code.
The result of this step is compiled output containing all classes from the module, ready for change extraction.
Change extraction
The compiler produces output for every class in the module, not just the ones you touched. HotSwan compares that output against the baseline of what the running app already has and keeps only the declarations that actually differ.
The comparison is against the build the device is running, not against your last edit, which is what keeps the two sides in agreement. A reload that carried a stale idea of the installed app would apply code against a shape that is no longer there, and that is the failure this step exists to prevent.
Filtering unchanged classes
After extraction, HotSwan compares each class against a baseline captured from the installed APK. Only classes whose bytecode actually differs from the baseline are applied to the device.
This comparison also performs a structural pre-check, and it is narrower than it sounds. What HotSwan checks at build time is a class's supertype and the interfaces it implements. Changing either one on a class that existed in the previous build cannot be applied in place, so HotSwan detects it while building and relaunches the app for you. Brand new classes and deleted classes never trigger it.
Compiler-generated classes also receive special handling. If your code changes cause the compiler to renumber generated classes, HotSwan detects the renumbering and falls back to a full build rather than sending mismatched classes.
The interpreter engine
This is the step that makes HotSwan 2 different from every version before it, and from hot reload as the platform defines it.
The traditional approach is to hand the runtime a new version of a class that is already loaded and ask it to accept the swap. Runtimes accept very little that way. A change inside the body of a method is usually fine. Anything that changes the shape of the code is not, which is why hot reload has historically meant editing values.
HotSwan does not ask. It carries its own interpreter into the running app, and the new code is executed rather than installed. When you save, the changed declarations are sent to the app, and the interpreter runs them in place of the compiled bodies they replace. The class the virtual machine loaded at startup is never redefined, because it is no longer the thing deciding what runs.
Three consequences follow, and they are the whole product:
- A structural edit is routed around the runtime instead of being forced through it, so adding composables, changing branching, and replacing whole screens all apply in place
- The app is the same process it was a moment ago, so the navigation stack, scroll position, and
remember{}values survive - The engine is HotSwan's own rather than the platform's, so the part that decides what runs did not have to be written three times for Android, Desktop, and iOS
The app holds a baseline of its own compiled code, written at build time, so the engine knows what the installed build contains and can tell a changed declaration from an unchanged one. Delivery differs per platform, since a JVM and a Kotlin/Native binary do not accept code the same way, but what happens after delivery is the same decision in all three.
Compose recomposition
After all classes have been swapped, HotSwan triggers a Compose recomposition so the UI reflects your changes. This is where Jetpack Compose's architecture becomes a major advantage.
The Compose runtime tracks your UI as a tree of composable groups. Each composable function call creates a group in the runtime's slot table, a data structure that stores the current state, parameters, and child compositions for every composable in your app. When HotSwan swaps a class, the runtime already knows exactly which scopes are affected and can recompose only those scopes, rather than rebuilding the entire UI tree.
HotSwan uses a three tier invalidation chain to trigger recomposition, trying the least disruptive approach first:
- Targeted recomposition (primary): Invalidates only the composable scopes affected by the change. The slot table remains intact, so all state is preserved:
remember{}values, scroll position, navigation stack, and every composable group's identity. - Composition reset (fallback): Disposes and recreates all compositions with fresh slot tables. Preserves
rememberSaveable{}, ViewModel instances, and navigation, but resets plainremember{}values. - Restart Activity (last resort): Recreates the entire Activity. Preserves SavedInstanceState and ViewModel instances but resets everything else.
In practice, targeted recomposition succeeds for the vast majority of changes. Because Compose distinguishes each composable by its group key in the slot table, HotSwan can swap code and trigger recomposition without losing the identity or state of any composable in the tree. You rarely lose any state during a hot reload.
Timing breakdown
| Step | Typical duration |
|---|---|
| File detection | Instant |
| Incremental Kotlin + D8 | First: 3-5s / After: 100ms-1s |
| Change extraction and filtering | < 100ms |
| Runtime class swap | < 50ms |
| Compose recomposition | < 100ms |
The bottleneck is Gradle compilation. Everything after compilation adds less than 300ms to the total cycle. As your project's incremental build performance improves (through better modularization, build cache hits, or faster hardware), HotSwan's reload time improves proportionally.
Why the first reload is slower
You may notice that the very first hot reload after launching your app takes noticeably longer than subsequent ones. This is because the Gradle daemon needs to warm up before it can compile efficiently.
On the first reload, the Gradle daemon performs JVM initialization, resolves dependencies, and constructs the full task graph for your project. The Kotlin compiler and D8 also start cold, without any JIT optimizations. All of this adds overhead to the first compilation cycle.
From the second reload onward, the Gradle daemon is already running and warmed up. The JVM's JIT compiler has optimized the hot paths, the task graph is cached, and incremental compilation can fully leverage previous build results. This is why subsequent reloads feel significantly faster.
The difference is entirely about Gradle daemon warmth, not about HotSwan itself. As long as the daemon stays alive (which it does throughout your IDE session), every reload after the first one benefits from the warm cache.
Ready to start? Install Compose HotSwan and see hot reload in action, or explore what you can hot reload.