Jetpack Compose State<T> and
MutableState<T> are general-purpose value holders backed by
the Compose snapshot system. Although they're most commonly used to drive UI
recomposition, the snapshot system itself isn't tied to UI rendering.
With the AppState libraries—Transform
and StateStore—you can hold, derive, observe, and persist
Compose State across ViewModels, managers, repositories, and background
services.
Why use Compose State beyond the UI?
When you read Compose State inside a state-observing block—such as a UI
composable, transform, or
derivedStateOf—the Compose runtime provides two key
benefits across any layer of your architecture:
- Automatic and dynamic dependency tracking: Reading a state's
.value(or usingbyproperty delegation) automatically registers that state as a dependency of the block. Because tracking is dynamic, dependencies are determined at runtime by which code paths execute. If a conditional branch isn't taken (for example, after anifcheck or earlyreturn), states inside that branch aren't read and changes to them don't trigger unnecessary recomputations. - Consistency and granularity: You can declare separate,
fine-grained
MutableStateproperties and wrap multi-property writes inSnapshot.withMutableSnapshot. Intermediate updates are isolated to the mutable snapshot and applied atomically when the block completes, so observers reading inside a snapshot never see inconsistent intermediate state and only recompute when the specific properties they read change. UnlikeStateFlow, you don't have to choose between a single monolithicStateFlow<UiState>(which invalidates every collector when any field changes) and separateStateFlowproperties (which can't update together in a single atomic transaction).
Choose the right state API across your architecture
Choose how to hold and persist Compose State based on how long a piece of
state should live and which components need to access it:
| State category | Lifetime and scope | Examples | Recommended Compose State APIs |
|---|---|---|---|
| Live state | Always stored in memory; queried fresh on startup and not persisted across process death. | Network connection status, Bluetooth connection status, temporary UI state such as item selection. |
|
| Transient UI state | Survives configuration changes and system-initiated process death, not stored indefinitely; scoped to a single screen or activity. | Expanded or collapsed UI elements, scroll position, screen search query or filter toggles. |
|
| Transient session state | Survives background process death, not stored indefinitely; shared across activities, services, or widgets. | Active focus timer, multi-step checkout cart, or in-progress form draft. |
|
| Permanent state | Kept on disk indefinitely. | User preferences and settings (DataStore), structured app data and media libraries (Room). | DataStore or Room flows collected inside transform using collectAsState |
Use Compose State in a ViewModel
Inside a ViewModel, you can declare independent screen
inputs as granular MutableState properties, persist them across process death
with SavedStateHandle.saveable, update multiple
properties atomically with
Snapshot.withMutableSnapshot, and combine them with
repository Flow streams inside a transform:
sealed interface TaskListState { data object Loading : TaskListState data class Success( val tasks: List<Task>, val selectedTaskIds: Set<String>, val isOnline: Boolean = true, ) : TaskListState } @HiltViewModel class TaskListViewModel @Inject constructor( private val tasksRepository: TasksRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet<String>()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set fun updateSearchQuery(query: String) { Snapshot.withMutableSnapshot { searchQuery = query selectedTaskIds = emptySet() } } val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, ) } }
Key patterns in this ViewModel:
- Use
SavedStateHandle.saveablefor reactive saved state: DelegatingsearchQuerytoSavedStateHandle.saveablewithmutableStateOf(fromandroidx.lifecycle:lifecycle-viewmodel-compose) persists the value across process death as aMutableState, sotransformtracks reads automatically. Avoid bareSavedStateHandle.saved, which returns a plain Kotlin property rather than ComposeStateand doesn't trigger recomposition when changed. - Atomic multi-property writes with
Snapshot.withMutableSnapshot: UpdatingsearchQueryandselectedTaskIdsinsideSnapshot.withMutableSnapshotisolates the writes and commits both changes in a single atomic snapshot transaction, so observers (includingtransform) see both updates together in one recomposition pass rather than observing the newsearchQuerypaired with staleselectedTaskIds. - Reuse flows across recompositions with
remember(key): WrappingtasksRepository.getTasks(searchQuery)inrememberwithsearchQueryas a key ensures that whenselectedTaskIdschanges and recomposes thetransformblock, Compose reuses the existing databaseFlowrather than restarting the query.
Share live system state across your app
For live system state shared across multiple ViewModels, activities, or
services—such as network or Bluetooth connection status—you don't want to
persist stale status to disk across process death. Instead, query the current
system state on startup, hold it in a private mutableStateOf property inside
an application-scoped singleton, and expose a read-only State<T>:
interface NetworkMonitor { val isOnline: State<Boolean> } @Singleton class LiveNetworkMonitor @Inject constructor( @ApplicationContext context: Context, ) : NetworkMonitor { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) private val _isOnline = mutableStateOf(checkCurrentConnectivity()) override val isOnline: State<Boolean> = _isOnline private val callback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities, ) { _isOnline.value = networkCapabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } override fun onLost(network: Network) { _isOnline.value = false } } init { connectivityManager.registerDefaultNetworkCallback(callback) } private fun checkCurrentConnectivity(): Boolean { val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } }
MutableState supports consistent reads and updates from any thread, so
NetworkCallback can update _isOnline.value directly from its background
callback thread. If a background callback updates multiple MutableState
properties together, wrap the writes in
Snapshot.withMutableSnapshot so they're applied
atomically.
Any component in your app can read networkMonitor.isOnline.value—whether as a
one-off check, inside a UI composable, or inside a state-observing block such as
listener, derivedStateOf, or
transform. For example, TaskListViewModel can read
networkMonitor.isOnline.value inside its transform block:
val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, isOnline = networkMonitor.isOnline.value, ) }
Because transform tracks networkMonitor.isOnline.value alongside
searchQuery, selectedTaskIds, and the repository Flow, any change to
connectivity automatically recomputes TaskListState.Success without
re-querying the database.