---
title: 'The Activity Lifecycle'
source: 'https://academia.sh/en/courses/android-platform-and-tools/activity-lifecycle'
course: 'Android Platform and Tools'
language: en
updated: '2026-10-21T05:02:35+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Orders the activity lifecycle callbacks from onCreate to onDestroy'
    - 'Measures that a field that was not saved returns with its default value after a configuration change'
    - 'Shows that the state of an activity the user closes is discarded'
    - 'Verifies that a UI request that reaches a stopped activity stays pending'
---

# The Activity Lifecycle

Measures the lifecycle callbacks of an activity, which state survives a configuration change and process death, and how a UI request that reaches a stopped activity is fulfilled.

A declared activity opened, and the user rotated the screen. The activity is destroyed and rebuilt, and the fields of the new activity carry the values of the first setup. This lesson measures which value comes back after the rebuild. The criterion is again **fulfillment**, but here the request itself asks for a value back: the counter that was `5` before the rotation, is it requested from the platform as `5` afterwards, and does it arrive as `5`?

The measure of this lesson is separate from the lifecycle measures of the earlier courses. In the PHP course "deferred" was kept apart from "pending"; in the lifecycle lesson of the Swift Language course the question was when an expression is evaluated. The question here is a different one: what is measured is not when the request is evaluated but how it is fulfilled in the lifecycle state the activity is in at that moment.

```kotlin
// Fulfillment.kt — classifies how the platform fulfills a request
sealed interface Outcome {
    data class Given(val value: String) : Outcome
    data class Waiting(val on: String) : Outcome
    data class Refused(val reason: String) : Outcome
}

class Ask(val label: String, val wanted: String, val outcome: Outcome) {
    val fulfillment: String = when (outcome) {
        is Outcome.Waiting -> "pending"
        is Outcome.Refused -> "unmet"
        is Outcome.Given -> if (outcome.value == wanted) "full" else "substitute"
    }

    fun report() {
        val detail = when (outcome) {
            is Outcome.Given -> "given=${outcome.value}"
            is Outcome.Waiting -> "waiting-on=${outcome.on}"
            is Outcome.Refused -> "reason=${outcome.reason}"
        }
        println("$label: wanted=$wanted | $detail -> $fulfillment")
    }
}

fun tally(asks: List<Ask>) {
    for (name in listOf("full", "substitute", "pending", "unmet")) {
        println("$name: ${asks.count { it.fulfillment == name }}")
    }
}
```

## Lifecycle Callbacks

An activity passes through certain states, and the platform calls a lifecycle callback at each transition. The order of setup is as follows.

- `onCreate`: the activity is set up and the UI is prepared. If a saved instance state exists, it arrives here.
- `onStart`: the activity becomes visible on the screen.
- `onResume`: the activity is in the foreground and interacts with the user.
- `onPause`: the activity leaves the foreground; another activity is coming to the front.
- `onStop`: the activity is no longer visible.
- `onDestroy`: the activity is destroyed.

On a configuration change such as rotating the screen, the platform first tears the activity down with `onPause`, `onStop`, and `onDestroy`, then rebuilds it with the new configuration through `onCreate`, `onStart`, and `onResume`. The fields disappear in between. `onSaveInstanceState`, which is called before the teardown, holds a small bundle; the rebuilt activity receives this bundle in `onCreate`.

```kotlin
// Platform code: only the counter is written to the saved instance state
import android.os.Bundle

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putInt("counter", counter)
}
```

Here `counter` is saved; no other field such as `draft` is written. The system can also end an entire background process when memory runs low. This process death depends on the same bundle: a field that is not saved does not come back.

The model sets up this rule of the platform. The helper file carries a single rule: if the key is in the saved bundle, that value is given; if not, the field's default value is given. The input values are plain strings.

```kotlin
// Restore.kt — restores a field from the saved instance state
fun restoreField(key: String, saved: Map<String, String>, defaults: Map<String, String>): Outcome =
    Outcome.Given(saved[key] ?: defaults.getValue(key))
```

## Three Fields on Rotation

Before the rotation three fields carry a value: `counter` is 5, `name` is Ada, `draft` is hello. `onSaveInstanceState` writes only the first two. The wanted value is always the one from before the rotation.

```kotlin
// Main.kt — how fields come back on a configuration change
fun main() {
    val before = mapOf("counter" to "5", "name" to "Ada", "draft" to "hello")
    val saved = mapOf("counter" to "5", "name" to "Ada")
    val defaults = mapOf("counter" to "0", "name" to "empty", "draft" to "empty")
    val asks = before.map { (key, value) -> Ask(key, value, restoreField(key, saved, defaults)) }
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
counter: wanted=5 | given=5 -> full
name: wanted=Ada | given=Ada -> full
draft: wanted=hello | given=empty -> substitute
full: 2
substitute: 1
pending: 0
unmet: 0
```

The two saved fields came back as wanted: full. The `draft` field, which was not saved, arrived with its default value: a value was given, but it is `empty`, not `hello`; substitute. The user sees that the text typed before the rotation is gone, and the code throws no error. For this reason substitute produces a quieter problem than unmet. An unmet request stops the code; a substitute flows on and carries the wrong value.

**AC1.** The model takes a single default value. The initial value of a real field depends on the setup in `onCreate`, and mechanisms such as a `ViewModel`, which carry state across a configuration change, were not measured in this lesson.

## Three Events, Three Fulfillments

The same bundle is returned from by different events. Rotation and process death keep the bundle. If the user closes the activity themselves, for example with the back button, the platform discards the state: the saved bundle is also dropped. The model below takes the event as an input as well.

```kotlin
// Main.kt — keeping state by event
fun restoreAfter(event: String, key: String, saved: Map<String, String>, defaults: Map<String, String>): Outcome =
    if (event == "user-closed") Outcome.Refused("state-discarded") else restoreField(key, saved, defaults)

fun main() {
    val saved = mapOf("counter" to "5")
    val defaults = mapOf("counter" to "0", "draft" to "empty")
    val asks = listOf(
        Ask("rotation-counter", "5", restoreAfter("rotation", "counter", saved, defaults)),
        Ask("death-draft", "hello", restoreAfter("process-death", "draft", saved, defaults)),
        Ask("close-counter", "5", restoreAfter("user-closed", "counter", saved, defaults)),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
rotation-counter: wanted=5 | given=5 -> full
death-draft: wanted=hello | given=empty -> substitute
close-counter: wanted=5 | reason=state-discarded -> unmet
full: 1
substitute: 1
pending: 0
unmet: 1
```

On rotation the saved field came back fully. On process death the field that was not saved returned as a substitute. When the user closed the activity, the state did not come back at all: unmet. Closing is not an error, it is the user's wish; the platform does not carry the state left over from a closed activity into the next launch.

**AC2.** The model separates the three events only by these names. Under which condition the system ends an activity, and the size limit of the bundle, were not measured.

## Callback Order

The order of the lifecycle callbacks changes with the event that reaches the activity. When the order departs from what is expected, the most common mistake is that the work meant for a callback never runs. This section measures four events in turn. The wanted value is the full chain described in the section on the order of setup; the given value is the callbacks the platform really calls for that event.

An app can choose to handle a configuration change itself. In that case the activity is not destroyed; only `onConfigurationChanged` is called. The choice is made in the manifest file with the `configChanges` attribute.

```xml
<activity android:name=".EditorActivity"
          android:configChanges="orientation|screenSize" />
```

The model sets up four events. `open` is the first launch. `rotate` is the default configuration change. `rotate-self-handled` is the same event with `configChanges` declared. `process-killed` is the system closing the background process; in this case a call to `onDestroy` is not guaranteed to arrive.

```kotlin
// Main.kt — callback chain by event
fun callbacksFor(event: String): Outcome = when (event) {
    "open" -> Outcome.Given("onCreate>onStart>onResume")
    "rotate" -> Outcome.Given("onPause>onStop>onDestroy>onCreate>onStart>onResume")
    "rotate-self-handled" -> Outcome.Given("onConfigurationChanged")
    else -> Outcome.Refused("onDestroy-not-guaranteed")
}

fun main() {
    val asks = listOf(
        Ask("open", "onCreate>onStart>onResume", callbacksFor("open")),
        Ask("rotate", "onPause>onStop>onDestroy>onCreate>onStart>onResume", callbacksFor("rotate")),
        Ask("rotate-self-handled", "onPause>onStop>onDestroy>onCreate>onStart>onResume", callbacksFor("rotate-self-handled")),
        Ask("process-killed", "onDestroy", callbacksFor("process-killed")),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
open: wanted=onCreate>onStart>onResume | given=onCreate>onStart>onResume -> full
rotate: wanted=onPause>onStop>onDestroy>onCreate>onStart>onResume | given=onPause>onStop>onDestroy>onCreate>onStart>onResume -> full
rotate-self-handled: wanted=onPause>onStop>onDestroy>onCreate>onStart>onResume | given=onConfigurationChanged -> substitute
process-killed: wanted=onDestroy | reason=onDestroy-not-guaranteed -> unmet
full: 2
substitute: 1
pending: 0
unmet: 1
```

The first two events gave the wanted chain exactly. The `rotate-self-handled` line came out substitute: because the app took over the configuration change, the activity was not destroyed, so the fields stayed in place. Here the substitute is harmless, even a wanted result; but a setup done in `onCreate` or a save done in `onSaveInstanceState` never runs on this path. Every activity that declares `configChanges` is obliged to reload the configuration-sensitive resources itself. The fourth line is unmet: code that relies on `onDestroy` for cleanup cannot do that cleanup when the process ends all at once. Data that has to last must be saved in `onStop` or earlier.

**AC3.** The model reduces the `configChanges` values to a single logical flag. The real attribute names each configuration type separately; a type that is not declared still destroys the activity. Which type leads to destruction under which condition was not measured.

## A Translucent Activity on Top

The `onStop` callback means the activity is no longer visible at all. When a new activity is opened on top, this is not always true: if the new activity does not cover the screen entirely, for example if it is opened in the form of a dialog or with a translucent theme, the activity underneath stays visible. In that case the activity underneath receives only `onPause`; `onStop` is not called.

```kotlin
// Platform code: declaring an activity with a translucent theme
// <activity android:name=".ConfirmActivity"
//           android:theme="@style/ConfirmDialogTheme" />
```

The measurement below gives the callbacks that reach the activity underneath according to how the top activity covers it. The wanted value is `onPause>onStop`.

```kotlin
// Main.kt — top activity and the callbacks of the activity below
fun belowCallbacks(coverage: String): Outcome = when (coverage) {
    "full-screen" -> Outcome.Given("onPause>onStop")
    "dialog" -> Outcome.Given("onPause")
    "same-task-same-screen" -> Outcome.Waiting("top-activity-closing")
    else -> Outcome.Refused("unknown-coverage")
}

fun main() {
    val asks = listOf("full-screen", "dialog", "same-task-same-screen", "none").map {
        Ask("top-$it", "onPause>onStop", belowCallbacks(it))
    }
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
top-full-screen: wanted=onPause>onStop | given=onPause>onStop -> full
top-dialog: wanted=onPause>onStop | given=onPause -> substitute
top-same-task-same-screen: wanted=onPause>onStop | waiting-on=top-activity-closing -> pending
top-none: wanted=onPause>onStop | reason=unknown-coverage -> unmet
full: 1
substitute: 1
pending: 1
unmet: 1
```

When a full-screen activity opened, the activity underneath was stopped: full. When an activity in the form of a dialog opened, the activity underneath received only `onPause` and was not stopped: substitute. The activity underneath is still visible on the screen, and it does not release the resources that would be released in `onStop`. This difference explains why code that closes resources such as the camera or location updates in `onStop` sometimes fails to do so: while a dialog is open, `onStop` never arrives. The third line shows another use: a state that is awaited until the top activity closes appears as pending.

**AC4.** The model separates the way of covering with four names. In multi-window use and in picture-in-picture mode, the visibility of the activity underneath and which callbacks arrive were not measured.

## The Bounding Measurement

The second effect of the lifecycle is the state in which a request is made. Code that wants to update the UI gets a different fulfillment depending on the state of the activity. An activity in the foreground applies the update at once. A stopped activity is not yet visible; the update waits until the activity is started again. An update sent to a destroyed activity goes nowhere.

```kotlin
// Main.kt — updating the UI by lifecycle state
fun updateUi(state: String): Outcome = when (state) {
    "resumed" -> Outcome.Given("updated")
    "stopped" -> Outcome.Waiting("started-state")
    else -> Outcome.Refused("no-activity")
}

fun main() {
    val asks = listOf(
        Ask("foreground", "updated", updateUi("resumed")),
        Ask("stopped", "updated", updateUi("stopped")),
        Ask("destroyed", "updated", updateUi("destroyed")),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
    println(asks.filter { it.fulfillment == "pending" }.map { it.label })
}
```

```text
foreground: wanted=updated | given=updated -> full
stopped: wanted=updated | waiting-on=started-state -> pending
destroyed: wanted=updated | reason=no-activity -> unmet
full: 1
substitute: 0
pending: 1
unmet: 1
[stopped]
```

The same update request got three different fulfillments in three states. The pending line matters: `stopped` was neither refused nor deferred; it waits on a specific condition, `started-state`, and will be applied when the condition arrives. This is the activity counterpart of the distinction in the PHP course: pending is a request whose condition has a name.

**AC5.** The model separates three states; `started` and `paused` did not get a line of their own. What exactly the visibility is in these two states changes with the platform version and with multi-window use.

## Summary

- This lesson's criterion: **fulfillment**. A request is fulfilled according to the lifecycle state of the activity. This lesson's fulfillment: **substitute**; a field that was not saved comes back with its default value without an error.
- On a configuration change the saved fields came back full, and the field that was not saved came back as a substitute with its default value: 2 full and 1 substitute.
- When the user closes the activity, the state is discarded; the same request stays unmet.
- A UI request that reaches a stopped activity stays pending and carries its condition by name: `started-state`.
- A substitute is silent, an unmet request is loud: the code goes on with the wrong value without raising an error.

## Next Step

The state of an activity is kept with `onSaveInstanceState`; the parts of a screen carry lifecycles of their own. The next lesson, Fragments, measures how fragments inside an activity are added and how a fragment transaction is fulfilled while the host activity is stopped.
