Activities, lifecycle, tasks, and back stack
Activity and task solve different problems
- Activity: a system-managed user-interface entry point.
- Task: a user journey represented by an Activity back stack and shown in Recents.
Task: “buy a product”
[CatalogActivity] → [ProductActivity] → [CheckoutActivity]
bottom top
Back normally removes the top destination and reveals the previous one. Home normally moves the task to background. Neither operation necessarily kills the app process.
Lifecycle through a concrete journey
class CheckoutActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state) // Construct UI and restore inputs.
}
override fun onStart() {
super.onStart() // Visible.
}
override fun onResume() {
super.onResume() // Foreground and interactive.
}
override fun onPause() {
// Lose foreground interaction; finish quickly.
super.onPause()
}
override fun onStop() {
// No longer visible.
super.onStop()
}
override fun onDestroy() {
// Instance cleanup; not guaranteed process-final callback.
super.onDestroy()
}
}
If a permission dialog or translucent Activity covers part of the screen, exact callback transitions can differ from a complete navigation away. Design from visibility/interaction requirements and test the real flow.
Rotation versus finish versus process death
| Cause | New Activity later? | ViewModel | Saved state | Meaning |
|---|---|---|---|---|
| Configuration change | Immediately | Usually retained | Available | Same logical destination |
Back/finish() | Normally no | Cleared | Not restoration contract | User left destination |
| Process death while stopped | If user returns | Gone | Small restorable state may exist | System reclaimed memory |
State ownership
data class CheckoutUiState(
val orderId: String,
val total: Money,
val submitting: Boolean,
val error: String?
)
- Activity/Compose renders and forwards events.
- ViewModel coordinates screen state across configuration recreation.
- SavedStateHandle stores small restoration inputs.
- Repository/database stores durable order truth.
The Activity should not become the database, network client, and business-rule engine simply because callbacks arrive there.
Default back-stack behaviour
standard Activity launch normally pushes a new instance:
A → B → B
This is legal. If duplicates are wrong for product UX, first check navigation event duplication. Launch modes are not a substitute for fixing double clicks or repeated effects.
Launch modes
| Mode | Reuse rule | Key implication |
|---|---|---|
standard | Always new instance | Default and predictable |
singleTop | Reuse only if target already on top | New Intent goes to onNewIntent() |
singleTask | One task-root-style instance; routes/clears according to task rules | Strong global task effects |
singleInstance | Isolated task behaviour | Rare; surprising UX |
singleInstancePerTask | One instance per task | Specialized document/task use |
Most apps should use default behaviour and Navigation APIs. Advanced launch modes change system task semantics and require tests from launcher, notification, deep link, external app, and Recents.
singleTop corner case
<activity
android:name=".ProductActivity"
android:launchMode="singleTop" />
class ProductActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
consumeProductId(intent)
}
}
If ProductActivity is already on top, Android does not call onCreate() again. Failing to handle onNewIntent() displays stale product data.
Intent flags interact with launch mode
NEW_TASKasks Android to choose/create an appropriate task.SINGLE_TOPreuses only a matching top instance.CLEAR_TOPremoves Activities above an existing target; astandardtarget may itself be recreated.
Avoid memorizing a slogan like “CLEAR_TOP always reuses.” Predict the starting stack, target launch mode, flags, and expected final stack—then test.
Deep-link cold start
Notification opens myapp://orders/ORD-42 with no process/task.
❌ Avoid dependency on previous screen
val order = CatalogActivity.selectedOrder
✅ Prefer reconstructable routing
val orderId = intent.data?.lastPathSegment
?.takeIf { it.matches(Regex("ORD-[0-9]+")) }
?: return finish()
viewModel.load(orderId)
Then apply authentication, authorization, record-exists, and back-stack policy.
Back versus Up
- Back returns through actual user history across apps/tasks.
- Up navigates within the app’s information hierarchy.
They often coincide, but an external deep link can make them different. Up may build/navigate to a parent destination even when that parent was not in the actual history.
Predictive Back
Modern Android can preview the destination while the gesture progresses. Use AndroidX back APIs/Navigation integration rather than arbitrary onBackPressed() overrides. Custom handling must remain state-consistent and allow system fallback.
An Activity is a UI entry point; a task is the user’s Activity history represented in Recents. Lifecycle callbacks describe visibility and interaction, not guaranteed process lifetime. Default standard launches push instances; launch modes and flags can reuse or reshape tasks, so reused Activities must handle onNewIntent. I use stable route IDs and durable repositories so launcher, notification, deep-link, and process-death entries all reconstruct correctly.
Exercises
- Predict stacks for A→B→C then
CLEAR_TOPlaunch of B with standard and singleTop B. - Design Back and Up after an external order deep link.
- Why can fixing duplicate navigation with
singleTaskcreate worse bugs?