Jetpack AppState: Taking Compose State Beyond the UI Layer

Jetpack AppState extends Compose snapshot state to the data layer with transform and State Store libraries.
The article introduces Google's Jetpack AppState, which extends Compose's snapshot state mechanism from the UI layer to the data and domain layers. Compose consists of Compose UI and an independent runtime; snapshot state offers automatic dynamic read tracking and balances consistency with granularity, and can be used outside UI. Based on this, the transform library writes data models as composable code blocks through headless composition, where reading is subscribing, and supports early returns, nested transforms, and an eager transform frame clock; State Store is a type-safe Compose state map shared across components and persisted by DataStore, filling the gap between SavedStateHandle and in-memory singletons. Both libraries are in alpha.
Title
Jetpack AppState: Taking Compose State Beyond the UI Layer
Meta Description
Jetpack AppState extends Compose snapshot state from the UI layer into the data and domain layers: transform uses headless composition to derive state, while State Store enables cross-component sharing and persistence, solving Flow orchestration and process-death pain points.
Article Content
Compose is often viewed as a UI toolkit, but underneath it actually consists of two layers: Compose UI, which handles layout, drawing, and input, and the Compose runtime, which is completely decoupled from UI and focuses on state management and recomposition. Jetpack AppState, proposed by Google's Jetpack architecture library team in a technical talk, is based on this understanding and attempts to extend Compose's snapshot state mechanism from the interface layer to the data and domain layers, solving two recurring pain points in Android apps: orchestration of complex state, and cross-component state sharing and survival.
Why Compose State Deserves to Be Taken Out of the UI
According to Google developer relations engineer Ben Walters and architecture library engineer Elif, as apps span phones, tablets, widgets, watches, TVs, and XR devices, state management behind the UI quickly becomes difficult: on one hand, multiple data streams, local UI inputs, and environmental state (such as network connectivity) must be woven together, often piling up operators like combine, flatMapLatest, and distinctUntilChanged; on the other hand, the same state must stay synchronized across Activity, Service, and App Widget, and survive process death when needed.
Compose's snapshot state happens to provide two features that Flow lacks. The first is automatic and dynamic read tracking: there is no need to subscribe or collect—reading .value itself is the subscription; and tracking happens at runtime, so only code paths that actually execute are registered as readers. The second is that it balances consistency and granularity: updating multiple values within a snapshot takes effect atomically, while different properties can be tracked independently, so updating one only invalidates the readers that truly depend on it.

The talk used a game state example to illustrate the difference: packing level, survival time, and score into a UI state class and implementing it with Flow means every score update requires copying the entire state object, causing interfaces that only read part of the data to be invalidated unnecessarily; switching to an implementation delegated to mutable state and wrapping writes with withMutableSnapshot means the interface passes the state directly to child components without reading it, so ultimately only the component that actually reads the score is recomposed. The speaker emphasized that this mechanism is not tied to UI—UI is just its most visible use case.
Compose's snapshot state mechanism stems from its runtime's tracking model for state reads and writes. In the snapshot system, reads of each state object (MutableState) are registered in the currently active snapshot, while writes are first staged in a snapshot copy and only take effect atomically upon explicit commit (Snapshot.apply or withMutableSnapshot). This design allows multiple snapshots to coexist concurrently without interfering with each other, while notifying dependents of invalidation through a global write observer (Snapshot.registerApplyObserver). Unlike Flow's cold stream model, snapshot state does not require an explicit subscription relationship—the act of reading itself constitutes dependency registration; and because tracking happens at runtime, only code paths that are actually executed are recorded as readers, making dependencies under conditional branches naturally precise. It is precisely this semantics of "reading is subscribing, writing is atomic commit" that distinguishes snapshot state from traditional reactive streams in consistency, granularity, and dynamism, and is also the fundamental reason it can be reused outside the UI layer.
Transform: Driving Data Models with the Compose Runtime
The first library in Jetpack AppState is transform, which answers the question: if the data model itself is state, how should state be computed and derived for all consumers? This idea did not come out of nowhere—the Molecule library has already proven that composable functions do not have to emit UI nodes and can produce pure data models through headless composition. transform combines this with the state model: the data model is directly state.

According to Elif, transform's API is just one function that returns an ordinary observable state rather than a Flow or StateFlow, and consumers have no way to know it was produced by transform. The key is that its code block is composable, so everything available in composables—LaunchedEffect, collectAsState, early returns, ordinary if branches, etc.—can be used directly.
A focus timer example demonstrates its value: session state comes from a timer engine that ticks every second, the current task comes from a Flow from the Room database, and the ViewModel needs to combine the two into a summary. The traditional approach requires using combine to chain two streams, doing branch checks inside the lambda, and finally stateIn; transform instead writes the entire derivation process as the code block itself, using collectAsState to bridge each Flow, where reading is subscribing. Thanks to dynamic read tracking, when in the idle state, Room's cold flow is not collected at all, and early returns let unnecessary processing be skipped entirely.
Headless composition refers to running Compose's composition process without creating any UI view tree. The Compose runtime itself does not depend on the Android View system; Recomposer and Composition can be started in a pure JVM environment, and the composed result is not interface nodes but arbitrary data objects. The Molecule library was the first to productize this capability: it exposes an entry point returning a StateFlow, drives composition in a background coroutine, and enables composable primitives such as LaunchedEffect, remember, and collectAsState to be used for building data pipelines. Because there is no window or display frame, headless composition needs to provide its own frame clock to drive recomposition—this is exactly the motivation for transform's transform frame clock. The significance of headless composition lies in reusing Compose's declarative programming model and precise read tracking without introducing the rest of the UI framework.
Nested transform and Cross-Interface State Consistency
When the same session summary needs to appear in a mini timer bar and a desktop widget, writing the derivation logic separately for each interface means three places of code must stay consistent with one another. A more reasonable approach is to derive it once at application scope, with all downstream consumers consuming the same state.

In a traditional implementation, the mini timer bar's ViewModel also has to additionally track a state representing whether it is expanded, stacking another layer of combine and stateIn. transform allows nesting: the ViewModel treats the upstream transform's output as ordinary state and reads it, while also using collectAsState to collect local state, mixing the two in the same block. Leveraging the granularity feature, toggling the expanded state only reruns this small block, leaving the application-level derivation and the widget completely unaffected. Because the mini timer bar and the widget read the same state, the two can never be inconsistent; tapping pause on the widget sends the change back through the manager, session flow, and summary, and all three interfaces update in sync.
Under the hood, transform creates a transform scope, starts the composable transformation within it, and returns disposable state; the transformation is automatically disposed when the scope ends. Because it is a headless environment, no display frame clock is available, so the team specifically implemented the transform frame clock: it adopts an eager strategy, producing a new frame immediately whenever consumed state changes; when idle, the clock parks, and unread state incurs no overhead. This mechanism is a lock-free state machine, where requesting a frame is just a compare-and-set. In addition, transform now supports rememberSaveable, saving data across configuration changes through the new saveStateContainer API, which is also the common foundation for Compose's saveable state registry, SavedStateHandle, and SavedStateRegistry.
State Store: A State Container Across Component Boundaries
Ben divides state in an app into three categories: real-time state that exists only in memory (such as network or Bluetooth connection state), transient state that must survive process death but does not need long-term storage (such as UI expansion state, navigation back stack, form drafts), and permanent state stored long-term (such as user preferences or a media library in a database).

In the past, architecture guidance required Compose state to remain only in the UI layer or ViewModel, with Flow used everywhere else, because once a state property needs to be merged with Flow, it has to be converted back using snapshotFlow. With transform, this conversion is no longer necessary: local UI inputs can be declared directly as mutableStateOf; queries that must survive process death can be backed by SavedStateHandle.savable; and connection state in a singleton can also be stored directly as Compose state, updating .value directly in callbacks, and bridging back to Flow with snapshotFlow when needed.
The real gap is session state that needs to be shared across multiple interfaces and components: SavedStateHandle writes to the Activity's bundle and is unavailable in components such as Service; while mutableStateOf in a singleton lives only in memory and cannot survive process death. State Store is designed for exactly this—a type-safe, Compose-state-based map that can be shared across components and persisted by DataStore. It has only three methods: setState writes a new value, updateState computes a new value based on the current value (such as calculating remaining time when toggling between running and paused), and getState returns read-only state and can override the default value at the call site.
Persistence only requires syncing DataStore to the store once, and then having each key that needs persistence implement the corresponding interface. Under the hood, it leverages transform's listener API (the same as transform but without returning a value, used only for observation and side effects), reading all keys within it and syncing changes back to DataStore through LaunchedEffect. The speaker emphasized that having a standard container like State Store is what enables such out-of-the-box integrations, and DataStore is only the first.
The talk concluded by announcing that Transform, State Store, and the DataStore integration have all entered alpha, documentation has been published on developer.android.com, and there is also an AppState recipes repository on GitHub providing examples. Both speakers repeatedly reminded the audience that the alpha stage is exactly the time to collect feedback and adjust APIs based on real use cases.
SavedStateHandle is a key-value container provided by Jetpack ViewModel for restoring state after the process is reclaimed by the system. It relies on SavedStateRegistry underneath, and the host of SavedStateRegistry is a component with a lifecycle and state-saving callbacks, such as an Activity or Fragment. In an Activity, the contents of SavedStateHandle are ultimately written to that Activity's saved instance state bundle and returned by the system on configuration changes or process recreation. The problem is that components such as Service and App Widget do not have an equivalent saved state registration mechanism and cannot directly hold a SavedStateHandle; at the same time, SavedStateHandle's scope is limited to a single ViewModel, and there is no natural propagation channel for sharing the same transient state across multiple interfaces or components. State Store uses DataStore as its persistence backend, bypassing the component-level bundle mechanism, thereby establishing a unified shared container that can survive process death across Activity, Service, and widgets.
Editorial Observations
AppState's ambition is not to add another state management framework, but to redefine the Compose runtime from a "UI engine" into a "general-purpose state engine." For teams that have fully adopted Compose, this means the data layer and UI layer can share the same state semantics, eliminating repeated conversions between Flow and state; for projects still using a mixed Flow architecture, transform provides a path for gradual migration. It is worth noting that all APIs are still in alpha and interfaces may change at any time, so migration costs should be evaluated before adoption in production.
One-line Summary
Jetpack AppState extends Compose's snapshot state mechanism to the data layer and introduces two libraries: transform and State Store.
Paragraph Summary
The article introduces Jetpack AppState proposed by Google, whose core idea is to extend Compose's snapshot state mechanism from the UI layer to the data and domain layers. Compose consists of Compose UI and an independent runtime; snapshot state has two major features—automatic dynamic read tracking and a balance of consistency and granularity—and can be used outside UI. Based on this, the transform library writes data models as composable code blocks through headless composition, where reading is subscribing, and supports early returns, nested transforms, and an eager transform frame clock; State Store is a type-safe Compose state map that can be shared across components and persisted by DataStore, filling the gap between SavedStateHandle and in-memory singletons. Both libraries have entered alpha.
Related articles

No Python Required: How Cursor AI Coding Is Reshaping Automated Testing and Test Development
Test engineers can do automated testing without learning Python? This article examines how Cursor and other AI coding tools empower functional testing, automation scripts, test platform development, and performance tuning, compares the cost and access restrictions of mainstream domestic and foreign tools, and offers capability transformation advice for testers.

Nearly 5,000 Affected! AI Coding Plugin Found with Backdoor, Officials Urgently Remove It
Claude Code's VS Code extension version 2.1.313 was maliciously tampered with, implanting a backdoor for remote login and persistent control, potentially affecting up to 5,000 users. Officials have removed the malicious version; this article details the attack method and cleanup steps.

Codex and Cursor Localization and Model Switching, All Solved by One Small Tool
A Bilibili creator shares CCX Plus, a small tool that solves both localization and model switching for Codex and Cursor—install and forget, reliable connections, no garbled text, immediate effect.