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 a compiled DEX file containing all classes from the module, ready for change extraction.
Change extraction
The compiled DEX file from Gradle contains every class in the module, not just the ones you changed. HotSwan extracts only the individual classes that changed, isolating them for transfer.
HotSwan uses the finalized build output that matches what is installed on the device. This ensures class definitions are consistent between the IDE and the running app, avoiding mismatch errors during the swap.
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. If a class has changed its method signatures or fields (its "shape"), the runtime cannot swap it in place. HotSwan detects this and automatically triggers a full incremental build and reinstall instead.
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.
Runtime class swap
Changed classes are applied from the IDE to the app through a direct device connection. HotSwan establishes a link between the IDE plugin and the client library running inside your app. The transfer is lightweight. Each changed class is applied individually, and for resource reloads, the compiled resource APK is applied as well.
The HotSwan client library includes a native agent that runs inside your app. When your app starts in debug mode, the agent is loaded into the Android Runtime. This is why Android API 28 or higher is required.
When the agent receives updated classes, it swaps the class definitions directly in the running app's memory. The key property of this operation is that existing object instances are not affected: they automatically use the new method bodies on the next call. No new objects are created, no state is lost.
For newly generated classes that do not yet exist in the running app, the agent defines them from scratch so they are immediately available. The swapped code runs as native ART bytecode, identical in performance to a normal build. There is no interpreter layer or behavioral divergence. Your hot reloaded code behaves exactly like production code.
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.
JetBrains Desktop Hot Reload
JetBrains has built a hot reload solution for Compose Desktop (and Compose for Web) that takes a structurally different approach from HotSwan. While HotSwan targets Android and works through runtime class swapping on a real device, the JetBrains implementation operates in a JVM environment where the constraints and trade-offs are different.
If you are interested in the foundations of hot reload and want to understand how different platforms tackle the same problem, the JetBrains team published a detailed write-up of their journey: The Journey to Compose Hot Reload 1.0.0. It covers the technical challenges they faced and the design decisions behind their implementation. Worth reading if you want a deeper understanding of what makes hot reload work across different Compose targets.