How Decompose Handles State Preservation Across Process Death on Android
Decompose preserves component state across Android process death by bridging its StateKeeper API to the Jetpack SavedStateRegistry, automatically serializing data via kotlinx.serialization and restoring it when the system recreates your activity.
Decompose is a Kotlin Multiplatform library for building component-based UIs that treats state preservation as a first-class feature. On Android, the library handles the complexity of surviving process death through a tight integration with AndroidX Saved State. This article examines the internal architecture—specifically how ComponentContext exposes state preservation and how DefaultJetpackComponentContext wires it to the Android framework.
The StateKeeper Architecture: From ComponentContext to Android
Decompose abstracts platform-specific state preservation behind the StateKeeper API. This design allows components to remain platform-agnostic while the framework handles Android-specific lifecycle events.
ComponentContext as StateKeeperOwner
Every Decompose component receives a ComponentContext in its constructor. This interface extends StateKeeperOwner, granting direct access to a StateKeeper instance.
In decompose/src/commonMain/kotlin/com/arkivanov/decompose/ComponentContext.kt, the interface hierarchy establishes this contract:
interface ComponentContext : StateKeeperOwner, InstanceKeeperOwner, BackHandlerOwner {
// ComponentContext aggregates essential lifecycle owners
}
The StateKeeperOwner provides the stateKeeper: StateKeeper property, which components use to consume previously saved state or register new state for preservation.
The Jetpack Bridge: DefaultJetpackComponentContext
On Android, Decompose creates a DefaultJetpackComponentContext that bridges the generic StateKeeper to Android's SavedStateRegistry. This implementation resides in jetpack-component-context/src/commonMain/kotlin/com/arkivanov/decompose/jetpackcomponentcontext/DefaultJetpackComponentContext.kt.
The critical linkage occurs in the init block (lines 58-64):
// Restore previously saved state from the Android bundle
stateKeeper.consume(key = KEY, strategy = SavedStateSerializer)
.also { savedState -> savedStateRegistryController.performRestore(savedState) }
// Save the current state into the Android bundle when required
stateKeeper.register(key = KEY, strategy = SavedStateSerializer) {
savedState().also(savedStateRegistryController::performSave)
}
The SavedStateSerializer implements StateKeeper.Serializer<Bundle>, converting between Decompose's internal representation and Android Bundle objects that the system persists across process death.
Implementing State Preservation in Components
Developers interact with state preservation through two primary patterns: manual consume/register calls or the experimental saveable property delegate.
Manual State Management with consume and register
For explicit control, components call stateKeeper.consume() during initialization and stateKeeper.register() in the init block:
import com.arkivanov.decompose.ComponentContext
import kotlinx.serialization.Serializable
class CounterComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
// Restore previously saved counter value, or start from 0
private var counter: Int = stateKeeper.consume(
key = "COUNTER",
strategy = Int.serializer()
) ?: 0
init {
// Save the current counter whenever Android asks for it
stateKeeper.register(
key = "COUNTER",
strategy = Int.serializer()
) { counter }
}
fun increment() { counter++ }
}
The consume/register calls map directly to Android's SavedStateRegistry via DefaultJetpackComponentContext, ensuring the counter value survives process termination.
Simplified State Handling with the saveable Delegate
Decompose provides an experimental saveable delegate that automatically handles both consumption and registration:
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.statekeeper.saveable
import kotlinx.serialization.Serializable
class UserComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
// `saveable` automatically wires consume + register
private var userName: String by saveable(
serializer = String.serializer(),
init = { "" } // default when no saved state
)
}
The delegate internally registers the same state-keeper entry that ends up in the Android bundle, reducing boilerplate while maintaining process-death survival.
Preserving Complex Objects with InstanceKeeper
For ViewModel-like objects that must survive both configuration changes and process death, combine InstanceKeeper with StateKeeper:
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.statekeeper.saveable
import kotlinx.serialization.Serializable
class CartComponent(componentContext: ComponentContext) : ComponentContext by componentContext {
private val cart = instanceKeeper.getOrCreate {
CartStatefulEntity(savedState = stateKeeper.consume("CART", CartState.serializer()))
}
init {
stateKeeper.register("CART", CartState.serializer()) { cart.state }
}
}
class CartStatefulEntity(savedState: CartState?) : InstanceKeeper.Instance {
var state: CartState = savedState ?: CartState()
private set
@Serializable data class CartState(val items: List<String> = emptyList())
}
The retained entity survives configuration changes through InstanceKeeper and process death through StateKeeper, providing comprehensive state persistence.
Serialization and Lifecycle Flow
Decompose relies on kotlinx.serialization to convert arbitrary data classes into a format storable in an Android Bundle. The complete lifecycle across process death follows this sequence:
- Component Initialization: The component calls
stateKeeper.consume()with a serializer (e.g.,Int.serializer()orMyState.serializer()). - State Restoration: If Android provides a saved bundle from a previous process,
DefaultJetpackComponentContextdeserializes it and returns the object; otherwise, the component uses a default value. - Registration: The component calls
stateKeeper.register(), providing a lambda that returns the current state. This lambda is invoked when Android requests state preservation. - Process Death: When the system kills the process, Android calls the registered callbacks.
DefaultJetpackComponentContexttriggersperformSave, serializing the state into aBundle. - Recreation: When the user returns and the activity recreates,
DefaultJetpackComponentContextcallsperformRestorewith the persisted bundle, making the saved state available viaconsume()on the next component initialization.
Developers can opt out of this behavior by passing serializer = null when creating navigation stacks or panels, which disables StateKeeper registration entirely.
Summary
- Decompose treats state preservation as a core feature available on all platforms, with Android-specific integration via Jetpack Saved State.
ComponentContextextendsStateKeeperOwner, exposingstateKeeperfor manualconsume/registeroperations or thesaveabledelegate.DefaultJetpackComponentContextbridges the genericStateKeeperto Android'sSavedStateRegistryusingSavedStateSerializer, ensuring bundles survive process death.kotlinx.serializationpowers the conversion between Kotlin objects and Android-persistable bundles.- Opt-out is available by passing
nullserializers, though the default behavior automatically handles process death survival.
Frequently Asked Questions
How does Decompose differ from ViewModel's SavedStateHandle?
Decompose provides a platform-agnostic StateKeeper API that works identically across Android, iOS, Desktop, and Web. While SavedStateHandle is Android-specific and tied to the ViewModel lifecycle, Decompose's stateKeeper integrates with Android's SavedStateRegistry only when running on Android via DefaultJetpackComponentContext, offering the same process-death survival without platform lock-in.
Can I use Decompose state preservation without kotlinx.serialization?
No. Decompose relies on kotlinx.serialization to convert state objects into a format that can be stored in Android bundles. You must provide a serializer (e.g., MyState.serializer()) when calling stateKeeper.consume() or stateKeeper.register(). The library does not support Java Serializable or Parcelable directly, though you can write custom serializers if needed.
What happens if I don't register any state with StateKeeper?
If you do not call stateKeeper.register() (or use the saveable delegate which registers automatically), your component state will not be persisted across process death. The component will reinitialize with default values when the system recreates your activity. Configuration changes (like rotation) may still preserve state through InstanceKeeper if you use retained instances, but process death requires explicit StateKeeper registration.
Is state preservation automatic for navigation stacks in Decompose?
Yes, but only if you provide serializers. When creating navigation stacks using childStack or childPanels, Decompose automatically wires the navigation state through StateKeeper if you pass non-null serializers for the configuration objects. If you pass serializer = null, the library disables state preservation for that navigation entity, and the stack will reset to initial state after process death.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →