Master Kotlin Multiplatform with Decompose — Part 3: Restoring state with InstanceKeeper and StateKeeper

Yeldar Nurpeissov
4 min readJul 4, 2024

--

Let’s make the Decompose components retain state across configuration changes and process death.

This article is part of a series on Kotlin Multiplatform with Decompose. If you haven’t read the previous articles, I recommend starting there.

The article covers:

  1. Creating a component
  2. Retaining state across configuration changes
  3. Recovering state after process death

Here is the starting source code.

Creating a component

In previous articles, we created two components: one for holding a list of posts and another for showing detailed post information. In this article, we will create a new component for post creation.

class DefaultCreateComponent(
componentContext: ComponentContext,
) : CreateComponent, ComponentContext by componentContext {

private val state = MutableValue(State()) // 1

override val model: Value<CreateComponent.Model> = state.map {
CreateComponent.Model(
title = it.title,
description = it.description,
author = it.author,
)
}

override fun onNameChanged(value: String) {
state.update { it.copy(title = value) }
}

override fun onDescriptionChanged(value: String) {
state.update { it.copy(description = value) }
}

override fun onAuthorChanged(value: String) {
state.update { it.copy(author = value) }
}
}
  1. Storing form state within the component.

Run the app

Fill out the form and rotate the phone horizontally.

Our data is not retained during configuration changes due to component recreation. Let's fix it!

Retaining state during configuration changes

The Essenty library, created by the author of the Decompose library, includes InstanceKeeper, similar to ViewModel from Android Jetpack.

Create a new class

private class Handler( // 1
initialState: State
) : InstanceKeeper.Instance { // 2
val state = MutableValue(initialState) // 3
}
  1. Create a class that retains memory over configuration changes.
  2. InstanceKeeper.Instance is a destroyable instance with an onDestroy() method, similar to onCleared() in ViewModel.
  3. Hold state within our InstanceKeeper.Instance.

Update the component

class DefaultCreateComponent(
componentContext: ComponentContext,
) : CreateComponent, ComponentContext by componentContext {

// 1
private val handler = instanceKeeper.getOrCreate(INSTANCE_KEY) { Handler(State()) }

override val model: Value<CreateComponent.Model> = handler.state.map {
CreateComponent.Model(
title = it.title,
description = it.description,
author = it.author,
)
}

override fun onNameChanged(value: String) {
handler.state.update { it.copy(title = value) }
}

override fun onDescriptionChanged(value: String) {
handler.state.update { it.copy(description = value) }
}

override fun onAuthorChanged(value: String) {
handler.state.update { it.copy(author = value) }
}

companion object {
private const val INSTANCE_KEY = "instance_key"
}
}
  1. Retrieve the Handler instance from instanceKeeper.

Run the app

Great! Now your class instance is saved over configuration changes.

Recovering state after process death

Although InstanceKeeper retains the class instance in memory during configuration changes, it does not persist through process death. To solve this problem, StateKeeper provided by Decompose serves as a solution, similar to onSaveInstanceState or SavedStateHandle.

Update the component

class DefaultCreateComponent(
componentContext: ComponentContext,
private val onFinished: () -> Unit,
) : CreateComponent, ComponentContext by componentContext {

private val handler = instanceKeeper.getOrCreate(INSTANCE_KEY) {
Handler( // 1
initialState = stateKeeper.consume(STATE_KEY, State.serializer()) ?: State()
)
}

init { // 2
stateKeeper.register(STATE_KEY, State.serializer()) { handler.state.value }
}

...

companion object {
private const val INSTANCE_KEY = "instance_key"
private const val STATE_KEY = "state_key"
}
}
  1. When our app restores after process death, we retrieve state from stateKeeper.
  2. The lambda will be invoked when the process is about to be killed, and the result of the lambda will be saved.

Run app

  1. Fill out the fields in your application.
  2. Navigate to the home screen, allowing the app to run in the background.
  3. Use the adb command to kill the process. Replace <YOUR APPLICATION ID> with your actual application ID. adb shell am kill <YOUR APPLICATION ID>
  4. Reopen the app.

Excellent! The app now restores state after process death.

Conclusion

Congratulations! 🎉 Your components can now be restored from both configuration changes and process death. Check out the source code on GitHub.

What’s next?

There’s room for improvement. Stay tuned for upcoming articles on Decompose and other enhancements.

Other Parts:

Connect

Let’s connect on LinkedIn and subscribe for updates!

Thank you!

--

--