DEV Community

Cover image for Day 5/100: Intent, Task & Back Stack and Why launchMode is a trap
Hoang Son
Hoang Son

Posted on

Day 5/100: Intent, Task & Back Stack and Why launchMode is a trap

This is Day 5 of my 100 Days to Senior Android Engineer series. Each post: what I thought I knew → what I actually learned → interview implications.


🔍 The concept

When a user opens your app, taps through several screens, and then presses Back — Android needs to know where to go. That navigation history lives in a Task, which is essentially a stack of Activities managed by the OS.

Most developers use Intents and navigate between Activities for years without ever thinking deeply about Tasks. That works — until you get a bug report that says:

"When I tap the notification, it opens a blank screen instead of the detail screen."
"After I share something from my app, pressing Back takes the user to our app's home screen instead of the sharing sheet."
"We have two instances of the same Activity open and I don't know why."

All three of these are Task and back stack bugs. And they're famously hard to reproduce because they depend on navigation history that you can't easily control in tests.


💡 What I thought I knew

My mental model was simple: Back Stack = list of screens, Back button = pop the top screen. launchMode existed for edge cases. Intents had flags for special situations.

I'd used FLAG_ACTIVITY_CLEAR_TOP and singleTop a few times, copy-pasted from Stack Overflow, and moved on.

That approach works until it spectacularly doesn't.


😳 What I actually learned

A Task is an OS-level concept, not an app concept

Tasks are owned by the Android system, not by your app. Your app doesn't control how many Tasks exist. The user — through their navigation history — does.

User flow that creates TWO tasks:

Task 1 (your app):
  [HomeActivity] → [ProductActivity] → [CheckoutActivity]

User opens Chrome, taps a deep link into your app:

Task 2 (from Chrome):
  [ProductActivity]   ← same Activity class, different Task
Enter fullscreen mode Exit fullscreen mode

Two instances of ProductActivity exist simultaneously in two different Tasks. The Back button behavior differs between them because they belong to different navigation histories.

This is expected and correct Android behavior. The problem is when your launchMode settings interfere with it.


launchMode — what each value actually does

<!-- Default — new instance every time -->
<activity android:launchMode="standard" />

<!-- Reuse existing instance if it's at the TOP of the stack -->
<activity android:launchMode="singleTop" />

<!-- Only one instance per Task -->
<activity android:launchMode="singleTask" />

<!-- Only one instance in the entire system, in its own Task -->
<activity android:launchMode="singleInstance" />
Enter fullscreen mode Exit fullscreen mode

On paper, this sounds useful. In practice:

singleTop is the only one worth using regularly. Perfect for notification tap targets — prevents stacking duplicate instances when the user taps multiple notifications quickly.

singleTask is where most bugs live. When Android reuses a singleTask Activity, it clears everything above it in the back stack. The whole stack. Not just the top.

Back stack before user taps notification:
[Home] → [List] → [Detail] → [Settings]
                                ↑ user is here

Notification tap targets Detail (singleTask):
[Home] → [Detail]
                ↑ List and Settings are GONE
Enter fullscreen mode Exit fullscreen mode

If you've ever seen "pressing Back after tapping a notification takes me to Home instead of the previous screen" — this is why.

singleInstance is almost never the right answer in a modern app. It creates its own isolated Task, which breaks the user's navigation model in ways that are hard to predict and harder to debug.


Intent flags — the lower-level control

Intent flags give you per-launch control instead of per-Activity declaration. They're more surgical than launchMode.

// The three flags you'll actually use:

// 1. If the Activity exists in the stack, bring it to front and clear above it
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP

// 2. Start fresh — clear the entire back stack (used for logout)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK

// 3. Don't create a new Task even if launched from outside the app
intent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
Enter fullscreen mode Exit fullscreen mode

The one combination every Android dev should know by heart:

// Logout — clear everything and start fresh
fun logout() {
    val intent = Intent(this, LoginActivity::class.java).apply {
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
    }
    startActivity(intent)
    // No finish() needed — CLEAR_TASK handles it
}
Enter fullscreen mode Exit fullscreen mode

FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK starts the target Activity in a new Task after clearing any existing Task for your app. This is the correct logout implementation — not finishAffinity(), not a chain of finish() calls.


Deep links and the Task problem

Deep links are where Task management gets genuinely complex.

When a user taps a deep link from another app (browser, email, messaging), Android can handle it two ways:

Scenario A — App is not running:
  New Task created → deep link destination shown
  User presses Back → where do they go?

Scenario B — App IS running with existing back stack:
  Does Android resume existing Task or create new one?
  Depends on launchMode + flags + TaskAffinity
Enter fullscreen mode Exit fullscreen mode

The correct approach for deep links is to use TaskStackBuilder to construct a synthetic back stack:

// When handling a deep link to a detail screen,
// build the back stack the user would have navigated through naturally
val stackBuilder = TaskStackBuilder.create(context).apply {
    // Add the parent chain
    addNextIntentWithParentStack(
        Intent(context, DetailActivity::class.java).apply {
            putExtra("item_id", itemId)
        }
    )
}
stackBuilder.startActivities()
Enter fullscreen mode Exit fullscreen mode

This creates:

[HomeActivity] → [ListActivity] → [DetailActivity]
Enter fullscreen mode Exit fullscreen mode

So when the user presses Back from the deep-linked Detail screen, they get a logical navigation history — not a blank Home screen or an exit from the app.

With Jetpack Navigation, deep link handling is automatic when you declare your deep links in the nav graph. The library builds the synthetic back stack for you.


taskAffinity — the setting nobody talks about

Every Activity has a taskAffinity — by default, it's your app's package name. This is what Android uses to decide which Task an Activity "belongs to".

This setting is almost never explicitly configured, and that's usually fine. But it's responsible for a class of bugs that are nearly impossible to diagnose without knowing it exists.

<!-- This Activity will try to live in a different Task -->
<activity
    android:name=".ShareReceiverActivity"
    android:taskAffinity="com.yourapp.sharing"
    android:launchMode="singleTask" />
Enter fullscreen mode Exit fullscreen mode

If you combine a custom taskAffinity with singleTask, the Activity will always land in its own dedicated Task. Useful for share targets or widgets that should be isolated from the main app flow. Dangerous if done accidentally.


🧪 The mental model that stuck

A decision tree for navigation decisions:

Need to navigate normally?
  → startActivity() with no flags. Default launchMode. Done.

Tapping a notification that targets a screen that might already be open?
  → singleTop on the target Activity.

Logging out or resetting app state?
  → FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK

Deep linking into a nested screen?
  → TaskStackBuilder (manual) or Jetpack Navigation deep links (automatic)

Need only one instance of a screen per Task?
  → singleTask, but document WHY and test the back stack carefully

Need a completely isolated screen (PiP, share target)?
  → singleInstance + custom taskAffinity, and be very intentional about it
Enter fullscreen mode Exit fullscreen mode

The default answer to "should I change launchMode?" is almost always no. The Navigation Component and ViewModel handle the scenarios that historically required launchMode hacks, and they do it without the back stack side effects.


❓ The interview questions

Question 1 — Mid-level:

"What's the difference between FLAG_ACTIVITY_CLEAR_TOP and FLAG_ACTIVITY_CLEAR_TASK?"

CLEAR_TOP clears all Activities above the target in the current Task and brings the target to the front. The Activities below the target survive.

CLEAR_TASK destroys the entire Task first, then starts the target fresh. Nothing survives. Usually paired with NEW_TASK.

// CLEAR_TOP: [Home] → [List] → [Detail] → [Settings]
// Navigate to List with CLEAR_TOP:
// Result: [Home] → [List]

// CLEAR_TASK: same back stack
// Navigate to Login with CLEAR_TASK | NEW_TASK:
// Result: [Login]  ← completely fresh
Enter fullscreen mode Exit fullscreen mode

Question 2 — Senior:

"A user taps a push notification for an order status update. Your app may or may not be running. The user might be on any screen. How do you ensure they land on the OrderDetailActivity with a correct back stack regardless of app state?"

// In your notification handler (FirebaseMessagingService or NotificationReceiver)
fun showOrderNotification(orderId: String) {
    val detailIntent = Intent(context, OrderDetailActivity::class.java).apply {
        putExtra("order_id", orderId)
        // Ensure we handle both app running and not running
        flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
    }

    val pendingIntent = TaskStackBuilder.create(context).run {
        addNextIntentWithParentStack(detailIntent)
        getPendingIntent(
            orderId.hashCode(),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }

    val notification = NotificationCompat.Builder(context, CHANNEL_ID)
        .setContentIntent(pendingIntent)
        .setAutoCancel(true)
        // ...
        .build()

    notificationManager.notify(orderId.hashCode(), notification)
}
Enter fullscreen mode Exit fullscreen mode

TaskStackBuilder synthesizes the back stack so the user can press Back naturally. FLAG_ACTIVITY_SINGLE_TOP prevents duplicate instances if the Activity is already on top. The hashCode() of orderId as the notification ID ensures multiple order notifications don't replace each other.


Question 3 — Senior / Architecture:

"Your team wants to migrate from multiple Activities to a single-Activity architecture with Jetpack Navigation. What happens to your back stack management, deep links, and notification handling?"

Single-Activity means the OS back stack now contains just one (or very few) Activities. Back stack management moves entirely into the Navigation Component's NavBackStack, which handles:

  • Fragment transactions as navigation actions
  • popUpTo / inclusive replacing FLAG_ACTIVITY_CLEAR_TOP
  • launchSingleTop on nav actions replacing singleTop launchMode
  • <deepLink> in nav graph replacing TaskStackBuilder manual setup

The tradeoff: you gain simplicity and predictability. You lose fine-grained OS-level Task control. For most apps, that's the right trade.

<!-- Navigation graph replaces launchMode and Intent flags -->
<action
    android:id="@+id/action_to_home"
    app:destination="@id/homeFragment"
    app:popUpTo="@id/nav_graph"
    app:popUpToInclusive="true"
    app:launchSingleTop="true" />
Enter fullscreen mode Exit fullscreen mode

What surprised me revisiting this

  1. I'd been using singleTask in a notification scenario for years without fully understanding that it clears the back stack above the target. It worked in our tests because we always tapped the notification from the home screen. It would have broken for any user mid-flow.

  2. TaskStackBuilder for notifications is shockingly underused. Most notification implementations I've seen in the wild use a simple PendingIntent with no back stack construction. Users tap the notification and Back exits the app. That's a bad UX that's trivially fixable.

  3. The Navigation Component genuinely removes the need for 90% of launchMode usage. I knew this conceptually, but going back through the docs made it concrete. Every singleTop and CLEAR_TOP use case has a clean Navigation Component equivalent.


Tomorrow

Day 6 → Context in Android — Application, Activity, Service: why using the wrong one causes memory leaks and crashes, and a rule of thumb that's served me well for years.

Have you ever shipped a singleTask bug that cleared a back stack unexpectedly? I definitely have. Tell me in the comments.


Day 4: Fragment Lifecycle — Why It's More Confusing Than Activity's

Top comments (0)