Lesson 14 of 83 intermediate

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

An Intent is like a taxi request — you either specify the exact destination (explicit intent: 'take me to MainActivity') or describe what you need (implicit intent: 'I need a camera'). A PendingIntent is like a pre-paid taxi voucher you give to someone else — they can use it later on your behalf. Launch modes are the hotel's room assignment policy — standard gives a new room every time, singleTop reuses your current room if you're already in it, singleTask gives you a private suite no matter where you came from.

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

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. 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. 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. 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. 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. 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. 6. FLAG_IMMUTABLE prevents any recipient (system or other app) from modifying the Intent before delivery — security best practice required on API 31+.
  7. 7. In ProjectDetailActivity.onCreate, handleIntent(intent) processes the launch intent — this covers the cold-start-from-notification case.
  8. 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?
Think about PendingIntent collisions between different mentions, API 31 security requirements, and stale intent data.
Show answer
Bug 1: requestCode is always 0 — all mention notifications share the same PendingIntent slot. When you post notification for mention-B, it overwrites the PendingIntent for mention-A. Tapping mention-A's notification now opens mention-B. Fix: use mentionId.hashCode() as the requestCode. Bug 2: On API 31+ (Android 12+), PendingIntents must include FLAG_IMMUTABLE or FLAG_MUTABLE. Missing this flag causes an IllegalArgumentException crash at PendingIntent.getActivity(). Fix: PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE. Bug 3: onNewIntent does NOT call setIntent(intent) — if any other code in this Activity calls getIntent() after onNewIntent, it still gets the original launch intent (wrong mentionId). Fix: add setIntent(intent) as the first line of onNewIntent.

Explain like I'm 5

An Intent is like sending a letter. An explicit Intent has a specific address on the envelope (go to THIS house). An implicit Intent just says what you want (deliver birthday cake) and the postal system figures out which house does that. A PendingIntent is like giving a pre-addressed, pre-stamped letter to your friend — they can send it later on your behalf without knowing your address. Launch modes decide if you get a new room at the hotel (standard), or if you reuse your room if you're already there (singleTop), or if you have a single permanent suite no matter what (singleTask).

Fun fact

FLAG_IMMUTABLE was made mandatory for PendingIntents targeting Android 12 (API 31) after a security researcher demonstrated that mutable PendingIntents could be hijacked by malicious apps to escalate privileges — they could modify the Intent extras before the system delivered it, effectively making the target app perform unintended actions.

Hands-on challenge

Build a notification system for a SaaS app: (1) Create a NotificationHelper that posts a 'New mention' notification with a PendingIntent targeting MentionDetailActivity (use FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT, unique requestCode per mentionId). (2) Make MentionDetailActivity singleTop with correct onNewIntent handling. (3) Handle the case where the app is not running (cold start from notification) vs already running (onNewIntent path). (4) Test: post two notifications for different mentions — tapping each should show the correct mention.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery