Intents, PendingIntents, Task Stack & Launch Modes
Navigate Android's multi-screen model — critical for deep linking and notification handling
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Intents are Android's inter-component messaging system. Explicit intents target specific components; implicit intents describe capabilities and let the system route them. PendingIntents are deferred, delegated intents used by notifications and alarms. Launch modes and task affinity control how Activities stack and whether instances are reused — critical for correct behavior in notification deep linking, multi-window, and back navigation.
Real-world relevance
In a SaaS collaboration app, tapping a push notification for 'You were mentioned in Project Alpha' must deep-link to the correct ProjectDetailActivity. If the app is already open on ProjectDetailActivity for a different project, singleTop ensures a new instance is created (different data). A PendingIntent (FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT) carries the projectId extra so the notification click always shows the right project. The NotificationHelper class creates unique requestCodes per project ID to ensure PendingIntents don't collide.
Key points
- Explicit intents — Specify the exact component to start: Intent(this, DetailActivity::class.java). Used for starting Activities, Services, and BroadcastReceivers within your own app. Always use explicit intents for internal navigation — implicit intents for internal components are a security risk since other apps can intercept them.
- Implicit intents — Describe an ACTION and optional DATA/CATEGORY — Android resolves which component handles it. ACTION_VIEW with a geo: URI opens Maps. ACTION_SEND opens a share dialog. ACTION_DIAL opens the dialer. The system checks all apps' IntentFilters in manifests and shows a chooser if multiple match.
- Intent flags — FLAG_ACTIVITY_NEW_TASK: start Activity in a new task (required when starting from non-Activity context). FLAG_ACTIVITY_CLEAR_TOP: if Activity already in stack, destroy everything above it and call onNewIntent(). FLAG_ACTIVITY_SINGLE_TOP: don't create new instance if already at top of stack. Flags and launch modes often overlap — flags take precedence at runtime.
- PendingIntent — Grants another app (System, NotificationManager, AlarmManager) permission to perform an Intent as if it were your app. Created with PendingIntent.getActivity(), getService(), getBroadcast(). The requestCode distinguishes multiple PendingIntents. FLAG_IMMUTABLE (required on API 23+ for notifications) means the extra cannot be changed by the recipient.
- PendingIntent flags — FLAG_UPDATE_CURRENT: keep existing PendingIntent but replace its extras. FLAG_CANCEL_CURRENT: cancel existing, create new. FLAG_ONE_SHOT: can only be used once. FLAG_IMMUTABLE (API 23+): system cannot modify the intent. FLAG_MUTABLE: allows system to fill in components (required for bubbles and widgets that need fillInIntent).
- Task and back stack — A Task is a stack of Activities — user presses Back to pop the stack. By default each app launch creates a new Task for the root Activity. Tasks can be moved to background (Home button) and resumed. Multiple Tasks can exist — switching apps switches Tasks. Each Task has its own back stack.
- Launch modes — standard — Default. Every startActivity() creates a new instance. Stack can have multiple instances of the same Activity. A → B → A → B → B creates a stack of 5. Good for independent screens like product detail pages where each instance has different data.
- Launch modes — singleTop — If an instance is already at the TOP of the current task's stack, reuse it and call onNewIntent(). If it's NOT at top, create a new instance. Useful for notification deep links into an already-open detail screen — avoids duplicate instances when user taps notification repeatedly.
- Launch modes — singleTask — Only one instance exists in the entire system. If already exists, Android brings its task to foreground, pops everything above it, and calls onNewIntent(). Used for main/hub Activities. WhatsApp's main chat list is singleTask — no matter where you come from, there's one instance.
- Launch modes — singleInstance — Like singleTask but the Activity lives alone in its own Task — no other Activities can be pushed onto its task. Starting a new Activity from singleInstance creates it in a new Task. Rarely used — telephony-related screens (incoming call UI) are the canonical example.
- taskAffinity — Defines which task an Activity 'belongs to'. Default is the app's package name. Set a custom taskAffinity to make an Activity prefer a different task. Combined with FLAG_ACTIVITY_NEW_TASK, forces an Activity into a specific task. Used for multi-window scenarios and document-centric apps.
- onNewIntent() — Called when a singleTop or singleTask Activity receives a new Intent while already in the stack. The Activity is NOT recreated — you get the new Intent via this callback. Must call setIntent(intent) if you want getIntent() to return the new intent later. Forgetting this is a common bug in notification handling.
Code example
// Explicit intent — internal navigation with extras
fun openWorkOrderDetail(context: Context, orderId: String) {
val intent = Intent(context, WorkOrderDetailActivity::class.java).apply {
putExtra(WorkOrderDetailActivity.EXTRA_ORDER_ID, orderId)
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
context.startActivity(intent)
}
// Implicit intent — share content
fun shareReport(context: Context, reportText: String) {
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, "Field Operations Report")
putExtra(Intent.EXTRA_TEXT, reportText)
}
context.startActivity(Intent.createChooser(shareIntent, "Share Report"))
}
// PendingIntent for notification deep link
fun buildNotificationPendingIntent(
context: Context,
projectId: String,
mentionId: String
): PendingIntent {
val deepLinkIntent = Intent(context, ProjectDetailActivity::class.java).apply {
putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, projectId)
putExtra(ProjectDetailActivity.EXTRA_MENTION_ID, mentionId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
return PendingIntent.getActivity(
context,
projectId.hashCode(), // Unique requestCode per project
deepLinkIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
// singleTop Activity handling onNewIntent
class ProjectDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent(intent)
}
// Called when singleTop reuses existing instance
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // IMPORTANT: update getIntent() result
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
val projectId = intent.getStringExtra(EXTRA_PROJECT_ID) ?: return
val mentionId = intent.getStringExtra(EXTRA_MENTION_ID)
viewModel.loadProject(projectId, mentionId)
}
companion object {
const val EXTRA_PROJECT_ID = "project_id"
const val EXTRA_MENTION_ID = "mention_id"
}
}Line-by-line walkthrough
- 1. openWorkOrderDetail uses an explicit Intent with the target class specified directly — no ambiguity, no interception risk. FLAG_ACTIVITY_SINGLE_TOP avoids duplicating the detail screen if it's already at the top.
- 2. shareReport uses ACTION_SEND with type 'text/plain' — an implicit intent. The system queries all installed apps for IntentFilters matching this action+type and shows a chooser. createChooser wraps it to always show the dialog even if only one app matches.
- 3. buildNotificationPendingIntent creates a PendingIntent.getActivity() — when the notification is tapped, it's equivalent to the user calling startActivity with deepLinkIntent from YOUR app's context.
- 4. projectId.hashCode() as requestCode gives each project a stable, unique integer. Two PendingIntents with the same requestCode and same action but different extras might be considered equal by the system — unique requestCodes prevent this.
- 5. FLAG_UPDATE_CURRENT means if a PendingIntent with this requestCode already exists (from a previous notification for the same project), its extras are updated with the new mentionId instead of creating a duplicate.
- 6. FLAG_IMMUTABLE prevents any recipient (system or other app) from modifying the Intent before delivery — security best practice required on API 31+.
- 7. In ProjectDetailActivity.onCreate, handleIntent(intent) processes the launch intent — this covers the cold-start-from-notification case.
- 8. onNewIntent is called when singleTop reuses the existing instance. setIntent(intent) is crucial — without it, getIntent() still returns the original launch intent, and any code elsewhere that calls getIntent() later will use stale data.
Spot the bug
// Notification helper
fun createMentionNotification(context: Context, mentionId: String, text: String) {
val intent = Intent(context, MentionActivity::class.java).apply {
putExtra("mention_id", mentionId)
}
val pendingIntent = PendingIntent.getActivity(
context,
0, // bug 1
intent,
PendingIntent.FLAG_UPDATE_CURRENT // bug 2 on API 31+
)
// ... build and post notification
}
// MentionActivity — declared singleTop in manifest
class MentionActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mentionId = intent.getStringExtra("mention_id")
loadMention(mentionId)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// bug 3 — missing line
val mentionId = intent.getStringExtra("mention_id")
loadMention(mentionId)
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Intents and Intent Filters — Android Developers (Android Developers)
- PendingIntent — Android Developers Reference (Android Developers)
- Tasks and the Back Stack (Android Developers)
- Android Launch Modes — A Visual Guide (Medium / Android Developers)
- Create and manage notification channels (Android Developers)