Surviving Process Death Without Permanent Storage: Ephemeral Persistence for Both StateHolders and ViewModels
In a previous article, we explored different strategies — Singletons, Shared ViewModels, and the StateHolder Pattern — to maintain global state. However, these patterns alone don’t inherently solve the issue of restoring data when Android kills and later restarts your app.
Jetpack Compose revolutionizes how you build Android UIs, yet process death remains a concern. If the system kills your app in the background and later re-creates it, you often need to restore some temporary data (ephemeral state) without storing it permanently on disk. In this article, we’ll show two approaches:
- SavedStateRegistry for a custom StateHolder (non-ViewModel).
- SavedStateHandle for a ViewModel (the recommended approach when you already have a ViewModel).
This way, you can see how to handle ephemeral data in a plain Kotlin class and in a ViewModel, while continuing to use the Compose UI model.
This article shows how to integrate these solutions with Koin for Dependency Injection (DI). You’ll see how to set up modules, inject classes, and keep ephemeral state alive across short-term process kills.
1. What Is Ephemeral State?
Ephemeral state is data you want to retain during normal app usage and across short-term kills by the system — e.g., phone runs out of memory and kills your app. It is not meant for permanent storage (like a user profile or app settings) but for transitory UI states or small session data. Once the user fully closes or uninstalls the app, the ephemeral data is lost.
Examples:
- A step the user is on in a multi-step compose screen.
- A simple form input the user typed but hasn’t submitted yet.
- A small flag or counter that shouldn’t be stored in a database.
2. Plain StateHolder + SavedStateRegistry
If you have a plain Kotlin class (often referred to as a StateHolder in some patterns) and want it to survive short-term process kills, you can manually hook into SavedStateRegistry
. This is helpful when:
- You aren’t using a ViewModel.
- You want a global or app-wide ephemeral class that isn’t tied to any one Activity’s lifecycle.
- You prefer to unify ephemeral storage logic for multiple classes without adopting
SavedStateHandle
.
2.1 Defining the StateHolder
// We assume ephemeral user session info
data class UserState(
val isLoggedIn: Boolean = false,
val userName: String = ""
)
interface UserStateHolder {
val userState: StateFlow<UserState>
fun updateUser(name: String)
fun clearUser()
// For ephemeral saving/restoring
fun onSaveInstanceState(): Bundle
fun onRestoreInstanceState(bundle: Bundle)
}
class UserStateHolderImpl : UserStateHolder {
private val _userState = MutableStateFlow(UserState())
override val userState: StateFlow<UserState> = _userState
override fun updateUser(name: String) {
_userState.update { current ->
current.copy(isLoggedIn = true, userName = name)
}
}
override fun clearUser() {
_userState.update { UserState() }
}
override fun onSaveInstanceState(): Bundle {
val current = _userState.value
return Bundle().apply {
putBoolean("IS_LOGGED_IN", current.isLoggedIn)
putString("USER_NAME", current.userName)
}
}
override fun onRestoreInstanceState(bundle: Bundle) {
val loggedIn = bundle.getBoolean("IS_LOGGED_IN", false)
val name = bundle.getString("USER_NAME", "")
_userState.update {
it.copy(isLoggedIn = loggedIn, userName = name ?: "")
}
}
}
2.2 Koin Module
import org.koin.dsl.module
val stateHolderModule = module {
// Provide the holder as a singleton
single<UserStateHolder> { UserStateHolderImpl() }
}
2.3 Registering in a Jetpack Compose Activity
Even if your UI is fully in Compose, you still have a ComponentActivity
:
class MainActivity : ComponentActivity() {
private val userStateHolder: UserStateHolder by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// If there's a previously saved bundle
savedStateRegistry.consumeRestoredStateForKey("USER_HOLDER")?.let {
userStateHolder.onRestoreInstanceState(it)
}
// Register for ephemeral saving
savedStateRegistry.registerSavedStateProvider("USER_HOLDER") {
userStateHolder.onSaveInstanceState()
}
setContent {
MyApp(userStateHolder)
}
}
}
@Composable
fun MyApp(userStateHolder: UserStateHolder) {
val userState by userStateHolder.userState.collectAsState()
if (userState.isLoggedIn) {
Text("Welcome, ${userState.userName}")
} else {
Button(onClick = { userStateHolder.updateUser("Alice") }) {
Text("Log In as Alice")
}
}
}
Notes:
consumeRestoredStateForKey("USER_HOLDER")
returns the previously savedBundle
if the system re-created the process.registerSavedStateProvider("USER_HOLDER")
ensures a newBundle
is generated whenever the system needs to save the app’s state.
3. ViewModel + SavedStateHandle (Recommended for ViewModels)
For ephemeral data that naturally belongs in a ViewModel, SavedStateHandle
is built into the framework. Koin can provide your ViewModel the same way, and SavedStateHandle
automatically saves the data if the system kills your app.
3.1 Defining the ViewModel
class MyViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// Provide a default
private val _counter = savedStateHandle.getStateFlow("COUNTER_KEY", 0)
val counter: StateFlow<Int> = _counter
fun increment() {
val current = _counter.value + 1
savedStateHandle["COUNTER_KEY"] = current
}
}
Note: We rely on getStateFlow("KEY", defaultValue)
so that Compose can collect it easily.
3.2 Koin Module for the ViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModule = module {
// Koin has a built-in way to handle the SavedStateHandle
viewModel { (handle: SavedStateHandle) ->
MyViewModel(handle)
}
}
3.3 Usage in Compose
@Composable
fun CounterScreen(
viewModel: MyViewModel = viewModel()
// or Koin can provide the instance with
// val viewModel by koinViewModel<MyViewModel>()
) {
val count by viewModel.counter.collectAsState()
Column {
Text("Count: $count")
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
}
}
When the process is killed and re-created, SavedStateHandle
automatically re-injects the last saved "COUNTER_KEY"
value so the StateFlow
resumes where it left off.
4. Choosing an Approach
In many situations, you’ll likely favor SavedStateHandle
for ephemeral data in a ViewModel because it’s fully supported and less manual. However, if you have a custom approach or need a global “holder” not bound to a single Activity’s lifecycle, hooking into SavedStateRegistry
is still valid.
5. General Tips
- Keep Data Small: Both
SavedStateRegistry
andSavedStateHandle
store data in aBundle
. Large data can cause problems. - No Permanent Storage: These solutions do not store data across full app restarts; they only restore ephemeral state if the system kills and re-creates the process.
- Koin Injection: You can inject either approach easily:
StateHolder
withsingle { ... }
ViewModel
withviewModel { (handle: SavedStateHandle) -> ... }
- Testing: For a
StateHolder
, you can create a mock or fake. For a ViewModel with Koin, you can override modules or provide a testSavedStateHandle
.
6. Conclusion
Handling ephemeral data in Jetpack Compose often requires that you:
- Store the data in memory for fast UI updates.
- Save it across short-term process kills so the user doesn’t lose everything.
You can achieve this in two ways with Koin:
- Plain Kotlin “StateHolder” +
SavedStateRegistry
: Ideal if you’re not using a ViewModel but still want ephemeral restoration. - ViewModel +
SavedStateHandle
: The recommended solution for ephemeral UI state in Compose-based apps. It’s simpler, well-supported, and automatically rehydrates state.
Either way, you have a robust method of surviving process death for transient data — without needing permanent solutions like DataStore or SharedPreferences. This keeps your code clean, leverages Koin for DI, and ensures your users have a seamless experience even in low-memory scenarios.