
Bridges SavedStateHandle and serialization to persist UI screen state across process death, offering reactive MutableStateFlow, onInit/onRestore hooks, transient exclusions, and easy unit testing.
Kotlin Multiplatform state preservation across process death.
Respawn bridges Android's SavedStateHandle with kotlinx.serialization to seamlessly persist and recover screen state across process boundaries. It eliminates the need for @Parcelize or manual bundle manipulation by providing a standard, reactive MutableStateFlow backed by pure Kotlin serialization.
In modern Android and Kotlin Multiplatform apps, ViewModel effortlessly survives configuration changes like screen rotations. However, when the OS kills the application process in the background due to memory pressure, all in-memory ViewModel state is lost.
Traditionally, surviving process death meant:
SavedStateHandle.@Parcelize annotations on data classes, breaking multiplatform purity.Respawn solves this by letting you define your screen state as a standard Kotlin @Serializable data class and wrapping it in a MutableStateFlow. It handles synchronous state saving and restoration under the hood without extra boilerplate.
Respawn is built for Kotlin Multiplatform and supports:
minSdk = 21 (Android 5.0+), compileSdk = 34 (Android 14+)iosArm64, iosSimulatorArm64
JVM_11 bytecode (Java 11+)Add the dependency to your commonMain source set in build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.buszi.respawn:respawn:1.0.1")
}
}
}Make sure the kotlinx.serialization plugin is applied to your project:
plugins {
kotlin("plugin.serialization") version "..."
}Define your UI state as a @Serializable data class and initialize it inside your ViewModel using respawnMutableStateFlow:
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import io.buszi.respawn.onInit
import io.buszi.respawn.onRestore
import io.buszi.respawn.respawnMutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.Serializable
@Serializable
data class ScreenState(
val counter: Int = 0,
val textInput: String = ""
)
class MyViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
// 1. Initialize your flow; automatically restored on process death
private val mutableState = savedStateHandle.respawnMutableStateFlow(::ScreenState)
val state = mutableState.asStateFlow()
init {
// 2. React to lifecycle events
savedStateHandle.onInit {
// Runs only on fresh application launches (e.g. initial network fetch)
}.onRestore {
// Runs only when recovering from process death (e.g. reload cached data or track analytics)
}
}
fun increment() {
// 3. Update flow normally; state will be preserved automatically
mutableState.update { it.copy(counter = it.counter + 1) }
}
}If a single ViewModel manages multiple independent UI components, dialogs, or sheets, you can pass custom keys:
class DashboardViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
val mainState = savedStateHandle.respawnMutableStateFlow(
init = { MainState() },
key = "main_state"
)
val filterState = savedStateHandle.respawnMutableStateFlow(
init = { FilterState() },
key = "filter_state"
)
}SavedStateHandle is subject to operating system transaction limits (such as Android's 1MB transaction buffer). Serializing large collections or heavy data structures into the saved state bundle can degrade performance or trigger TransactionTooLargeException.
For data that can be quickly re-fetched or loaded from a local database/cache (such as feed items, paginated lists, or search results), use the @Transient annotation from kotlinx.serialization to exclude them from state persistence:
@Serializable
data class FeedState(
val selectedTab: String = "Home", // Persisted across process death
@Transient val feedItems: List<FeedItem> = emptyList() // Omitted from serialization
)Upon restoration, selectedTab is retained, and you can reload feedItems from your local database inside onRestore:
init {
savedStateHandle.onInit {
fetchFeedFromNetwork(state.value.selectedTab)
}.onRestore {
loadCachedFeedFromDatabase(state.value.selectedTab)
}
}Because SavedStateHandle is multiplatform in Jetpack Lifecycle, testing ViewModels that use Respawn is straightforward and requires no mocking:
class MyViewModelTest {
@Test
fun testInitialState() {
val viewModel = MyViewModel(SavedStateHandle())
assertEquals(0, viewModel.state.value.counter)
}
@Test
fun testStateMutation() {
val viewModel = MyViewModel(SavedStateHandle())
viewModel.increment()
assertEquals(1, viewModel.state.value.counter)
}
}The repository contains a sample Compose Multiplatform application demonstrating Respawn in action across all supported targets:
./gradlew :sample:desktopApp:run
androidApp targetsample/iosApp/iosApp.xcodeproj in Xcode and run the app./gradlew :sample:webApp:jsBrowserDevelopmentRun
./gradlew :sample:webApp:wasmJsBrowserDevelopmentRun
Generate the API documentation using Dokka:
./gradlew :respawn:dokkaGeneratePublicationHtmlGenerated HTML docs will be exported to the /docs directory (configured for GitHub Pages).
Copyright 2026
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Kotlin Multiplatform state preservation across process death.
Respawn bridges Android's SavedStateHandle with kotlinx.serialization to seamlessly persist and recover screen state across process boundaries. It eliminates the need for @Parcelize or manual bundle manipulation by providing a standard, reactive MutableStateFlow backed by pure Kotlin serialization.
In modern Android and Kotlin Multiplatform apps, ViewModel effortlessly survives configuration changes like screen rotations. However, when the OS kills the application process in the background due to memory pressure, all in-memory ViewModel state is lost.
Traditionally, surviving process death meant:
SavedStateHandle.@Parcelize annotations on data classes, breaking multiplatform purity.Respawn solves this by letting you define your screen state as a standard Kotlin @Serializable data class and wrapping it in a MutableStateFlow. It handles synchronous state saving and restoration under the hood without extra boilerplate.
Respawn is built for Kotlin Multiplatform and supports:
minSdk = 21 (Android 5.0+), compileSdk = 34 (Android 14+)iosArm64, iosSimulatorArm64
JVM_11 bytecode (Java 11+)Add the dependency to your commonMain source set in build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.buszi.respawn:respawn:1.0.1")
}
}
}Make sure the kotlinx.serialization plugin is applied to your project:
plugins {
kotlin("plugin.serialization") version "..."
}Define your UI state as a @Serializable data class and initialize it inside your ViewModel using respawnMutableStateFlow:
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import io.buszi.respawn.onInit
import io.buszi.respawn.onRestore
import io.buszi.respawn.respawnMutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.Serializable
@Serializable
data class ScreenState(
val counter: Int = 0,
val textInput: String = ""
)
class MyViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
// 1. Initialize your flow; automatically restored on process death
private val mutableState = savedStateHandle.respawnMutableStateFlow(::ScreenState)
val state = mutableState.asStateFlow()
init {
// 2. React to lifecycle events
savedStateHandle.onInit {
// Runs only on fresh application launches (e.g. initial network fetch)
}.onRestore {
// Runs only when recovering from process death (e.g. reload cached data or track analytics)
}
}
fun increment() {
// 3. Update flow normally; state will be preserved automatically
mutableState.update { it.copy(counter = it.counter + 1) }
}
}If a single ViewModel manages multiple independent UI components, dialogs, or sheets, you can pass custom keys:
class DashboardViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
val mainState = savedStateHandle.respawnMutableStateFlow(
init = { MainState() },
key = "main_state"
)
val filterState = savedStateHandle.respawnMutableStateFlow(
init = { FilterState() },
key = "filter_state"
)
}SavedStateHandle is subject to operating system transaction limits (such as Android's 1MB transaction buffer). Serializing large collections or heavy data structures into the saved state bundle can degrade performance or trigger TransactionTooLargeException.
For data that can be quickly re-fetched or loaded from a local database/cache (such as feed items, paginated lists, or search results), use the @Transient annotation from kotlinx.serialization to exclude them from state persistence:
@Serializable
data class FeedState(
val selectedTab: String = "Home", // Persisted across process death
@Transient val feedItems: List<FeedItem> = emptyList() // Omitted from serialization
)Upon restoration, selectedTab is retained, and you can reload feedItems from your local database inside onRestore:
init {
savedStateHandle.onInit {
fetchFeedFromNetwork(state.value.selectedTab)
}.onRestore {
loadCachedFeedFromDatabase(state.value.selectedTab)
}
}Because SavedStateHandle is multiplatform in Jetpack Lifecycle, testing ViewModels that use Respawn is straightforward and requires no mocking:
class MyViewModelTest {
@Test
fun testInitialState() {
val viewModel = MyViewModel(SavedStateHandle())
assertEquals(0, viewModel.state.value.counter)
}
@Test
fun testStateMutation() {
val viewModel = MyViewModel(SavedStateHandle())
viewModel.increment()
assertEquals(1, viewModel.state.value.counter)
}
}The repository contains a sample Compose Multiplatform application demonstrating Respawn in action across all supported targets:
./gradlew :sample:desktopApp:run
androidApp targetsample/iosApp/iosApp.xcodeproj in Xcode and run the app./gradlew :sample:webApp:jsBrowserDevelopmentRun
./gradlew :sample:webApp:wasmJsBrowserDevelopmentRun
Generate the API documentation using Dokka:
./gradlew :respawn:dokkaGeneratePublicationHtmlGenerated HTML docs will be exported to the /docs directory (configured for GitHub Pages).
Copyright 2026
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.