respawn

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.

Why Respawn?

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:

  • Storing individual primitive keys manually in SavedStateHandle.

  • Using Android-specific @Parcelize annotations on data classes, breaking multiplatform purity.

  • Writing tedious bundle mapping code.

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.

Supported Targets & Compatibility

Respawn is built for Kotlin Multiplatform and supports:

  • Android: minSdk = 21 (Android 5.0+), compileSdk = 34 (Android 14+)

  • iOS: iosArm64, iosSimulatorArm64

  • JVM: JVM_11 bytecode (Java 11+)

  • JavaScript: Browser (IR)

  • WasmJS: Browser

Installation

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")
}
}
}

Quick Start

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) }
}
}

Multiple State Flows

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"
)
}

Performance & Memory: Using @Transient

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)
}
}

Packages

Link copied to clipboard
common