Process Death Handling via Automatic StateFlow Persistence with Reanimator (Android & KMP)
Ioannis Anifantakis on 2025-04-21
Process Death Handling via Automatic State Persistence (Android & KMP)

Why Saving App Information is Very Important
When we build mobile apps, for Android phones or using Kotlin Multiplatform (KMP), users expect apps to remember where they were and what they were doing, even if they switch to another app for a moment or get a phone call.
Now a big challenge, especially on Android, is something called “process death”. To save resources, the Android system might completely stop your app if it’s running in the background (not visible on the screen). This isn’t a crash; it’s a normal Android behavior.
However, when the user comes back to your app, Android restarts it only with the navigation stack restored and the rest of the state not restored. If you haven’t saved the screen’s state properly you will end up with improperly restored UI.
The MVI Problem: One State Object, Two Kinds of Data
Many modern app developers like using patterns such as MVI (Model-View-Intent). A common idea in MVI is to define a data class to host a single-state object for each screen.
However, this single state object often contains two different kinds of information mixed together:
- Persistent State: Information that must be saved and restored after process death.
- Transient State: Temporary information that should NOT be saved. This state should reset to its default value when the app restarts.
@Serializable data class MyScreenState( // --- Save This --- val userProfile: UserProfile? = null, val items: List<Item> = emptyList(), // --- Do Not Save This (Transient) --- val isLoading: Boolean = false, val isDownloading: Boolean = false )
If we simply save the entire MyScreenState
object using SavedStateHandle
, we run into trouble. When the app restarts after process death, the isLoading
flag might still be true
(showing a spinner forever!) or an old status might still show that doesn’t make sense anymore.
To fix this manually, more code would need to be added for filtering what needs to be persisted and resetting would be needed for whatever should not be persisted, adding complexity and boilerplate code – exactly the kind of repetitive work MVI tries to help us avoid!
Approach 1 — @Transient
+ light snippet
If your state class is primarily used within a specific ViewModel and not serialized elsewhere with different needs, Kotlin's built-in @Transient
annotation is often the simplest solution.
You mark the fields directly in the data class:
import androidx.lifecycle.SavedStateHandle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.serialization.json.Json import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty inline fun <reified T> SavedStateHandle.getMutableStateFlow( defaultValue: T, coroutineScope: CoroutineScope, key: String? = null ) = PropertyDelegateProvider<Any, ReadOnlyProperty<Any, MutableStateFlow<T>>> { thisRef, property -> val viewModelClassName = thisRef.let { it::class.simpleName } ?: "UnknownViewModel" val actualKey = key ?: "${viewModelClassName}_${property.name}" val json = Json { ignoreUnknownKeys = true }, val stateFlow = MutableStateFlow( get<String>(actualKey)?.let { json.decodeFromString<T>(it) } ?: defaultValue ).also { flow -> flow.onEach { set(actualKey, json.encodeToString(it)) }.launchIn(coroutineScope) } ReadOnlyProperty { _, _ -> stateFlow } }
And in your ViewModel:
// YOUR SERIALZABLE DATA CLASS WITH TRANSIENT FIELDS @Serializable data class MyScreenState( val userProfile: UserProfile? = null, val items: List<Item> = emptyList(), @Transient val isLoading: Boolean = false, @Transient val isDownloading: Boolean = false, ) // YOUR STATE AT THE VIEWMODEL private val _state by savedStateHandle.getMutableStateFlow( coroutineScope = viewModelScope, defaultValue = MyScreenState(), ) val state = _state.asStateFlow()
This simple snippet wires up loading/decoding, saving/encoding, and collection efficiently.
If you control the model class and @Transient
fits your needs perfectly, this solution is excellent and keeps dependencies minimal.
Where the Snippet Shines
When you don’t have any @Transient
properties in your state, otherwise a data class that doesn’t fall short in the “Falls Short” list!
Where the Snippet Falls Short
The @Transient
approach works beautifully when the data class definition aligns perfectly with your persistence needs. However, you might encounter situations where it's less ideal:
- Transient Logic: Your Transient Logic is handled within the data class and not at the ViewModel— The most important problem.
- Reused Models: If the same state data class needs all the fields serialized for any other case,
@Transient
would block these fields from serialization.
A larger snippet that promotes the idea that the ViewModel should control which parts of its state survive process death. It focuses on giving the ViewModel this control over transient fields, especially for the purpose of saving state via SavedStateHandle
.
It uses the familiar getMutableStateFlow
extension function but adds the optional transientProperties
parameter:
The Reanimator Snippet
import androidx.lifecycle.SavedStateHandle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonObject import kotlinx.serialization.serializer import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty @OptIn(ExperimentalSerializationApi::class) inline fun <reified T : Any> SavedStateHandle.getMutableStateFlow( defaultValue: T, key: String? = null, coroutineScope: CoroutineScope, transientProperties: Set<String> = emptySet(), ): PropertyDelegateProvider<Any, ReadOnlyProperty<Any, MutableStateFlow<T>>> { return PropertyDelegateProvider { owner, property -> // build a stable key for SavedStateHandle, val viewModelClassName = owner.let { it::class.simpleName } ?: "UnknownViewModel" val actualKey = key ?: "${viewModelClassName}_${property.name}" val serializer = serializer<T>() val json = Json { ignoreUnknownKeys = true } // ---------- initial value ---------- val initial: T = get<String>(actualKey)?.let { stored -> if (transientProperties.isEmpty()) { // no transients in list, just decode the json runCatching { json.decodeFromString(serializer, stored) } .getOrElse { defaultValue } } else { // transients in the list, merge with defaults val saved = json.parseToJsonElement(stored).jsonObject val fresh = json.encodeToJsonElement(serializer, defaultValue).jsonObject // rebuild JSON: keep everything except the transient keys, // then add the default values for those keys val patched = buildJsonObject { saved.forEach { (k, v) -> if (k !in transientProperties) put(k, v) } transientProperties.forEach { k -> fresh[k]?.let { put(k, it) } } } runCatching { json.decodeFromJsonElement(serializer, patched) } .getOrElse { defaultValue } } } ?: defaultValue // ---------- Persist each StateFlow emission into SavedStateHandle ---------- MutableStateFlow(initial).also { flow -> flow.onEach { value -> // initially encode all properties val element = json.encodeToJsonElement(serializer, value) // then remove transient ones if "transientProperties" is not empty val cleanedObj = (element as? JsonObject) ?.takeIf { transientProperties.isNotEmpty() } ?.let { JsonObject(it.filterKeys { k -> k !in transientProperties }) } ?: element // return the encoded value as SavedStateHandle key/value set(actualKey, cleanedObj.toString()) }.launchIn(coroutineScope) }.let { ReadOnlyProperty { _, _ -> it } } } }
Here is how you use it:
// YOUR SERIALZABLE DATA CLASS WITH TRANSIENT FIELDS @Serializable data class MyScreenState( val userProfile: UserProfile? = null, val items: List<Item> = emptyList(), val isLoading: Boolean = false, // no need to mark it @Transient val isDownloading: Boolean = false // no need to mark it @Transient ) // In ViewModel: private val _state by savedStateHandle.getMutableStateFlow( defaultValue = MyScreenState(), // Your starting state object coroutineScope = viewModelScope, // Scope for background saving (usually viewModelScope) // Define transient fields HERE, not necessarily in MyScreenState transientProperties = setOf("isLoading", "isDownloading") ) val state: StateFlow<MyScreenState> = _state.asStateFlow()
Reanimator uses the same fast, direct serialization path as the simple snippet and will respect any @Transient
annotations already present in your data class, so you can use it in place of the simpler snippet as well if you wish to make use of @Transient
annotation.
However, the library's extra internal logic for filtering/merging only activates when you explicitly leverage the transientProperties
feature to gain ViewModel-level control.
The Reanimator Library
Reanimator Library is a short library that just packs the reanimator snippet seen above.
I added it as a library to avoid pasting long snippets, but also to allow for more refined usage by improvements that could be added along the way.
All you need to do is add this library:
kotlin { sourceSets { commonMain.dependencies { implementation("eu.anifantakis:reanimator:1.0.5") // ... other libraries } } }
or just copy the reanimator snippet from above :)
Reanimator in Action: A KMP Example Explained
Let’s look at the code example again, explaining it more. Remember, you can get the full working code from the demo project: github.com/ioannisa/ReanimatorUIDemo.
1. Add the Library (in build.gradle.kts
): This line tells your project to include the Reanimator library.
2. Define State & Intents (in commonMain
): This data class holds all info for the product screen. @Serializable
allows Reanimator to convert it.
@Serializable // Needs to be Serializable data class SimpleProductState( // --- Save This (Persistent) --- val products: List<String> = emptyList(), // The list should be saved val selectedProduct: String? = null, // The selection should be saved // --- Do Not Save This (Transient) --- val isLoading: Boolean = false, // Loading is temporary, reset it val errorMessage: String? = null // Errors are temporary, reset it ) // User actions sealed interface SimpleProductIntent { /* ... LoadProducts, SelectProduct, etc. ... */ }
We clearly see the mix: products
and selectedProduct
need saving, while isLoading
and errorMessage
are temporary.
3. Create the ViewModel (in commonMain
or androidMain
): The ViewModel manages the state and logic.
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import eu.anifantakis.lib.reanimator.getMutableStateFlow class SimpleProductViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { // List the names of properties NOT to save private val transientProperties = setOf("isLoading", "errorMessage") // Reanimator makes saving state automatic here! private val _state: MutableStateFlow<SimpleProductState> by savedStateHandle.getMutableStateFlow( defaultValue = SimpleProductState(), // Initial state, also provides defaults for transient parts on restore coroutineScope = viewModelScope, // Scope for background saving work transientProperties = transientProperties // Pass the list of properties to ignore when saving ) val state: StateFlow<SimpleProductState> = _state.asStateFlow() // --- Handle User Actions --- fun processIntent(intent: SimpleProductIntent) { if (intent is SimpleProductIntent.LoadProducts) { _state.update { it.copy(isLoading = true, errorMessage = null) } } // ... handle other intents ... } // ... other ViewModel functions ... }
How to Get Started
Ready to make state saving easier?
- Add Library: Add the dependency
implementation("eu.anifantakis:reanimator:1.0.2")
to yourbuild.gradle.kts
file (usually incommonMain
dependencies). - Maven Central: You can find more library details here: https://central.sonatype.com/artifact/eu.anifantakis/reanimator.
- Source Code: See how it works on GitHub: https://github.com/ioannisa/reanimator.
- Demo App: See a working example on GitHub: https://github.com/ioannisa/ReanimatorUIDemo.
- How to Use: Just make sure your state data class is marked with
@Serializable
and use thesavedStateHandle.getMutableStateFlow(...)
delegate in your ViewModel.
Saving screen state correctly, especially when dealing with process death or complex state objects in MVI, is essential for creating good, professional apps. It used to involve a lot of difficult, repetitive code.
Reanimator offers a simple solution for Android and KMP developers. It automates the process of saving your StateFlow
state using SavedStateHandle
. Most importantly, it lets you easily tell it which parts of your state are temporary (transient properties) and should not be saved.
Enjoyed the article?
SUBSCRIBE and CLAP on Medium
Ioannis Anifantakis anifantakis.eu | LinkedIn | YouTube | X.com | Medium