Skip to content
Commitwise
Menu
Android Development

Activity lifecycle and configuration changes

Know when Android creates, pauses, stops and destroys your Activity, what survives a rotation and what survives the system killing your app, and how to run work only while the screen is visible, in Kotlin with Java where it helps.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

The user fills in half a form, rotates the phone, and the form is empty. Another user switches to the camera app to take a photo for your app, comes back, and your app has restarted on the home screen. A third leaves your app in the background, and the location icon stays on all evening. These are three different bugs with one cause: Android decides when your screen lives and dies, and the code assumed otherwise.

New to some of these? Open what you need, or skip ahead.

Basics:What is an Activity?

An Activity is an Android app screen, or, in modern Compose apps, often the single container that hosts all the screens. Android creates it, calls its methods (onCreate, onStart, …) as the user moves around, and destroys it when it’s no longer needed.

Basics:What is a ViewModel?

A ViewModel is an object that holds a screen’s state and logic, separate from the UI code. On Android, the system keeps the same ViewModel instance when the Activity is recreated after a rotation, so data survives. The ViewModel and unidirectional data flow tutorial covers it in detail.

Basics:What are coroutines?

Coroutines are Kotlin’s way of writing code that waits (for the network, a database, a timer) without blocking the screen. launch { … } starts one; suspend functions can pause and resume. Every coroutine runs in a scope that decides how long it may live: lifecycleScope lives as long as an Activity, viewModelScope as long as a ViewModel. See Coroutines and Flow.

Part Not started

The lifecycle callbacks

Think of a theater stage. An actor is hired (created), walks onto the stage (started, visible), and steps into the spotlight (resumed, the one the audience interacts with). Then they step out of the spotlight, walk off stage, and eventually leave the theater. Android tells your Activity at each of those moments by calling one of its methods.

The lifecycle is a state machine (INITIALIZED → CREATED → STARTED → RESUMED and back down to DESTROYED) exposed through callbacks and, more usefully, through Lifecycle observers and lifecycle.currentState. The practical rules: pair start/stop at the same level (onStart/onStop for “while visible”, onResume/onPause for “while in front”), keep onPause fast (the next Activity waits for it), and never assume onDestroy runs (process death skips it).

Activity lifecycle

onPause, onStoponRestart, onStartCreatedonCreateStartedonStart / onStopResumedonResume / onPauseStoppednot visibleDestroyedonDestroyProcess killedno callback

Tap a box to see what it does.

class MapActivity : ComponentActivity() {
    private val viewModel: MapViewModel by viewModels()
    private lateinit var locationClient: LocationClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)             // set up once
        locationClient = LocationClient(this)
        setContent { MapScreen(viewModel) }
    }

    override fun onStart() {
        super.onStart()                                // now visible: start live location
        locationClient.start { viewModel.onLocation(it) }
    }

    override fun onStop() {
        locationClient.stop()                          // not visible anymore: stop it
        super.onStop()
    }
}
public class MapActivity extends AppCompatActivity {
    private MapViewModel viewModel;
    private LocationClient locationClient;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);             // set up once
        setContentView(R.layout.activity_map);
        viewModel = new ViewModelProvider(this).get(MapViewModel.class);
        locationClient = new LocationClient(this);
    }

    @Override protected void onStart() {
        super.onStart();                                // now visible: start live location
        locationClient.start(viewModel::onLocation);
    }

    @Override protected void onStop() {
        locationClient.stop();                          // not visible anymore: stop it
        super.onStop();
    }
}

The rule that prevents most lifecycle bugs: whatever you start in one callback, stop in its partner. onStart ↔ onStop, onResume ↔ onPause. Location, sensors, live data from a server and animations usually belong to onStart/onStop: they run while the user can see the screen and stop when they can’t.

Predict the output

Activity A is on screen. The user taps a button that opens Activity B, then presses Back. Which order of A's callbacks is right?

Practice: log it yourself

  1. Override every callback in a test Activity and log its name: Log.d("Lifecycle", "onStart").
  2. Open another screen, press Back, press Home, come back, rotate the phone, switch dark mode.
  3. Watch Logcat filtered by “Lifecycle”. Rotation and dark mode show a full onDestroy → onCreate: the Activity is recreated.
  4. In Developer options, turn on Don’t keep activities and repeat: every Activity is destroyed as soon as it’s in the background, a quick way to find state that’s lost.

Lifecycle-aware components

Overriding callbacks in every Activity scales poorly. LifecycleObserver/DefaultLifecycleObserver let a component manage itself (lifecycle.addObserver(locationClient) with onStart/onStop inside it), and in Compose, LifecycleStartEffect / LifecycleResumeEffect and collectAsStateWithLifecycle() tie work to the host lifecycle without touching the Activity. Prefer these over callbacks in screens: the start/stop pair lives in one place, and it’s reusable across Activities and Fragments.

  • States: CREATED → STARTED (visible) → RESUMED (focused, interactive), mirrored on the way down. Pair work at the same level: onStart/onStop for visibility, onResume/onPause for focus (camera, exclusive resources). Keep onPause cheap.
  • A started-but-not-resumed Activity is visible (multi-window, dialogs, transparent activities on top).
  • onDestroy isn’t guaranteed (process death). Use DefaultLifecycleObserver, LifecycleStartEffect, collectAsStateWithLifecycle instead of per-Activity overrides.

Check this part

  1. Where should live location updates for a map screen start and stop?

  2. Activity A opens Activity B. What happens to A?

  3. Why can't cleanup rely on onDestroy?

Android moves an Activity through created, started (visible), resumed (in front, interactive), then back through paused, stopped and destroyed, calling onCreate, onStart, onResume, onPause, onStop and onDestroy. Start things in the callback that matches when they're needed, and stop them in its pair.

Part Not started

Rotation: the Activity starts over

When the phone rotates (or dark mode, the language or the font size changes), Android destroys the Activity and creates a new one with the new configuration, so layouts, strings and colors are picked again. It’s like the stage being rebuilt between two scenes: anything the actor was holding in their hands is gone unless it was put somewhere safe.

A configuration change recreates the Activity (and its Fragments and composition) so resources are re-resolved. The ViewModel store is retained across it (ViewModelStore survives via non-config instance), saved instance state is written to a Bundle and restored, and everything else (fields, remember, in-flight work scoped to the Activity) is discarded. Opting out with android:configChanges is reserved for special cases (video, games); it makes you responsible for re-applying resources and doesn’t help with process death.

Where the state livesSurvives rotation?Survives process death?
A field in the ActivityNoNo
remember { } in ComposeNoNo
rememberSaveable { } / onSaveInstanceStateYesYes (small values only)
A ViewModel propertyYesNo
SavedStateHandle in a ViewModelYesYes
A database, DataStore, a fileYesYes
Spot the bugThe profile reloads on every rotation

Tap the line with the bug.

class ProfileActivity : ComponentActivity() {
    private var profile by mutableStateOf<Profile?>(null)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            profile = api.loadProfile()
        }
        setContent { ProfileScreen(profile) }
    }
}
public class ProfileActivity extends AppCompatActivity {
    private Profile profile;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_profile);
        api.loadProfile(result -> {
            profile = result;
            render(profile);
        });
    }
}

Small UI state: rememberSaveable

Not everything belongs in a ViewModel. Whether a section is expanded, which tab is selected, the text of a search field before it’s submitted: that’s UI state, and Compose keeps it across rotation with rememberSaveable:

@Composable
fun FaqItem(question: String, answer: String) {
    var expanded by rememberSaveable { mutableStateOf(false) }   // survives rotation and process death
    Column(Modifier.clickable { expanded = !expanded }) {
        Text(question, style = MaterialTheme.typography.titleMedium)
        if (expanded) Text(answer)
    }
}

remember keeps a value while the composable stays on screen; rememberSaveable also writes it to the saved-state Bundle, so it comes back after the Activity is recreated. Only small values fit there (the whole Bundle is limited to roughly a few hundred KB, shared by the whole screen): numbers, short strings, ids, never lists of data or images.

Exerciseeasy

A form that survives rotation

A “Contact us” screen has a name field, an email field and a message box. Make everything the user typed survive rotation, dark mode switching and the app being killed in the background, without saving to the database.

›

Typed text is small UI state. Which Compose tool keeps state across both recreation and process death?

SolutionTry it yourself first
@Composable
fun ContactForm(onSend: (name: String, email: String, message: String) -> Unit) {
    var name by rememberSaveable { mutableStateOf("") }
    var email by rememberSaveable { mutableStateOf("") }
    var message by rememberSaveable { mutableStateOf("") }

    Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
        OutlinedTextField(name, { name = it }, label = { Text("Name") })
        OutlinedTextField(email, { email = it }, label = { Text("Email") })
        OutlinedTextField(message, { message = it }, label = { Text("Message") }, minLines = 4)
        Button(onClick = { onSend(name, email, message) }, enabled = name.isNotBlank() && "@" in email) {
            Text("Send")
        }
    }
}

Test it: type something, rotate, switch dark mode; then press Home, run adb shell am kill <your.package> (which simulates the system killing a background app) and reopen the app from Recents. The text should still be there. If the form lived in a ViewModel instead, you’d use SavedStateHandle for the same result (see the next part).

Configuration changes beyond rotation

Font scale, locale, dark mode, window size (foldables, split screen, freeform windows on tablets and ChromeOS), keyboard availability and density changes all recreate by default. On large screens and foldables, resizing is frequent, so recreation must be cheap: no work in onCreate that a ViewModel could hold, no synchronous I/O, and layout chosen from WindowSizeClass rather than orientation. Android 16 targets ignore orientation and resizability restrictions on large screens, so “portrait only” is no longer a way to avoid the problem.

  • Configuration changes (rotation, dark mode, locale, font scale, window size) recreate the Activity; ViewModel and saved state survive, fields, remember and Activity-scoped work don’t.
  • Hold data and in-flight work in the ViewModel (viewModelScope), UI state in rememberSaveable; avoid android:configChanges except for special cases.
  • Large screens resize often; recreation must be cheap; layout by WindowSizeClass, not orientation.

Check this part

  1. What happens to the Activity when the phone is rotated?

  2. Which survives a rotation?

  3. A network request started in Activity.onCreate. What happens on rotation?

Rotating the screen, switching dark mode or changing the language destroys the Activity and creates a new one. Fields and remember are lost; ViewModel survives; small UI state belongs in rememberSaveable or saved instance state. Work started in onCreate restarts unless it lives in a ViewModel.

Part Not started

When the whole app is killed

When memory is low, Android kills apps that are in the background, without asking and without calling onDestroy. When the user comes back from Recents, they expect to find your app exactly where they left it. Android recreates the screen they were on and hands it the saved state; everything else that was in memory, the ViewModel included, is gone. This is called process death, and it happens all the time on real phones, especially when users switch to the camera or a banking app.

Process death is the lifecycle event most apps get wrong because it never happens in development (plenty of RAM, debugger attached). On return, the system recreates the task’s Activities and restores their saved-instance Bundles, including SavedStateHandle and rememberSaveable entries; ViewModels are new, singletons are re-initialized, and any in-memory cache or session is empty. The design rule: the UI must be reconstructible from saved ids plus persisted data, and nothing may assume a previous screen’s in-memory state.

class SearchViewModel(
    private val savedState: SavedStateHandle,
    private val repository: SearchRepository,
) : ViewModel() {
    val query: StateFlow<String> = savedState.getStateFlow("query", "")    // saved in the Bundle

    val results: StateFlow<List<Item>> = query
        .debounce(300)
        .mapLatest { q -> if (q.isBlank()) emptyList() else repository.search(q) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

    fun onQueryChange(q: String) { savedState["query"] = q }
}
public class SearchViewModel extends ViewModel {
    private final SavedStateHandle savedState;

    public SearchViewModel(SavedStateHandle savedState) {
        this.savedState = savedState;
    }

    public LiveData<String> query() {
        return savedState.getLiveData("query", "");        // saved in the Bundle
    }

    public void onQueryChange(String q) { savedState.set("query", q); }
}

The search query (small) is saved; the results (large) are not, because they can be loaded again from the query. That’s the general recipe: save the minimum needed to rebuild the screen (ids, text the user typed, the selected tab), and reload the rest.

To test it: open the screen, type a query, press Home, then run adb shell am kill your.package.name (or turn on Don’t keep activities for a lighter version) and return through Recents.

Production puzzles

Predict the output

A checkout screen keeps: (a) the cart id in SavedStateHandle, (b) the loaded cart in a ViewModel StateFlow, (c) the 'gift wrap' checkbox in remember, (d) the coupon field text in rememberSaveable. The user switches to their banking app, Android kills your process, and they return. What's still there?

Spot the bugGPS that keeps running in the background

Tap the line with the bug.

class RunTrackerActivity : ComponentActivity() {
    private val viewModel: RunViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            locationRepository.locations.collect { viewModel.onLocation(it) }
        }
        setContent { RunScreen(viewModel) }
    }
}
public class RunTrackerActivity extends AppCompatActivity {
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_run);
        locationRepository.addListener(location -> viewModel.onLocation(location));
    }
}

Exercisemedium

Restoring a multi-step checkout

A checkout has four steps: address, delivery slot, payment, review. Users often leave at the payment step to look up a card in their banking app, and Android kills the app. Today they come back to an empty address step. Design what’s saved where, how each step rebuilds itself, and how you’d test it.

›

Separate what the user decided (small, must survive) from data that can be fetched again (slots, cart contents, prices). Also think about data that must never be saved in a Bundle.

SolutionTry it yourself first
  • Navigation state: use Navigation Compose with the current step as part of the back stack; it’s saved and restored automatically, so the user returns to the payment step.
  • User decisions: the selected address id, the chosen slot id, the “gift wrap” flag, coupon text: in SavedStateHandle of a checkout-scoped ViewModel (a navigation graph-scoped ViewModel shared by the four steps), and also persisted to a local draft (DataStore/Room) so they survive the app being swiped away.
  • Re-fetchable data: cart contents, slot availability and prices are reloaded from the saved ids on restore; the review step shows “prices updated” if anything changed since.
  • Never in the Bundle: card numbers, CVC and tokens. The payment provider’s SDK owns card entry; after process death the user re-enters the CVC, which is expected and safe. Also keep Bundle size small (no lists, no images); TransactionTooLargeException crashes come from oversized saved state.
  • Testing: an instrumented test that drives to the payment step, calls ActivityScenario.recreate() for configuration changes, and a manual/automated check with adb shell am kill while the app is in the background; plus “Don’t keep activities” in QA builds.
  • Process death recreates Activities with their saved Bundles; ViewModels, singletons and caches are new. Never reproduce it with the debugger attached; use adb shell am kill from the background or “Don’t keep activities”.
  • Save minimal, reconstructive state (ids, user input) in SavedStateHandle/rememberSaveable; persist important drafts; reload data from ids. Keep Bundles small; never put secrets there.
  • Collect with repeatOnLifecycle(STARTED) / collectAsStateWithLifecycle; lifecycleScope.launch alone runs until destroy. Background work that must continue is a foreground service or WorkManager.

Check this part

  1. What is process death?

  2. Which survives process death?

  3. How long does lifecycleScope.launch { flow.collect { … } } collect?

When your app is in the background, Android can kill its process to free memory; the user returns to a recreated screen with nothing in memory, ViewModel included. SavedStateHandle and rememberSaveable restore small state; everything important must also be persisted. Collect flows only while the screen is started.

Key takeaways

  • Android calls onCreate, onStart, onResume, onPause, onStop and onDestroy; start work in one and stop it in its partner.
  • Rotation and dark mode destroy and recreate the Activity: keep data in a ViewModel and small UI state in rememberSaveable.
  • Android can kill a background app at any time; only saved state (SavedStateHandle, rememberSaveable) and persisted data come back.
  • Collect flows only while the screen is started.
  • Pair work by lifecycle level; prefer lifecycle-aware components over per-Activity overrides.
  • Recreation must be cheap: ViewModel-held work, no I/O in onCreate, layout by window size.
  • Design for process death: reconstructible screens from saved ids plus persisted data; test with am kill.
  • repeatOnLifecycle/collectAsStateWithLifecycle for UI collection; services or WorkManager for real background work.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.