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.

Breaking down the ViewModel's Internal Mechanisms and Multiplatform

Android's ViewModel has become an essential component of modern Android development, providing a lifecycle-aware container for UI-related data that survives configuration changes. While the API appears simple on the surface, the internal machinery reveals sophisticated design decisions around lifecycle management, multiplatform abstraction, resource cleanup, and thread-safe caching. Understanding how ViewModel works under the hood helps you make better architectural decisions and avoid subtle bugs. In this article, you'll dive deep into how Jetpack ViewModel works internally, exploring how the ViewModelStore retains instances across configuration changes, how ViewModelProvider orchestrates creation and caching, how the factory pattern enables flexible instantiation, how CreationExtras enables stateless factories, how resource cleanup is managed through the Closeable pattern, and how viewModelScope integrates coroutines with ViewModel lifecycle. This isn't a guide on using ViewModel, it's an exploration of the internal machinery that makes lifecycle-aware state management possible. The fundamental problem: Surviving configuration changes Configuration changes present a fundamental challenge for Android development. When a user rotates their device, changes language settings, or triggers any configuration change, the system destroys and recreates the Activity. Any data stored in the Activity is lost: kotlin class MyActivity : ComponentActivity { private var userData: User? = null // Lost on rotation! override fun onCreatesavedInstanceState: Bundle? { super.onCreatesavedInstanceState // Must reload data after every rotation loadUserData } } The naive approach is to use onSaveInstanceState: kotlin override fun onSaveInstanceStateoutState: Bundle { super.onSaveInstanceStateoutState outState.putParcelable"user", userData } override fun onCreatesavedInstanceState: Bundle? { super.onCreatesavedInstanceState userData = savedInstanceState?.getParcelable"user" } This works for small, serializable data. But what about large datasets, network connections, or objects that can't be serialized? What about ongoing operations like network requests? The Bundle approach fails for these cases, both because of size limitations and because serialization/deserialization is expensive. ViewModel solves this by providing a lifecycle-aware container that survives configuration changes through a retained object pattern, not serialization. The ViewModelStore: The retention mechanism At the heart of ViewModel's configuration-change survival is ViewModelStore, a simple key-value store that holds ViewModel instances: kotlin public open class ViewModelStore { private val map = mutableMapOf<String, ViewModel @RestrictToRestrictTo.Scope.LIBRARYGROUP public fun putkey: String, viewModel: ViewModel { val oldViewModel = map.putkey, viewModel oldViewModel?.clear } @RestrictToRestrictTo.Scope.LIBRARYGROUP public operator fun getkey: String: ViewModel? { return mapkey } @RestrictToRestrictTo.Scope.LIBRARYGROUP public fun keys: Set<String { return HashSetmap.keys } public fun clear { for vm in map.values { vm.clear } map.clear } } The implementation is remarkably straightforward, just a MutableMap<String, ViewModel. The magic isn't in the store itself, it's in how the store is retained. Key replacement behavior Notice the put method's behavior: kotlin public fun putkey: String, viewModel: ViewModel { val oldViewModel = map.putkey, viewModel oldViewModel?.clear } If a ViewModel already exists with the same key, the old ViewModel is immediately cleared. This ensures proper cleanup when a ViewModel is replaced. You might wonder when this happens, it occurs when you request a ViewModel with the same key but a different type: kotlin // First request creates TestViewModel1 with key "mykey" val vm1: TestViewModel1 = viewModelProvider"mykey", TestViewModel1::class // Second request with same key but different type val vm2: TestViewModel2 = viewModelProvider"mykey", TestViewModel2::class // vm1.onCleared has been called, vm1 is no longer valid This behavior is validated in the test suite: kotlin @Test fun twoViewModelsWithSameKey { val key = "thekey" val vm1 = viewModelProviderkey, TestViewModel1::class assertThatvm1.cleared.isFalse val vw2 = viewModelProviderkey, TestViewModel2::class assertThatvw2.isNotNull assertThatvm1.cleared.isTrue } The ViewModelStoreOwner contract The ViewModelStoreOwner interface defines who owns the store: kotlin public interface ViewModelStoreOwner { public val viewModelStore: ViewModelStore } This simple interface is implemented by ComponentActivity, Fragment, and NavBackStackEntry. The owner's responsibility is twofold:

Kotlin MultiplatformCoroutinesArchitecture
Tuesday, January 6, 2026
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.