Launch Modes in Android - with Examples

Photo by NASA on Unsplash

Launch Modes in Android - with Examples

A Task is a model that the Android system uses to manage a collection of Activities that the user is interacting with through some application's workflow. In the simplest, default case we would have a one-to-one mapping between one task and one application's set of Activities. When a user is navigating through a multi-activity application, each new Activity that is launched is added to the associated Task.

To keep everything organized, a Task places its Activities in a Back Stack; with each successively opened Activity being added to the top of the stack. This is also what helps facilitate the back navigation when the user presses the back button or swipes back; the current activity gets popped off the stack to be replaced with the Activity underneath it.

As noted earlier, one Task for one application's set of Activities is the default behavior. However, the Android framework does provide the developer with the power to manipulate this behavior either through the manifest file when declaring the Activity to the system, or by using Intent flags.

In this article, I want to focus on the Android manifest file and all the different ways in which we can manipulate the Activity-Task association behavior when defining different Activities in the app's manifest.

I've created a toy app to easily exemplify all the different behaviors. I will provide small code snippets where needed, but the full sample project can be found here on GitHub.

Project Setup

I created a brand new project using Android Studio's "Empty Views Activity" template. Then I created six new Activities:

  1. ActivityA

  2. ActivityB

  3. ActivityC

  4. ActivityD

  5. ActivityE

  6. ActivityF

Each Activity is basically the same in terms of layout and functionality. Their layout contains a TextView at the top to easily distinguish between each Activity. Then they also contain six buttons, each to open one of the Activities when clicked.

Activities A through C

Activities D through F

Also, in each Activity, I overrode the onNewIntent(...) function and displayed a Toast each time it was called. This function will be important later. An example of what it looks like in ActivityA:

class ActivityA : AppCompatActivity() {
    // ...

    override fun onNewIntent(intent: Intent?) {  
        super.onNewIntent(intent)  
        Toast.makeText(  
            this,  
            "onNewIntent called for Activity A",  
            Toast.LENGTH_LONG,  
        ).show()  
    }
}

Lastly, instead of just renaming MainActivity, I updated the AndroidManifest.xml to make ActivityA the default launched Activity instead of MainActivity.

<manifest ...>  
  ...
<application ...>  
 ...
    <activity  
        android:name=".launchactivities.ActivityA"  
        android:exported="true"  
        <intent-filter>  
            <action android:name="android.intent.action.MAIN" />  

            <category android:name="android.intent.category.LAUNCHER" />  
        </intent-filter>  
    </activity>  
    <activity  
        android:name=".MainActivity"  
        android:exported="true">  
    <!-- <intent-filter>-->  
    <!-- <action android:name="android.intent.action.MAIN" />-->  

    <!-- <category android:name="android.intent.category.LAUNCHER" />-->  
    <!-- </intent-filter>-->  
    </activity>  
</application>  

</manifest>

A Quick Note on a Task's Affinity

Each Activity, whether explicitly defined or not, has an Affinity. For a Task, its Affinity would be the Affinity for the Activity at the root of the Task. This holds even if there are multiple Activities with differing Affinities all in one Task (this is possible if all activities are using the standard Launch Mode that we'll discuss later). For example, say a Task has a Back Stack of the following Activities with Affinities:

A (.MyAffinity) -> B (.MyOtherAffinity) -> C (.YetAnotherAffinity)

Then the Affinity for the task would be .MyAffinity.

An Affinity denotes to Android which Task an Activity prefers to be in. By default, every Activity in an application has the same Affinity. However, there are no hard rules on which Affinity an Activity can have. All Activities in the same application can each have distinct Affinities, and even one Activity from one application can share an Affinity with an Activity from another application.

An Affinity is only one criterion for the OS to determine which Task to place a new Activity in. As we will see later, the Affinity combines with the Launch Mode to ultimately determine the placement of the newly opened Activity.

To set the Affinity of an Activity, you can use the taskAffinity XML attribute for the <activity> tag in the AndroidManifest.xml. If the taskAffinity attribute is not set, then the default Affinity is the value for the namespace property in the build.gradle file.

<manifest ...>  
  ...
<application ...>  
 ...
    <activity  
        android:name=".launchactivities.ActivityB"  
        android:taskAffinity="my.Affinity"
        android:exported="true"   
    </activity>    
</application>  
</manifest>
// build.gradle
android {   
    ...
    // default Affinity value
    namespace = "com.nicholasfragiskatos.androidlaunchmodes"      
}

Valid taskAffinity Values

The only rule I could find about appropriate values is from this part of the documentation where it says,

The taskAffinity attribute takes a string value that must be different than the default package name declared in the <manifest> element.

However, I noticed that the value requires a period separator somewhere in the name. Having something like taskAffinity="MyAffinity" causes a build error:

Error running 'app': The application could not be installed: INSTALL_PARSE_FAILED_MANIFEST_MALFORMED

The following values seem to be OK:

  • .myAffinity

  • my.Affinity

  • myAffinity.

  • ..myAffinity

  • . (yes, just a period)

Viewing Tasks on the System

I could not find a good graphical way to view all running Tasks on an emulator in Android Studio. There were some options, but they appeared to be old and incompatible with modern versions of the IDE. The best alternative I could find is using the Android Debugging Bridge (ADB) on the command line and looking through the output of the following command:

adb shell dumpsys activity recents

This will give you a list of recent Tasks, and show the list of Activities for the Task, as well as the Affinity, among other information. For example: (output formatted and abbreviated)

Recent tasks: Recent #0:{54288a5 #148 type=standard A=10164:.com.nicholasfragiskatos.androidlaunchmodes}

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{50c032f u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t107}, 
ActivityRecord{eca6d90 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t107}, 
ActivityRecord{a3a3a7 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t107}, 
ActivityRecord{419e7d8 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t107}
]

Recent Numbering

The numbering changes based on which Task you viewed most recently. It's best to ignore the number and follow the Task ID (#148 from the example below):

Recent #0:{54288a5 #148 type=standard A=10164:.com.nicholasfragiskatos.androidlaunchmodes}

Launch Modes

Launch Modes are a way for the user to specify to the system how a new instance of an Activity should be related to the current Task. We can specify a Launch Mode in either the AndroidManifest.xml when defining the Activity to the system or in Intent flags when launching the new Activity. There are five different Launch Modes.

standard

For this Launch Mode, a new instance of the Activity is created each time regardless of if there already exists an instance of the Activity in the Task. This is the default behavior. You can have as many of the same instances of an Activity across multiple tasks. For instance, all three of these examples are valid states when using the standard Launch Mode:

Examples of valid standard launch mode states

In the example project, to demonstrate this, launch ActivityA multiple times.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: 

... 

Activities=[ 
ActivityRecord{449ced3 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t114}, 
ActivityRecord{5d70c45 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t114}, 
ActivityRecord{7a9c8d3 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t114}, 
ActivityRecord{948d5c1 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t114}, 
ActivityRecord{bffb7c9 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t114}
]
💡
By default, the new instance of the Activity will be created and placed in the same Task that it was launched from. However, as we will see later, depending on other factors it is not guaranteed.

singleTop

This Launch Mode is almost identical to standard. The only difference is that if the Activity you are trying to launch already has an existing instance at the top of the current Task's stack, then instead of creating a new instance, the system will just call the Activity's onNewIntent(...) function.

Considering the example below with the state of the stack being A -> B -> C -> D -> E,1 if we try and launch E again no new instance of ActivityE is created since it's at the top. Instead, the system re-uses the existing instance and just calls onNewIntent(...).2 However, when we try and launch ActivityC again, an instance already exists on the stack, but it's not at the top, so a new instance is created and added to the stack.3

example of singleTop launch mode behavior

In the example project, to demonstrate this, first, we set the Launch Mode in the manifest for ActivityE:

<manifest>  
    ... 
<application  
    ...
    <activity  
        android:name=".launchactivities.ActivityE"  
        android:launchMode="singleTop"  
        android:exported="false" />  
    ...
    <activity  
        android:name=".launchactivities.ActivityC"  
        android:launchMode="singleTop"  
        android:exported="false" />
</application>  
</manifest>

Then, remember that we overrode the onNewIntent(...) function for each Activity to display a Toast:

override fun onNewIntent(intent: Intent?) {  
    super.onNewIntent(intent)  
    Toast.makeText(  
        this,  
        "onNewIntent called for Activity E",  
        Toast.LENGTH_LONG,  
    ).show()  
}

Starting Activities A -> B -> C -> D -> E -> E will result in the onNewIntent(...) function being called, but no additional instance of ActivityE is being added to the stack.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: 

... 

Activities=[ 
ActivityRecord{f04dc64 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t118}, 
ActivityRecord{97af3cc u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t118}, 
ActivityRecord{15ff5c9 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t118}, 
ActivityRecord{6efecbf u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t118}, 
ActivityRecord{5ff0a89 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityE} t118}, 
]

However, starting ActivityC again will result in it being added to the stack.

Recent tasks:

Recent #0: 

... 

Activities=[ 
ActivityRecord{f04dc64 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t118}, 
ActivityRecord{97af3cc u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t118}, 
ActivityRecord{15ff5c9 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t118}, 
ActivityRecord{6efecbf u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t118}, 
ActivityRecord{5ff0a89 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityE} t118}, 
ActivityRecord{582171d u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t118}
]

singleTask

With this Launch Mode, the goal for the system is to maintain a single instance of the Activity at a time. The system will either re-use an existing instance of the Activity, create a new task with the Activity at the root of the stack or use an existing Task that shares the same Affinity. The behavior depends on whether there is already an existing instance of the Activity in some Task, and then whether there's a Task with the same Affinity.

Existing Activity

If the system can find an existing instance of the Activity in either the current Task or another Task then the launched Intent will be routed to the existing Activity to re-use it, and the Activity's onNewIntent(...) function is invoked, similar to what happens in singleTop. However, what also happens is that all Activities that are above the re-used Activity will be popped off the stack, leaving the re-used activity back at the top of the stack for the Task.

In the following example, for the original state, we have Task #1 and Task #2. ActivityD is set as singleTask and has .MyAffinity. All other Activities have the standard Launch Mode with the default Affinity.

If we try and start ActivityD again either in Task #1 or Task #2, then the system will find the existing ActivityD instance, and route the Intent there. This will result in all other Activities above it being removed from the stack.

example of singleTask launch mode behavior for existing instance of activity

To demonstrate this in the example project, we update the Launch Mode and Affinity in the manifest for ActivityD. Set all other Activities back to standard Launch Mode.

<activity  
    android:name=".launchactivities.ActivityD"  
    android:taskAffinity=".myAffinity"  
    android:launchMode="singleTask"  
    android:exported="false" />

Starting Activities A -> B -> C -> D -> E -> A -> B will get us to the original state from the example above where Task #1 has A -> B -> C in its Back Stack, and Task #2 has D -> E -> A -> B in its stack.

adb shell dumpsys activity recents output:

Recent tasks: 

Recent #0: Task{54288a5 #148 type=standard A=10164:.myAffinity}

...

affinity=10164:.myAffinity

...

Activities=[ 
ActivityRecord{e2e12b1 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t148}, 
ActivityRecord{94e9d35 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityE} t148}, 
ActivityRecord{c8cccf7 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t148}, 
ActivityRecord{a2fb2ef u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t148}]

...

Recent #1: Task{cb4f549 #147 type=standard A=10164:com.nicholasfragiskatos.androidlaunchmodes}

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{9d069aa u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t147}, 
ActivityRecord{53edb1a u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t147}, 
ActivityRecord{aa7fbe u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t147}
]

If we start ActivityD again from either Activity the onNewIntent(...) function will be called for ActivityD and the Activities above it (E, A, B) will be removed from the stack.

adb shell dumpsys activity recents output:

Recent tasks: Recent #0: Task{54288a5 #148 type=standard A=10164:.myAffinity ...

affinity=10164:.myAffinity

...

Activities=[ 
ActivityRecord{e2e12b1 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t148}
]

Activity Does Not Exist - Shared Affinity

If the Activity does not already exist and it shares the same Affinity with a Task, then when it is created for the first time it will be placed at the top of the Back Stack of that Task. Every subsequent Activity that is launched will continue to be added to the stack like normal (assuming the other Activities are using the standard Launch Mode).

In the diagram below, we start with an original state of A -> B -> C.1 ActivityD has been set as singleTask with the default Affinity. Since the Affinity matches, launching ActivityD will result in ActivityD being placed at the top of the current Task.2 We can then add A -> B -> C again.3 If we try and launch ActivityD again, the OS sees that an instance already exists so it routes the Intent to onNewIntent(...) and all Activities above ActivityD are popped off the stack until ActivityD is at the top.4

example of singleTask launch mode behavior for non existing instance but same affinity

To demonstrate this in the example project, we update the Launch Mode in the manifest for ActivityD. Set all other Activities back to standard Launch Mode.

<activity  
    android:name=".launchactivities.ActivityD"  
    android:launchMode="singleTask"  
    android:exported="false" />

Starting Activities A -> B -> C will get us to the original state in the diagram.

adb shell dumpsys activity recents output:

Recent tasks: Recent #0: Task{ed463cf #170 type=standard A=10164:com.nicholasfragiskatos.androidlaunchmodes}

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{e19f818 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t170}, 
ActivityRecord{36def6e u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t170}, 
ActivityRecord{96afca3 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t170}
]

Starting ActivityD places the Activity in the current Task.

adb shell dumpsys activity recents output:

Recent tasks: Recent #0: Task{ed463cf #170 type=standard A=10164:com.nicholasfragiskatos.androidlaunchmodes}

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{e19f818 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t170}, 
ActivityRecord{36def6e u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t170}, 
ActivityRecord{96afca3 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t170}, 
ActivityRecord{f802a9e u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t170}
]

Starting A -> B -> C again from the new Task will create new instances on the stack above ActivityD.

adb shell dumpsys activity recents output:

Recent tasks: Recent #0: Task{ed463cf #170 type=standard A=10164:com.nicholasfragiskatos.androidlaunchmodes}

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{e19f818 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t170}, 
ActivityRecord{36def6e u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t170}, 
ActivityRecord{96afca3 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t170}, 
ActivityRecord{f802a9e u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t170}, 
ActivityRecord{bacf62a u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t170}, 
ActivityRecord{7f315f6 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t170}, 
ActivityRecord{356f985 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t170}
]

If we start ActivityD again the onNewIntent(...) function will be called for ActivityD and the Activities above it (A, B, C) will be removed from the stack.

adb shell dumpsys activity recents output:

Recent tasks: Recent #0: Task{ed463cf #170 type=standard A=10164:com.nicholasfragiskatos.androidlaunchmodes}

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{e19f818 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t170}, 
ActivityRecord{36def6e u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t170}, 
ActivityRecord{96afca3 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t170}, 
ActivityRecord{f802a9e u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t170}]

Activity Does Not Exist - Different Affinity

If the Activity does not already exist and it does not share the same Affinity with an existing Task, then when it is created for the first time it will be placed at the root of a new Task. Every subsequent Activity that is launched in this new Task will continue to be added to the stack like normal (assuming the other Activities are using the standard Launch Mode).

In the diagram below, we start with one Task using the default Affinity (Task #1) with an original state of A -> B -> C.1 ActivityD has been set as singleTask with .MyAffinity. Since there is no existing instance of ActivityD, and no Task shares the same Affinity, starting ActivityD will result in a new Task (Task #2) being created and ActivityD being placed at the root.2. And of course, from here, we can expect normal behavior. Starting A -> B -> C while in Task #2 will create those Activities and place them on the stack.3 If we try and launch ActivityD again, the OS sees that an instance already exists so it routes the Intent to onNewIntent(...) and all Activities above ActivityD are popped off the stack until ActivityD is at the top.4

example of singleTask launch mode behavior for non existing instance and different affinity

To demonstrate this in the example project, we update the Launch Mode and Affinity in the manifest for ActivityD. Set all other Activities back to standard Launch Mode.

<activity  
    android:name=".launchactivities.ActivityD"  
    android:taskAffinity=".MyAffinity"  
    android:launchMode="singleTask"  
    android:exported="false" />

Starting Activities A -> B -> C will get us to the original state in the diagram.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: Task{1aa5d3a #161 type=standard A=10164:com.nicholasfragiskatos.androidlaunchmodes

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{9fa33e2 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t161}, 
ActivityRecord{1104ae6 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t161}, 
ActivityRecord{a23a934 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t161}
]

Starting ActivityD creates the new Task.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: Task{28ea774 #162 type=standard A=10164:.MyAffinity}

...

affinity=10164:.MyAffinity

...

Activities=[ 
ActivityRecord{53ca0ba u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t162}
]

Starting A -> B -> C again from the new Task will create new instances on the stack above ActivityD.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: Task{28ea774 #162 type=standard A=10164:.MyAffinity

...

affinity=10164:.MyAffinity

...

Activities=[ 
ActivityRecord{53ca0ba u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t162}, 
ActivityRecord{b15cb2e u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t162}, 
ActivityRecord{31480bf u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t162}, 
ActivityRecord{3de0f78 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t162}
]

And finally, starting ActivityD again will result in onNewIntent(...) being called and the stack is cleared until ActivityD is at the top.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: Task{28ea774 #162 type=standard A=10164:.MyAffinity}

...

affinity=10164:.MyAffinity

...

Activities=[ 
ActivityRecord{53ca0ba u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t162}
]

singleInstance

This Launch Mode is similar to singleTask in a few ways:

  1. The system maintains a single instance of the Activity at a time

  2. Creating an instance of a singleInstance Activity will always create a new Task if there is no existing instance already in the system.

  3. If an existing instance of the singleInstance Activity already exists, then it is re-used and its onNewIntent(...) function is invoked.

There's no need to go over the similarities again, but there is one important difference; an Activity with singleInstance will always be the only Activity in a Task. This means that any other Activity launched by the singleInstance Activity will always be placed in a different Task.

In the diagram below, we start with one Task (Task #1) with an original state of A -> B -> C.1 ActivityD has been set as singleInstance. Since no existing instance of ActivityD exists starting ActivityD will result in a new Task (Task #2) being created and ActivityD being placed at the root.2. Starting E -> F while in Task #2 will create the Activities in Task #1.3

example of singleInstnace launch mode

To demonstrate this in the example project, we update the Launch Mode and Affinity in the manifest for ActivityD. Set all other Activities back to standard Launch Mode.

<activity  
    android:name=".launchactivities.ActivityD"  
    android:taskAffinity=".MyAffinity"  
    android:launchMode="singleInstance"  
    android:exported="false" />

Starting Activities A -> B -> C will get us to the original state in the diagram.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: Task{513838 #184 type=standard A=10164:com.nicholasfragiskatos.androidlaunchmodes}

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{1722f24 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t184}, 
ActivityRecord{dc1435 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t184}, 
ActivityRecord{2327270 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t184}
]

Starting ActivityD creates the new Task.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: Task{5194c82 #185 type=standard A=10164:.MyAffinity}

...

affinity=10164:.MyAffinity

...

Activities=[ 
ActivityRecord{34baff u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD} t185}
]

Starting E -> F in the new Task will create new instances in the original Task.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: Task{513838 #184 type=standard A=10164:com.nicholasfragiskatos.androidlaunchmodes

...

affinity=10164:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{1722f24 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA} t184}, 
ActivityRecord{dc1435 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB} t184}, 
ActivityRecord{2327270 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC} t184}, 
ActivityRecord{628a66 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityE} t184}, 
ActivityRecord{dfb1fee u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityF} t184}
]

singleInstancePerTask

The last Launch Mode is similar to both singleTask and singleInstance in a few ways:

  1. Like singleInstance, the new Activity will always be created in a new Task if there is no existing instance regardless of Affinity.

  2. Like singleTask, other Activities can exist within the Task.

  3. Like singleTask, if the singleInstancePerTask Activity is already a member of a Task, and if we try and launch the Activity again, then all Activities above it in the stack will be removed.

Both the singleTask and the singleInstance Launch Mode shared the singleton property, in which the system will always maintain only a single instance of the Activity at a time across all Tasks. Unlike the latter, it is possible for the singleInstancePerTask Activity to be started in multiple Tasks while still maintaining only one instance per Task. This behavior is possible through the use of the FLAG_ACTIVITY_MULTIPLE_TASK, or FLAG_ACTIVITY_NEW_DOCUMENT Intent flags. This article only covers Launch Modes as defined through the manifest, but I thought it important enough to mention.

Sharing Affinity

For the singleInstancePerTask Launch Mode, the Activity will always be created in a new Task if there is no existing instance, regardless of Affinity. This holds even with matching Affinities, but the behavior is a little different.

Typically, when a new Task has been launched a new window shows up in the app switcher on the device, like the screenshot below:

example of task switcher showing multiple tasks

However, if we try and launch a singleInstancePerTask Activity and there's an existing Task that shares the same Affinity then the system hides the original Task, and we only see one entry in the app switcher.

In the diagram below we start with one Task (Task #1) with an original state of A -> B.1 ActivityE is started using the singleInstancePerTask Launch Mode and .MyAffinity which results in a Task #2 being created.2 ActivityF is started using the singleInstancePerTask Launch Mode and .MyOtherAffinity which results in Task #3 being created.3 ActivityA and ActivityB are started in Task #2 and placed on the top of the stack.4 Next, ActivityD is started using singleInstancePerTask and .MyAffinity which results in Task #4 being created. Since Task #2 is also already using .MyAffinity, Task #2 is hidden.5 Lastly, ActivityC is started using singleInstancePerTask and the default Affinity which results in Task #5 being created. Since Task #1 is also using the default Affinity, Task #1 is hidden.6

example of singleinstancepertask behavior when sharing affinity with another task

To replicate this in the example project start by setting ActivityC, ActivityD, ActivityE, and ActivityF as follows:

<activity  
    android:name=".launchactivities.ActivityF"  
    android:taskAffinity=".MyOtherAffinity"  
    android:launchMode="singleInstancePerTask"  
    android:exported="false" />  
<activity  
    android:name=".launchactivities.ActivityE"  
    android:taskAffinity=".MyAffinity"  
    android:launchMode="singleInstancePerTask"  
    android:exported="false" />  
<activity  
    android:name=".launchactivities.ActivityD"  
    android:taskAffinity=".MyAffinity"  
    android:launchMode="singleInstancePerTask"  
    android:exported="false" />  
<activity  
    android:name=".launchactivities.ActivityC"  
    android:launchMode="singleInstancePerTask"  
    android:exported="false" />

Starting Activities A -> B will get us to the original state in the diagram.

adb shell dumpsys activity recents output:

Recent tasks:

Recent #0: Task{5936bd5 #103 type=standard A=10193:com.nicholasfragiskatos.androidlaunchmodes

...

affinity=10193:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{68ab88c u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA t103}, 
ActivityRecord{aa8eee0 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB t103}
]

Then after launching ActivityE and ActivityF we have 3 tasks total.

adb shell dumpsys activity recents output: Recent tasks:

Recent #0: Task{d80e152 #105 type=standard A=10193:.MyOtherAffinity

...

affinity=10193:.MyOtherAffinity

...

Activities=[ 
ActivityRecord{f68d1dd u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityF t105} 
]

Recent #1: Task{ba9599a #104 type=standard A=10193:.MyAffinity}

...

affinity=10193:.MyAffinity

...

Activities=[ 
ActivityRecord{7d25e45 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityE t104} 
]

Recent #2: Task{5936bd5 #103 type=standard A=10193:com.nicholasfragiskatos.androidlaunchmodes

...

affinity=10193:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{68ab88c u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityA t103}, 
ActivityRecord{aa8eee0 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityB t103}
]

After launching ActivityD we see a new Task created, as expected, to house ActivityD. We also notice that the previous Task with .MyAffinity (id=104, has ActivityE) is no longer in the recent Task list, but it does show up in an extra line above the recent tasks list as part of an mHiddenTasks output value.

adb shell dumpsys activity recents output: Recent tasks:

mHiddenTasks=[
Task{ba9599a #104 type=standard A=10193:.MyAffinity}
]

Recent tasks:

Recent #0: Task{4a12fcd #106 type=standard A=10193:.MyAffinity

...

affinity=10193:.MyAffinity

...

Activities=[ 
ActivityRecord{815b164 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityD t106}
]

Likewise, after launching ActivityC, a new singleInstancePerTask Activity is created with the default Affinity, and now the previous Task with the default Affinity (id=103) is added to the hidden Task list.

adb shell dumpsys activity recents output: Recent tasks:

mHiddenTasks=[
Task{ba9599a #104 type=standard A=10193:.MyAffinity}, 
Task{5936bd5 #103 type=standard A=10193:com.nicholasfragiskatos.androidlaunchmodes}
]

Recent tasks:

Recent #0: Task{5ece29 #107 type=standard A=10193:com.nicholasfragiskatos.androidlaunchmodes}

...

affinity=10193:com.nicholasfragiskatos.androidlaunchmodes

...

Activities=[ 
ActivityRecord{2bf9fb0 u0 com.nicholasfragiskatos.androidlaunchmodes/.launchactivities.ActivityC t107}
]

Conclusion

The Android framework allows the developer to customize how the system manages the relationships between Activities, Tasks, and its Back Stack.

The main goal of this article was to review how we can utilize different Launch Modes to manipulate the Task-Activity relationship, but before that, we needed to lay down some groundwork. First we learned about Tasks and Affinities, and how they are related to Activities. Then we learned what an Affinity is and how manipulating an Activity's Affinity can alter how an Activity associates with a particular Task. Lastly, to help facilitate our understanding we utilized ADB to show the current state of Tasks on a device.

With the groundwork in place, we were able to dive into manipulating an Activity's Launch Mode in its manifest declaration. Each <activity> tag supports a launchMode attribute that has five different valid values: standard, singleTop, singleTask, singleInstance, and singleInstancePerTask. We studied each Launch Mode in detail and saw how they behave in theory and in a real demo application. While many of the Launch Modes shared behavior with one or more of the other Launch Modes, they all provided their own unique behavior.


If you noticed anything in the article that is incorrect or isn't clear, please let me know. I always appreciate the feedback.