RizTech Academy logo
RizTech Academy
How an Android App Is Put TogetherLesson 5 of 635 min

The lifecycle: the thing that breaks naive apps

Arjun is a trader in Nashik comparing onion prices. He taps "Check price" three times, the counter on screen says 3, and he turns his phone sideways to see the table better. The counter says 0. Nothing crashed, no error was logged, and the bug will reach the Play Store unless you know why it happens. This lesson builds that exact app, runs it on an emulator, and reads what Android actually did.

The demonstration

Replace the body of MainActivity.kt in a scratch copy of the project (not your real one) with this. It logs every lifecycle callback with Log.d, and keeps a counter in an ordinary property of the activity:

package com.riztechacademy.mandibhav

import android.os.Bundle
import android.os.Process
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

private const val TAG = "Lifecycle"

class MainActivity : ComponentActivity() {

    // A plain property on the activity. The mutableIntStateOf wrapper only
    // makes the screen redraw when it changes; where it lives is the point.
    private var checks by mutableIntStateOf(0)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(
            TAG,
            "onCreate pid=${Process.myPid()} activity=${hashCode()} " +
                "restored=${savedInstanceState != null} checks=$checks"
        )
        enableEdgeToEdge()
        setContent {
            MaterialTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column(Modifier.padding(innerPadding).padding(16.dp)) {
                        Text("Onion at Lasalgaon: checked $checks times")
                        Button(onClick = {
                            checks++
                            Log.d(TAG, "tap checks=$checks")
                        }) {
                            Text("Check price")
                        }
                    }
                }
            }
        }
    }

    override fun onStart() {
        super.onStart()
        Log.d(TAG, "onStart")
    }

    override fun onResume() {
        super.onResume()
        Log.d(TAG, "onResume")
    }

    override fun onPause() {
        Log.d(TAG, "onPause")
        super.onPause()
    }

    override fun onStop() {
        Log.d(TAG, "onStop")
        super.onStop()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        Log.d(TAG, "onSaveInstanceState checks=$checks")
        super.onSaveInstanceState(outState)
    }

    override fun onDestroy() {
        Log.d(TAG, "onDestroy isChangingConfigurations=$isChangingConfigurations checks=$checks")
        super.onDestroy()
    }
}

mutableIntStateOf is Compose state, which the next module explains; here it only makes the text update when checks changes. The thing under test is that checks is a property of the activity object. Each log line also prints the process id (pid) and the activity object's hashCode(), so you can tell whether you are looking at the same process and the same object.

Run it, open the Logcat window in Android Studio, and filter with tag:Lifecycle. From the command line, adb logcat -s Lifecycle shows the same lines.

Launch, and three taps

11-08 14:29:47.873 D/Lifecycle(12179): onCreate pid=12179 activity=200314805 restored=false checks=0
11-08 14:29:47.906 D/Lifecycle(12179): onStart
11-08 14:29:47.916 D/Lifecycle(12179): onResume
11-08 14:29:52.969 D/Lifecycle(12179): tap checks=1
11-08 14:29:53.612 D/Lifecycle(12179): tap checks=2
11-08 14:29:54.246 D/Lifecycle(12179): tap checks=3

Created, started, resumed, in 43 milliseconds. restored=false because there was no previous instance. Now the app is in the resumed state: on screen, in front, taking input.

Rotate

On the emulator, use the rotate button in the emulator's toolbar. (To do it from a script: adb shell settings put system accelerometer_rotation 0 to stop auto-rotate, then adb shell settings put system user_rotation 1 for landscape and 0 to come back. Put accelerometer_rotation back to 1 afterwards.)

11-08 14:29:55.156 D/Lifecycle(12179): onPause
11-08 14:29:55.159 D/Lifecycle(12179): onStop
11-08 14:29:55.160 D/Lifecycle(12179): onSaveInstanceState checks=3
11-08 14:29:55.193 D/Lifecycle(12179): onDestroy isChangingConfigurations=true checks=3
11-08 14:29:55.228 D/Lifecycle(12179): onCreate pid=12179 activity=252370821 restored=true checks=0
11-08 14:29:55.234 D/Lifecycle(12179): onStart
11-08 14:29:55.236 D/Lifecycle(12179): onResume

And the screen now reads "Onion at Lasalgaon: checked 0 times".

Read it slowly, because this is the whole lesson:

  • The activity was destroyed: onDestroy ran, with isChangingConfigurations=true and checks=3. The count was still there at that moment.
  • A new activity object was created: the hash changed from 200314805 to 252370821. Its checks started at the property's initial value, 0.
  • The process stayed the same: pid=12179 throughout. Nothing was killed. The app's memory is intact; only the activity was thrown away.
  • restored=true: Android handed the new activity a Bundle from onSaveInstanceState. It contained Android's own saved state and none of ours, because we put nothing in it.

Rotation is a configuration change, and by default a configuration change destroys and recreates the activity. Anything you keep in the activity object dies with it. That is the counter bug, and every form of it: a half-typed price alert, a selected market, a scroll position, a list you downloaded.

Why would Android do something so drastic? Because resources can depend on the configuration. A landscape screen might use different dimensions; a phone switched to Hindi needs every string from values-hi. Recreating the activity is the one way Android can guarantee every resource is reloaded for the new configuration. Rotation is only the most common trigger. Others are: changing the system language, switching dark mode on or off, changing the font size, resizing the app in split-screen, and folding or unfolding a foldable.

Home, and coming back

A fresh run (so a new pid), two taps, then Home:

11-08 14:32:20.992 D/Lifecycle(13288): onCreate pid=13288 activity=200314805 restored=false checks=0
11-08 14:32:21.014 D/Lifecycle(13288): onStart
11-08 14:32:21.018 D/Lifecycle(13288): onResume
11-08 14:32:25.520 D/Lifecycle(13288): tap checks=1
11-08 14:32:26.166 D/Lifecycle(13288): tap checks=2
11-08 14:32:26.895 D/Lifecycle(13288): onPause
11-08 14:32:27.942 D/Lifecycle(13288): onStop
11-08 14:32:27.943 D/Lifecycle(13288): onSaveInstanceState checks=2

No onDestroy. The activity is stopped: not visible, but the object still exists. Open the app again from the launcher:

11-08 14:32:30.023 D/Lifecycle(13288): onStart
11-08 14:32:30.024 D/Lifecycle(13288): onResume

Same object, no onCreate, and the screen still says 2. (There is also an onRestart callback before onStart in this case; the demonstration does not override it.) This is why the bug survives testing: Home and back behaves perfectly, and that is what most people test.

Back

Press Back on the main screen:

11-08 14:32:34.275 D/Lifecycle(13288): onPause
11-08 14:32:35.346 D/Lifecycle(13288): onStop
11-08 14:32:35.355 D/Lifecycle(13288): onDestroy isChangingConfigurations=false checks=2

isChangingConfigurations=false: this destruction is final. Notice there was no onSaveInstanceState; the user said they were finished, so there is nothing to restore. The process was still alive afterwards (adb shell pidof com.riztechacademy.mandibhav still printed 13288), and opening the app again gave a fresh activity:

11-08 14:32:37.443 D/Lifecycle(13288): onCreate pid=13288 activity=155904899 restored=false checks=0
``` That is correct behaviour; a user who backs out expects to start again.

This was on an Android 16 emulator. Android versions have changed what Back does to the last activity in an app, and on some phones you will see `onStop` without `onDestroy`. Do not write code that depends on either.

## Process death

The last case is the one users hit most and developers test least. Press Home, so the app is in the background. Android may now kill the whole process to free memory for the camera or a game, without calling `onDestroy` or anything else, and on a budget phone with 3 GB of RAM it does this often. To make it happen on demand, while the app is in the background:

```bash
adb shell am kill com.riztechacademy.mandibhav

Before, after four taps on that fresh activity and Home:

11-08 14:32:46.529 D/Lifecycle(13288): onPause
11-08 14:32:47.589 D/Lifecycle(13288): onStop
11-08 14:32:47.590 D/Lifecycle(13288): onSaveInstanceState checks=4

Then the kill, which logs nothing at all. Then the user opens the app from Recents or the launcher:

11-08 14:32:52.070 D/Lifecycle(13511): onCreate pid=13511 activity=204768842 restored=true checks=0
11-08 14:32:52.091 D/Lifecycle(13511): onStart
11-08 14:32:52.096 D/Lifecycle(13511): onResume

A new process (13511, not 13288), so every object, every variable and every singleton in the app was gone. Yet restored=true: Android kept the saved Bundle outside your process and handed it back, so the user returns to the same screen as if nothing happened. Only your count is missing. From the user's side the app forgot what they were doing while they answered a phone call.

The callbacks, as a reference

Callback When it runs Use it for
onCreate Activity object created and attached One-time setup: setContent, reading the launch intent
onStart About to become visible Starting things that update the visible UI
onResume In front and taking input Things that need focus, such as a camera preview
onPause Losing focus; may still be partly visible Pausing those; keep it very short
onStop No longer visible Stopping updates; saving work you must not lose
onSaveInstanceState Before a stop that may be followed by recreation Small UI state, so it survives rotation and process death
onDestroy Finishing, or recreating for a configuration change Final cleanup. Not called on process death

And the four scenarios from the logs:

What happened Activity object Process Your checks
Home, then back Same Same Kept
Rotation New Same Lost
Back, then reopen New Same Lost (correctly)
Background, process killed, reopen New New Lost, though the screen was restored

The trap: making rotation stop

Search for this bug and you will find advice to add android:configChanges="orientation|screenSize" to the activity in the manifest, which tells Android not to recreate the activity on rotation. The counter survives rotation, and the advice looks right. It is not. It does nothing for process death, nothing for a language or dark-mode change you did not list, and it makes you responsible for reloading anything that should differ in landscape. It hides the bug in the one case you tested.

The real fixes keep state somewhere that outlives the activity:

  • A ViewModel survives configuration changes, because Android keeps it across the recreation. It does not survive process death. That is module 5, "ViewModel, and surviving rotation".
  • Saved state (rememberSaveable in Compose, SavedStateHandle in a ViewModel) goes into that Bundle, so it survives process death too. It is meant for small values like an id or a typed query, not a list of prices. That is module 5, "Saved state, and surviving process death".
  • Data that matters beyond this screen goes to disk, which is the local-data module.

Until then, every Mandi Bhav screen you build in this course should be rotated and process-killed before you call it done.

Check your work

On rotation, which callbacks run, in order? onPause, onStop, onSaveInstanceState, onDestroy, then onCreate, onStart, onResume on a new instance.

How do the logs prove the activity object was replaced? The hashCode() changed (200314805 to 252370821).

How do they prove the process was not? The pid stayed 12179.

Why did checks reset to 0? It was a property of the activity object, and a new object starts with the initial value.

What does isChangingConfigurations tell you in onDestroy? Whether the activity is being destroyed only to be recreated (true) or for good (false).

Does Home destroy the activity? No; it stops it. Returning runs onStart and onResume on the same object.

What survives process death? The saved-instance-state Bundle. Every object in the old process is gone.

Why is android:configChanges not the fix? It covers only the listed configuration changes, does nothing for process death, and leaves resource reloading to you.

Practice 3: what does a dark-mode switch log? The same sequence as rotation: onPause, onStop, onSaveInstanceState, onDestroy isChangingConfigurations=true, then a new onCreate. Dark mode is a configuration change too.

Practice 5: what does am kill do to a foreground app? Nothing. In the run for this lesson pidof printed the same pid before and after. It only kills processes Android considers safe to kill, which is why you press Home first.

Practice 6: what did configChanges fix? Rotation only: with it, rotating logged no lifecycle lines and the screen kept "checked 2 times". Process death still resets the count, and Back still (correctly) starts fresh.

Practice

  1. Build the demonstration in a scratch copy of the project, run it, and reproduce each of the four scenarios. Compare your log lines with the ones above. The pids, hashes and times will differ; the order should not.
  2. Rotate twice in a row and confirm there are two different activity hashes after the first one.
  3. Switch the emulator to dark mode (from the quick settings) while the app is open. Predict the log lines, then check.
  4. Press Home, then run adb shell am kill com.riztechacademy.mandibhav, then open the app from Recents. Confirm the pid changed and restored=true.
  5. Break it on purpose: run am kill while the app is in front. Nothing happens, because Android does not kill a foreground app this way. Read what pidof com.riztechacademy.mandibhav prints before and after.
  6. Harder: add android:configChanges="orientation|screenSize" to MainActivity in the manifest. Rotate and see the count survive, with no lifecycle lines logged. Now repeat practice 4. Write down which of the four scenarios the "fix" actually fixed, then remove it.

Official documentation

Next: resources, the reason Android recreates activities in the first place, and how to get Mandi Bhav into Hindi and Marathi.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship