Android & Kotlin Technical Articles

Detailed articles on Android development, Jetpack Compose internals, Kotlin coroutines, and open source library design by skydoves, Google Developer Expert and maintainer of Android libraries with 40M+ annual downloads. Read practical guides on Retrofit, Compose Preview, BottomSheet UI, coroutine compilation, and more.

Exclusive Articles
RSS

This is a collection of private or subscriber-first articles written by the Dove Letter, skydoves (Jaewoong). These articles can be released somewhere like Medium in the future, but always they will be revealed for Dove Letter members first.

Compose Hot Reload: Make changes to your UI code in a Compose Multiplatform application, and see the results in real time

Table of Contents 1. Introductionintroduction 2. Architecture Overviewarchitecture-overview 3. Core Modulescore-modules - hot-reload-agenthot-reload-agent-the-java-instrumentation-agent - hot-reload-orchestrationhot-reload-orchestration-communication-backbone - hot-reload-runtime-jvmhot-reload-runtime-jvm-runtime-integration - hot-reload-gradle-pluginhot-reload-gradle-plugin-build-integration - hot-reload-corehot-reload-core-shared-utilities - hot-reload-analysishot-reload-analysis-bytecode-analysis 4. The Hot Reload Flowthe-hot-reload-flow 5. Communication Protocolcommunication-protocol 6. State Managementstate-management 7. Static Field Re-initializationstatic-field-re-initialization 8. Compose Integrationcompose-integration 9. Window Managementwindow-management 10. Advanced Topicsadvanced-topics Introduction When developing user interfaces, the traditional cycle of making a change, recompiling, restarting the application, and navigating back to the state you were testing can be frustrating. Each iteration can take tens of seconds, breaking your flow and making it harder to experiment with different designs. Compose Hot Reload addresses this problem by enabling real-time UI updates in Compose Multiplatform applications without requiring a full restart. The system works by combining the JVM's HotSwap capabilities with Compose's recomposition model. When you save a file, the changes propagate to your running application within a second or two, and you can see the updated UI immediately while preserving the application's current state. This means you don't lose your place in a multi-step workflow or need to manually recreate the conditions you were testing. The implementation involves multiple layers working together. A Java agent instruments the application's bytecode and handles class redefinition at runtime. An orchestration protocol coordinates communication between the build system and the running application. The runtime integration provides Compose-aware UI updates that only recompose the parts of your interface that actually changed. Finally, a Gradle plugin integrates everything into the build system so you can use hot reload without complicated configuration. What Hot Reload Can Do Hot reload supports instant UI updates when you modify composable functions, change layout logic, or update visual properties. The system preserves your application's state across reloads, so if you're testing a form with several fields filled in, those values remain after the code updates. When classes change, the system performs selective invalidation of Compose groups, recomposing only the affected parts of the UI rather than rebuilding everything. It can also re-initialize static fields when their definitions change, which is important for singleton objects and global configuration. The window state persists across reloads and even across restarts, so your window doesn't jump to a different position every time you test a change. All of this works across multiple processes, with the build system, IDE, and application coordinating through a shared orchestration layer. Architecture Overview The hot reload system is built from several interconnected modules, each handling a specific part of the process. Understanding how these modules work together helps explain both what hot reload can do and what its limitations are. ┌──────────────────────────────────────────────────────────┐ │ Developer's IDE │ │ │ │ Source Code ── Kotlin Compiler ── .class files │ └────────────────────────┬─────────────────────────────────┘ │ │ File System Watch ▼ ┌──────────────────────────────────────────────────────────┐ │ Gradle Plugin │ │ │ │ • ComposeHotSnapshotTask detect changes │ │ • ComposeHotReloadTask send reload request │ │ • ComposeHotRun launch with agent │ └────────────────────────┬─────────────────────────────────┘ │ │ TCP Socket Binary Protocol ▼ ┌──────────────────────────────────────────────────────────┐ │ Orchestration Server │ │ │ │ • Message Broadcasting │ │ • State Management │ │ • Client Coordination │ └────────────────────────┬─────────────────────────────────┘ │ │ ReloadClassesRequest ▼ ┌──────────────────────────────────────────────────────────┐ │ Java Agent │ │ │ │ • Bytecode Transformation │ │ • Class Redefinition │ │ • Static Re-initialization │ │ • Compose Group Invalidation │ └────────────────────────┬─────────────────────────────────┘ │ │ Instrumentation API ▼ ┌──────────────────────────────────────────────────────────┐ │ Runtime JVM │ │ │ │ • DevelopmentEntryPoint │ │ • HotReloadState Management │ │ • Composition Reset │ │ • UI Re-rendering │ └──────────────────────────────────────────────────────────┘ The flow starts in your IDE, where the Kotlin compiler transforms your source code into bytecode. The Gradle plugin watches for these changes and creates snapshots of what actually changed between compilations. When changes are detected, the plugin sends a reload request through the orchestration server, which acts as a message broker between different components. The Java agent receives this request and performs the actual class redefinition using the JVM's instrumentation API. Finally, the runtime integration updates the Compose UI by triggering recomposition of the affected parts. This architecture keeps the concerns separated. The build system handles compilation and change detection. The orchestration layer handles communication without either side needing to know the implementation details of the other. The agent handles the low-level bytecode manipulation. The runtime handles the high-level UI updates. This separation means you can understand and modify each piece independently. Core Modules hot-reload-agent: The Java Instrumentation Agent The agent runs inside your application's JVM process and handles the actual class redefinition. It's loaded at startup through the Java agent mechanism, which gives it special privileges to intercept and modify class loading. The agent is the most complex part of the system because it has to handle bytecode transformation, track class loaders, coordinate with the Compose runtime, and manage all the edge cases that come with redefining classes at runtime. Entry Point When your application starts with hot reload enabled, the JVM calls the agent's premain function before your application's main method runs. This function initializes all the subsystems the agent needs: kotlin // hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/agent.kt @file:JvmName"ComposeHotReloadAgent" fun premain@Suppress"unused" args: String?, instrumentation: Instrumentation { startDevTools startOrchestration createPidfile startWritingLogs launchWindowInstrumentationinstrumentation launchComposeInstrumentationinstrumentation launchRuntimeTrackinginstrumentation launchReloadRequestHandlerinstrumentation launchJdwpTrackerinstrumentation } The agent first starts any development tools that might be available, then establishes a connection to the orchestration server. It creates a pidfile that contains information about how to connect to this application instance, which the Gradle plugin uses later to send reload requests. Log writing starts so you can debug issues when they occur. Then the agent registers several class file transformers with the JVM's instrumentation API. Each transformer intercepts class loading for a specific purpose. Window instrumentation injects code to wrap your UI in the development entry point. Compose instrumentation enables hot reload mode in the Compose runtime. Runtime tracking builds a global view of all loaded classes and their relationships. The reload request handler listens for incoming reload requests and processes them. JDWP tracking monitors whether a debugger is attached, which can affect how hot reload behaves. Key Components RuntimeTrackingTransformer Every time the JVM loads a class, the runtime tracking transformer gets a chance to inspect it. The transformer builds a comprehensive map of what classes exist in your application and what Compose groups they contain: kotlin // hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/runtimeTracking.kt internal fun launchRuntimeTrackinginstrumentation: Instrumentation { val transformer = RuntimeTrackingTransformer instrumentation.addTransformertransformer, false } private class RuntimeTrackingTransformer : ClassFileTransformer { override fun transform loader: ClassLoader?, className: String?, classBeingRedefined: Class<?, protectionDomain: ProtectionDomain?, classfileBuffer: ByteArray : ByteArray? { // Track class loading val classId = ClassId.fromSlashDelimitedFqnclassName classLoadersclassId = WeakReferenceloader // Perform bytecode analysis val analysis = performComposeAnalysisclassfileBuffer applicationInfo.update { current - current.withClassclassId, analysis } return null // No transformation during initial load } } The transform method receives the raw bytecode of every class as it loads. The method first extracts the class identifier from the internal JVM format, which uses slashes instead of dots to separate package names. It stores a weak reference to the class loader that loaded this class. Weak references are important here because class loaders can be garbage collected, and we don't want to prevent that by holding strong references to them. Then the transformer performs bytecode analysis to understand what Compose groups exist in this class. This analysis involves parsing the bytecode to find calls to methods like startRestartGroup and startReplaceableGroup, which the Compose compiler inserts around composable functions. The results go into a global applicationInfo structure that tracks everything the agent knows about the application. The transformer returns null to indicate it's not modifying the bytecode during initial loading. The modification happens later when classes are redefined during hot reload. This two-phase approach avoids potential class circularity errors that can occur if you try to transform classes too early in the JVM's initialization sequence. ComposeTransformer The Compose transformer watches for the Compose runtime itself to load and configures it for hot reload: kotlin // hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/compose.kt private class ComposeTransformer : ClassFileTransformer { override fun transform loader: ClassLoader?, className: String?, classBeingRedefined: Class<?, protectionDomain: ProtectionDomain?, classfileBuffer: ByteArray : ByteArray? { if className == "androidx/compose/runtime/Recomposer\$Companion" { // Enable hot reload mode Recomposer.Companion::class.java .getMethod"setHotReloadEnabled", Boolean::class.java .invokenull, true // Set up group invalidation Recomposer.Companion::class.java .getMethod"setGroupInvalidator", Function1::class.java .invokenull, groupInvalidator } return null } } When the Recomposer companion object loads, the transformer calls setHotReloadEnabledtrue on it. The Recomposer is the core of Compose's runtime, responsible for scheduling recomposition and managing the composition tree. Enabling hot reload mode changes how it handles recomposition, allowing the agent to trigger targeted recomposition of specific groups rather than rebuilding the entire UI. The transformer also installs a group invalidator callback. After class redefinition, the agent will call this callback with information about which Compose groups need to be recomposed. The Recomposer uses this information to invalidate only the affected groups, avoiding unnecessary recomposition of UI elements that didn't change. WindowInstrumentation The window instrumentation transformer wraps your UI in a development entry point without requiring you to modify your application code: kotlin // hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/window.kt private class WindowInstrumentation : ClassFileTransformer { override fun transform loader: ClassLoader?, className: String?, classBeingRedefined: Class<?, protectionDomain: ProtectionDomain?, classfileBuffer: ByteArray : ByteArray { if className == "androidx/compose/ui/awt/ComposeWindow" { return transformComposeWindowclassfileBuffer } return classfileBuffer } private fun transformComposeWindowbytecode: ByteArray: ByteArray { val clazz = ClassPool.getDefault.makeClassbytecode.inputStream // Redirect setContent to DevelopmentEntryPoint clazz.getDeclaredMethod"setContent".apply { insertBefore""" org.jetbrains.compose.reload.jvm.JvmDevelopmentEntryPoint .setContentthis, $$1, $$2, $$3; return; """ } return clazz.toBytecode } } Every Compose Desktop application calls setContent on a ComposeWindow to define its UI. The window instrumentation intercepts this call and redirects it through the DevelopmentEntryPoint, which adds hot reload support. The instrumentation uses Javassist to insert bytecode at the beginning of the setContent method that calls the development entry point instead of the original implementation. The inserted code receives all the same parameters as the original method using Javassist's $$ syntax, which expands to the list of method parameters. After calling the development entry point, the instrumented method returns immediately, preventing the original implementation from running. This approach works transparently without requiring developers to change their application code. Reload Request Handler When the build system detects changes and sends a reload request, the reload request handler processes it: kotlin // hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/reloadRequestHandler.kt internal fun launchReloadRequestHandlerinstrumentation: Instrumentation { orchestration.asFlow .filterIsInstance<ReloadClassesRequest .onEach { request - // Ensure reload happens on UI thread SwingUtilities.invokeAndWait { val result = performReloadinstrumentation, request // Send result back ReloadClassesResult reloadRequestId = request.messageId, isSuccess = result.isSuccess, errorMessage = result.errorMessage, errorStacktrace = result.errorStacktrace .sendBlocking } } .launchInreloadScope } The handler listens to the orchestration message flow and filters for reload class requests. When one arrives, it schedules the reload to happen on the Swing Event Dispatch Thread. Running on the EDT is crucial because the JVM's class redefinition mechanism can cause problems if you try to redefine classes while they're actively being used on other threads. By doing all the work on the EDT, we ensure that any Compose UI code is idle during the redefinition process. The invokeAndWait call blocks the handler's coroutine until the reload completes on the EDT. This blocking behavior is intentional because the handler needs to send back a result message indicating success or failure, and it can't do that until the reload actually finishes. After the reload completes, the handler sends a ReloadClassesResult message back through the orchestration, which the build system receives and displays to the developer. Class Reloading Logic The actual reload process involves several steps, all coordinated by the reload function: kotlin // hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/reload.kt internal fun Context.reload instrumentation: Instrumentation, reloadRequestId: OrchestrationMessageId, pendingChanges: Map<File, ReloadClassesRequest.ChangeType : Try<Reload = Try { val definitions = pendingChanges.mapNotNull { file, change - if change == ReloadClassesRequest.ChangeType.Removed { return@mapNotNull null } // Read the new bytecode val code = file.readBytes val classId = ClassIdcode ?: return@mapNotNull null // Find the appropriate ClassLoader val loader = findClassLoaderclassId.get ?: return@mapNotNull null // Load original class val originalClass = loader.loadClassclassId.toFqn // Transform bytecode with Javassist val clazz = getClassPoolloader.makeClasscode.inputStream // Add static re-initialization support clazz.transformForStaticsInitializationoriginalClass

Kotlin MultiplatformCompose MultiplatformCompose
Tuesday, October 14, 2025

Like what you see?

Subscribe to Dove Letter to get weekly insights about Android and Kotlin development, plus access to exclusive content and discussions.