Buried in the Android Dev Summit announcements (Oct 28, 2026) is a new Jetpack library group called AppState, at 1.0.0-alpha01. Its idea is simple: Compose State is built on the snapshot system, which is not tied to rendering, so you can use it in ViewModels, repositories and services instead of reaching for StateFlow everywhere. The docs describe two libraries, Transform and StateStore.
1. Setup
Because transform and listener take @Composable blocks, every module that calls them needs the Compose compiler plugin and buildFeatures { compose = true }, even a non-UI data module. Modules that only read and write State<T> do not.
dependencies {
implementation(libs.androidx.appstate.transform)
implementation(libs.androidx.appstate.statestore)
// Optional: persist StateStore keys with DataStore
implementation(libs.androidx.appstate.statestore.datastore)
implementation(libs.kotlinx.serialization.core)
testImplementation(libs.androidx.appstate.transform.testing)
}
The artifacts are androidx.appstate.transform:transform, androidx.appstate.statestore:statestore and the optional statestore-datastore and transform-testing. The DataStore integration also needs the Kotlin serialization plugin and kotlinx-serialization-core (1.11.0 in the guide).
2. Transform: Compose Without the UI
transform runs a composable block with no UI attached. Whenever a State read inside the block changes, the block recomposes and the returned State updates. The signatures from the docs:
public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable (R) -> R,
): State<R>
public suspend fun listener(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable () -> Unit,
)
There is also a @Composable overload of transform that follows the calling composition and runs the block on Dispatchers.Default, which is useful for filtering a large list off the main thread. A ViewModel version, adapted from the official example, switches between online and offline sources with plain if:
class HomeViewModel(
private val mediaRepository: MediaRepository,
private val networkMonitor: NetworkMonitor,
) : ViewModel() {
val uiState: State<HomeUiState> = transform<HomeUiState>(
initialValue = HomeUiState.Loading,
scope = viewModelScope,
) {
val isOnline by networkMonitor.isOnline
val tracks = if (isOnline) {
mediaRepository.allTracksFlow.collectAsState(initial = null).value
} else {
mediaRepository.downloadedTracksFlow
.collectAsState(initial = null).value
} ?: return@transform HomeUiState.Loading
HomeUiState.Success(tracks = tracks, isOnline = isOnline)
}
}
Read tracking is dynamic: only the branch that ran in the latest recomposition is observed. That replaces a combine chain, and the docs point out that Flow.combine has typed overloads only up to five flows, while here each extra input is one more read.
Debouncing is just a LaunchedEffect keyed on the input:
var query: String by mutableStateOf("")
val searchResults: List<String> by transform<List<String>>(
initialValue = emptyList(),
scope = coroutineScope,
) {
var results by remember { mutableStateOf(emptyList<String>()) }
LaunchedEffect(query) {
if (query.isBlank()) {
results = emptyList()
return@LaunchedEffect
}
delay(300.milliseconds)
results = api.search(query)
}
results
}
listener
listener is for reacting to state in a non-UI component without producing a derived value. It is a suspend function that keeps recomposing until its coroutine is cancelled. The docs use it for a focus timer: when the session is Running, a LaunchedEffect waits until the end time and posts a notification; pausing the session removes that branch from composition and cancels the timer automatically.
3. StateStore: Keyed, Typed Shared State
StateStore is an in-memory, type-safe container that exposes values as Compose State. The docs position it for sharing state between activities, between an activity and a service or AppFunction, and between an activity and a widget. Keys are typed objects that carry their default:
@Serializable
private data object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)
class FocusSessionManager(private val stateStore: StateStore) {
val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
fun pauseFocus(nowMillis: Long) {
stateStore.updateState(FocusSessionKey) { current ->
if (current is FocusSession.Running) {
FocusSession.Paused(
taskId = current.taskId,
remainingMillis = (current.endTimeMillis - nowMillis)
.coerceAtLeast(0L),
)
} else current
}
}
}
The API surface is small: getState, setState, updateState and a keys set. Keep keys private or internal to the manager that owns them and inject the store into managers rather than UI classes.
4. Surviving Process Death with DataStore
Without extra setup the store is lost when the process dies. To persist a key, mark it with PersistToDataStore and call syncToDataStore once at startup:
class ProductivityApplication : Application() {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val stateStore = StateStore().also { store ->
applicationScope.launch {
store.syncToDataStore(
path = dataStoreFile("statestore.json").absolutePath,
coroutineScope = applicationScope,
)
}
}
}
Until DataStore finishes reading, getState returns the key's default, so never assume a restored value on first read. Every change to a persisted key serializes all persisted keys into one file, so the docs recommend persisting timestamps rather than ticks, debouncing rapid input, and keeping large or queryable data in Room.
5. Picking the Right State API
The "Use Compose State outside the UI" page gives a lifetime-based decision table that I found more useful than the libraries themselves:
- Live state (connectivity, selection):
mutableStateOfin a ViewModel or singleton; do not persist it. - Transient UI state (search query, scroll):
rememberSaveable, orSavedStateHandle.saveablein a ViewModel. - Transient session state (timer, checkout cart, form draft):
StateStorewithPersistToDataStore. - Permanent state: DataStore or Room, collected inside
transform.
Two details matter in practice. Use SavedStateHandle.saveable { mutableStateOf(...) } rather than bare saved, because only the former is Compose state that transform can track. And wrap multi-property writes in Snapshot.withMutableSnapshot so observers never see a new query paired with a stale selection, which is the atomicity a set of separate StateFlows cannot give you.
6. When Not to Use It
transform is the current value, not an event stream. If state changes several times before the next recomposition, only the latest value is used, and an update equal to the previous one is skipped. Use Flow for running totals, event logs, one-time commands that must fire twice, and backpressure-sensitive streams.
Prefer plain derivedStateOf when all inputs are already State and you want synchronous, same-snapshot results. A transform scoped to viewModelScope also stays active while the UI is in the background, which is different from a lifecycle-aware collector. And StateStore is process-local; cross-process sharing still needs IPC.
7. Testing
The transform-testing artifact adds transformTest, which wraps runTest and exposes backgroundScope, testDispatcher and an advance() that applies snapshot notifications and recomposes:
@Test
fun testStateUpdate() = transformTest {
var sourceState by mutableStateOf(0)
val state = transform(
initialValue = 0,
scope = backgroundScope,
dispatcher = testDispatcher,
) { sourceState }
assertThat(state.value).isEqualTo(0)
sourceState = 1
advance()
assertThat(state.value).isEqualTo(1)
}
Design classes that create a transform to accept the scope and dispatcher as constructor parameters so tests can inject these. The docs note that a flow-driven branch switch can need two advance() calls: one to recompose into the new branch and one to apply the freshly collected value.
What I'd Do
This is alpha, and the signatures can change, so I would not rebuild a production ViewModel layer on it. Where I would try it: one feature with several combined inputs and a debounce, where a combine chain has grown unreadable, and one process-death-sensitive session such as a timer or in-progress form, using StateStore with DataStore. If you already hold UI state in SavedStateHandle, see Lifecycle 2.11 scoped ViewModels for the surrounding ViewModel changes, and DataStore 1.3 for the persistence side.
Sources: "Agentic and Flexible: A next era of Android development" (Android Developers Blog, Oct 28, 2026) and the AppState, Transform, StateStore and "Use Compose State outside the UI" pages on developer.android.com, plus the AndroidX release page. Code is adapted from the official examples.
No comments yet. Be the first to leave one!