Lifecycle and app components cheat sheet

The 18 Lifecycle and app components interview answers in 30 seconds each, plus quick facts. Printable, free.

One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions

Rules of thumb

  • A opens B: A.onPause, B created/started/resumed, then A.onStop.
  • Configuration changes recreate the Activity; process death recreates the process.
  • Save IDs and small input in SavedStateHandle; everything else on disk.
  • Use viewLifecycleOwner for anything that touches a Fragment's views.
  • FLAG_IMMUTABLE for PendingIntents unless the receiver must fill data.
  • Foreground services need a type; deferrable work goes to WorkManager.

The Activity lifecycle

onCreate sets up the UI and reads saved state. onStart means visible; onResume means in front and taking input. onPause means losing focus, which also happens in multi-window or when a dialog-style Activity appears. onStop means no longer visible, and onDestroy means finishing or being recreated. Keep onPause fast; save work in onStop or continuously.

Lifecycle order: A opens B

A pauses first, then B is created, started and resumed, and only then is A stopped: A.onPause, B.onCreate, B.onStart, B.onResume, A.onStop. A is paused before B appears so it can release the foreground, and stopped after B is visible. If B is transparent or dialog-themed, A stays visible, so it is paused but never stopped.

Configuration changes

By default, the Activity is destroyed and recreated with new resources for the new configuration: orientation, screen size, locale, dark mode or font scale. ViewModels survive; views and remember state don't; rememberSaveable and saved instance state are restored. The right fix is to keep state in the right holder, not to block recreation.

Process death

When an app is in the background, Android can kill its process to free memory. When the user returns, Android creates a new process and recreates the last Activity with its saved instance state, so it looks as if the app never left. ViewModels, singletons and in-memory caches are gone. Only saved state (SavedStateHandle, rememberSaveable) and disk survive.

SavedStateHandle

SavedStateHandle is a key-value map passed into a ViewModel that is saved into the Activity's saved state and restored after process death. Put small values that let you rebuild the screen: IDs, the current step, filter choices, unsaved input. It also carries navigation arguments into the destination's ViewModel.

Fragment and view lifecycles

A Fragment object can outlive its view: on the back stack, the Fragment stays alive while its view is destroyed and later recreated. So it has its own lifecycle and a separate viewLifecycleOwner. Anything that touches views, such as collecting UI state or binding, must use viewLifecycleOwner, or it keeps a reference to a dead view and may observe twice.

class FeedViewModel : ViewModel() {
    val items: StateFlow<List<String>> = MutableStateFlow(emptyList())
}

How a ViewModel survives

Each Activity, Fragment and navigation entry is a ViewModelStoreOwner with a ViewModelStore, a map of ViewModels. On a configuration change, ComponentActivity keeps that store across the recreation and hands it to the new instance, so by viewModels() finds the existing ViewModel. When the owner finishes for good, the store is cleared and each ViewModel's onCleared runs.

Launch modes and tasks

standard creates a new instance every time. singleTop reuses the instance only if it's already on top, calling onNewIntent. singleTask keeps one instance in its task and clears anything above it. singleInstance puts it alone in its own task. singleInstancePerTask, added in Android 12, allows one instance per task. Flags like FLAG_ACTIVITY_CLEAR_TOP change this per launch.

Explicit and implicit intents

An explicit intent names the component to start, usually inside your app. An implicit intent describes an action and data, like ACTION_VIEW with a URL, and the system finds apps whose intent filters match. Since Android 11, apps can't see most other installed apps by default, so queries about which apps can handle an intent need a <queries> entry.

fun openLink(context: Context, url: String) {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    try {
        context.startActivity(intent)
    } catch (e: ActivityNotFoundException) {
        Toast.makeText(context, "No browser installed", Toast.LENGTH_SHORT).show()
    }
}

PendingIntent and mutability

A PendingIntent is a token that lets another app or the system, such as a notification or an alarm, fire an intent later with your app's identity and permissions. Since Android 12 you must say whether others can change it. Use FLAG_IMMUTABLE almost always; use FLAG_MUTABLE only when the receiver must fill in data, like inline reply or bubbles.

The Activity Result API

Register a launcher with registerForActivityResult(contract) { result -> } during setup, then call launcher.launch(input) when needed. Built-in contracts cover permissions, picking media, taking pictures and starting an Activity for a result. It replaces startActivityForResult and onActivityResult, which are deprecated, and survives process death because results go to the registered callback.

Services and foreground service types

Use a foreground service for work the user is aware of and that must continue now, such as playing music, navigation or an active call. It must show a notification and, since Android 14, declare a foreground service type with the matching permission. For deferrable background work, use WorkManager instead. Background apps can't start foreground services except in specific allowed cases.

class RunService : Service() {
    override fun onBind(intent: Intent?): IBinder? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = NotificationCompat.Builder(this, "run")
            .setContentTitle("Recording your run")
            .setSmallIcon(android.R.drawable.ic_menu_mylocation)
            .build()
        ServiceCompat.startForeground(this, 1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
        return START_STICKY
    }
}

Broadcast receivers

A BroadcastReceiver reacts to system or app events. Since Android 8, most implicit broadcasts can't be received by receivers declared in the manifest; register them at runtime while your app runs instead. Since Android 14, runtime receivers must say whether they're exported with RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED, unless they only receive system broadcasts. onReceive runs on the main thread and must finish quickly.

fun listenForPowerSave(context: Context, onChange: () -> Unit): BroadcastReceiver {
    val receiver = object : BroadcastReceiver() {
        override fun onReceive(c: Context, intent: Intent) = onChange()
    }
    ContextCompat.registerReceiver(
        context, receiver,
        IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED),
        ContextCompat.RECEIVER_NOT_EXPORTED
    )
    return receiver
}

Content providers

A ContentProvider exposes structured data to other apps through content:// URIs with query, insert, update and delete, and enforces permissions on it. You need one to share data with other apps, for search suggestions, or to serve files through FileProvider. For data used only inside your app, a Room database or repository is simpler.

Runtime permissions

Ask at the moment the user tries the feature, not at launch. Check with ContextCompat.checkSelfPermission; if denied, show a short explanation when shouldShowRequestPermissionRationale is true, then request with the Activity Result API. If the user denies twice, the system stops showing the dialog, so offer a link to the app's settings instead.

@Composable
fun EnableAlertsButton(onGranted: () -> Unit) {
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
        if (granted) onGranted()
    }
    Button(onClick = { launcher.launch(Manifest.permission.POST_NOTIFICATIONS) }) {
        Text("Turn on alerts")
    }
}

Application class and startup work

Application.onCreate runs before any Activity on every cold start, including starts for a broadcast or a job. Only put work there that every entry point needs immediately, like dependency injection setup and crash reporting. Defer everything else: initialise lazily on first use, with App Startup, or after the first frame. Every millisecond there delays the first screen.

TransactionTooLargeException

Intents, saved instance state and other cross-process calls go through Binder, whose transaction buffer is about 1 MB shared by all in-flight transactions in the process. Putting large data in an Intent extra or a saved Bundle, such as a bitmap or a long list, can exceed it, and the app crashes, often when the Activity stops and saves state. Pass IDs and load data from disk or a repository.

Parcelable vs Serializable

Serializable is the standard Java interface: easy to add, but it uses reflection and is slow and produces large output. Parcelable is Android's own interface, designed for fast Binder transfer, and the @Parcelize plugin generates the code for you. For Intents and saved state, use @Parcelize. For storage or network, use kotlinx.serialization or JSON, not either of these.

@Parcelize
data class Filter(val query: String, val minPrice: Int) : Parcelable

Quick facts

onSaveInstanceState timing
On Android 9 (API 28) and later, onSaveInstanceState runs after onStop.
Exported components
Since Android 12, every component with an intent filter must set android:exported.
Package visibility
Since Android 11, apps see only some other apps by default; declare <queries> for the intents you check.
Notification permission
POST_NOTIFICATIONS became a runtime permission in Android 13 (API 33).
Foreground service types
Android 14 requires each foreground service to declare a type, such as location or mediaPlayback.