← Back to Blog

2026-06-19

savedInstanceState: The Nolan Plot Twist Inside Android's onCreate()

Every good Nolan movie introduces something insignificant in the first act, lets you forget about it, and then brings it back when the world is falling apart. Android's savedInstanceState feels suspiciously similar.

11 min read
AndroidActivity LifecycleKotlinCompose

Opening scene: a nullable extra walks into onCreate

Every Android Activity begins with a scene that looks almost too ordinary:

kotlin
1override fun onCreate(savedInstanceState: Bundle?) {
2 super.onCreate(savedInstanceState)
3}

I remember seeing that Bundle? for the first time and mentally pushing it off-screen.

Nullable. Quiet. Optional. Surely a side character.

On the first launch, I check it and the answer is boring; that Bundle is empty.

kotlin
1savedInstanceState == null

The Activity is born. The UI appears. The audience relaxes. We think we understand the movie.

Foreshadowing

The most dangerous parameter in Android is the one that looks useless during the happy path.

Act 1: the normal world

The first act is calm. Very very calm.

The Activity is created. I inflate the classic XML layout with setContentView, or I enter the Compose era and call setContent.

kotlin
1override fun onCreate(savedInstanceState: Bundle?) {
2 super.onCreate(savedInstanceState)
3
4 setContentView(R.layout.activity_main)
5
6 counter = 0
7 selectedTab = "Home"
8}

In Compose, the stage is different, but the story is familiar:

kotlin
1override fun onCreate(savedInstanceState: Bundle?) {
2 super.onCreate(savedInstanceState)
3
4 setContent {
5 CounterScreen()
6 }
7}

Variables initialize. The user taps. Scrolls. Types. Selects a tab. The screen starts accumulating tiny bits of human intention.

And because everything is working, nobody asks what happens if the world ends.

Mermaid Diagram

But every good thing comes to an end. Some interactions can turn catastrophical when not handled right.

Act 2: the catastrophe

The user rotates the phone.

Nothing dramatic, really. A wrist movement. A casual betrayal.

Android looks at the screen and says, "New configuration. New resources. New layout constraints. We are going to rebuild this."

And then the old Activity instance walks into the fire.

Mermaid Diagram

That counter local field? Gone.

That draft text I stored in an Activity property? Gone.

That "temporary" selected tab variable? Gone.

The audience panics: progress lost.

The hidden device: onSaveInstanceState

But Android, being Android, does not destroy the world without offering one last phone call.

Before destruction, the Activity gets a chance to pack a small memory capsule:

kotlin
1override fun onSaveInstanceState(outState: Bundle) {
2 super.onSaveInstanceState(outState)
3
4 outState.putInt("counter", counter)
5 outState.putString("selected_tab", selectedTab)
6 outState.putString("draft", draftText)
7}

This is not a database. This is not cloud sync. This is not the place to hide a bitmap the size of a crime scene.

It is a Bundle: key-value storage for lightweight, serializable, transient UI state.

Used for things like:

  • counters
  • selected tabs
  • typed drafts
  • scroll position-ish markers
  • lightweight flags needed to reconstruct the screen

NOT used for things like:

  • ViewModels
  • Contexts
  • Bitmaps
  • huge lists
  • sockets
  • repositories
  • database-sized data

Rule of thumb

If it feels like luggage, do not put it in the Bundle. The Bundle is for clues.

Mermaid Diagram

Act 3: rebirth

Android creates a brand-new Activity instance.

Same class. Same onCreate(savedInstanceState: Bundle?).

But this time, the Bundle is not null.

The ignored side character returns in the third act carrying the memory of the old world.

kotlin
1override fun onCreate(savedInstanceState: Bundle?) {
2 super.onCreate(savedInstanceState)
3 setContentView(R.layout.activity_main)
4
5 counter = savedInstanceState?.getInt("counter") ?: 0
6 selectedTab = savedInstanceState?.getString("selected_tab") ?: "Home"
7 draftText = savedInstanceState?.getString("draft").orEmpty()
8}

That is the plot twist inside onCreate.

At first launch, savedInstanceState is null because there is no previous world to remember.

After recreation, it may contain what the old Activity saved before it vanished.

Table 1: lifecycle callbacks and movie roles

Callback

Movie role

What happens

onCreate()Opening shot

Activity instance is created and UI setup begins.

onStart()Lights come upActivity becomes visible.
onResume()The scene is liveUser can interact with the Activity.
onSaveInstanceState()The hidden note

Small UI state is saved before possible destruction.

onPause()The warning sirenActivity is losing focus.
onStop()Curtains closeActivity is no longer visible.
onDestroy()The explosionThe Activity instance is destroyed.

The technical cut: what savedInstanceState actually solves

savedInstanceState restores transient UI state across configuration changes and some system-initiated process recreation.

It is a Bundle, so it stores key-value pairs. The values must be lightweight and saveable: primitives, strings, small arrays, Parcelable, Serializable, and similar objects Android can marshal.

It is not long-term persistence.

It is not your source of truth.

It is the note slipped under the door before the room gets rebuilt.

Table 2: what belongs in Bundle?

Good for Bundle

Bad for Bundle

Why

Counter valueRepository instance

A number is small; a repository is an object graph.

Selected tab idContext

IDs are reconstructable; Context belongs to the system lifecycle.

Typed draft textBitmap

Small text is fine; large memory objects are not.

Sort/filter selectionSocket connection

Selections describe UI; connections must be recreated.

Scroll item keyHuge list from API

Save position clues; reload or cache real data elsewhere.

Classic Activity example: the memory capsule

kotlin
1class CounterActivity : AppCompatActivity() {
2
3 private var counter = 0
4 private lateinit var counterText: TextView
5
6 override fun onCreate(savedInstanceState: Bundle?) {
7 super.onCreate(savedInstanceState)
8 setContentView(R.layout.activity_counter)
9
10 counterText = findViewById(R.id.counterText)
11
12 counter = savedInstanceState?.getInt(KEY_COUNTER) ?: 0
13 render()
14
15 findViewById<Button>(R.id.incrementButton).setOnClickListener {
16 counter++
17 render()
18 }
19 }
20
21 override fun onSaveInstanceState(outState: Bundle) {
22 super.onSaveInstanceState(outState)
23 outState.putInt(KEY_COUNTER, counter)
24 }
25
26 private fun render() {
27 counterText.text = counter.toString()
28 }
29
30 companion object {
31 private const val KEY_COUNTER = "counter"
32 }
33}

Notice the rhythm:

  1. Read from savedInstanceState in onCreate.
  2. Use a default when it is null.
  3. Save back into outState before destruction.

The story survives because the clue was written down.

Modern Android: ViewModel, SavedStateHandle, and Compose

This is where the sequel rewrites the genre.

For screen-level state, ViewModel is usually the better place to hold state across rotation. A ViewModel survives configuration changes, so your Activity can be destroyed and recreated while the ViewModel keeps breathing behind the curtain.

But ViewModel is not magic immortality.

If the process dies, the ViewModel is gone unless important state is backed by saved state or persistence.

That is where SavedStateHandle enters.

It gives a ViewModel a small saved-state store for values that should survive process recreation.

kotlin
1class CounterViewModel(
2 private val savedStateHandle: SavedStateHandle
3) : ViewModel() {
4
5 var counter: Int
6 get() = savedStateHandle[KEY_COUNTER] ?: 0
7 private set(value) {
8 savedStateHandle[KEY_COUNTER] = value
9 }
10
11 fun increment() {
12 counter++
13 }
14
15 companion object {
16 private const val KEY_COUNTER = "counter"
17 }
18}

For durable truth, use real persistence: Room, DataStore, files, backend sync. The Bundle is not the vault. It is the clue taped under the desk.

Compose: remember versus rememberSaveable

Compose adds another timeline.

remember survives recomposition.

It does not survive Activity recreation.

kotlin
1@Composable
2fun CounterScreen() {
3 var count by remember { mutableIntStateOf(0) }
4
5 Button(onClick = { count++ }) {
6 Text("Count: $count")
7 }
8}

This is fine when Compose redraws the screen because state nearby changed.

But rotate the Activity, and that composable enters a new universe.

If the value is small and saveable, use rememberSaveable:

kotlin
1@Composable
2fun CounterScreen() {
3 var count by rememberSaveable { mutableIntStateOf(0) }
4
5 Button(onClick = { count++ }) {
6 Text("Count: $count")
7 }
8}

rememberSaveable uses saved instance state-style persistence for saveable values. It is Compose speaking the same ancient language as the Bundle, just with better stage lighting.

Table 3: state survival matrix

State holder

Recomposition

Rotation

Process death

App reinstall

Local variableNoNoNoNo
rememberYesNoNoNo
rememberSaveableYesYesSometimesNo
ViewModelYesYesNoNo
SavedStateHandleYesYesOften, for saved small stateNo
Room/DataStoreYes, after reloadYesYesNo, unless synced/backed up

Decision tree: where should the state live?

Mermaid Diagram

Conclusion

If someone asks me what savedInstanceState does, I would say:

savedInstanceState is a Bundle passed into onCreate when Android recreates an Activity after previously saving its transient UI state. It is commonly used after configuration changes like rotation, and sometimes after system-initiated process recreation. It should hold lightweight, serializable UI state, not heavy objects or long-term data.

Then I would add the modern version:

In modern Android, I usually prefer ViewModel for screen state across rotation, SavedStateHandle for small state that should survive process death, remember for recomposition-only state, rememberSaveable for saveable Compose state, and Room/DataStore for durable persistence.

That answer says the quiet part out loud: Android state is not one problem. It is several timelines stacked on top of each other.

Summary

The first time I saw savedInstanceState: Bundle?, I thought it was boilerplate.

Then the device rotated, the Activity died, and the new Activity woke up holding the old world's memories.

The Bundle was never the hero. It was the message hidden inside the watch.

Next: ViewModel — the character who survives rotation without needing a dramatic memory capsule.