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:
ActivityA
ActivityB
ActivityC
ActivityD
ActivityE
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.
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:
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
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.
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
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
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:
The system maintains a single instance of the Activity at a time
Creating an instance of a
singleInstance
Activity will always create a new Task if there is no existing instance already in the system.If an existing instance of the
singleInstance
Activity already exists, then it is re-used and itsonNewIntent(...)
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
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:
Like
singleInstance
, the new Activity will always be created in a new Task if there is no existing instance regardless of Affinity.Like
singleTask
, other Activities can exist within the Task.Like
singleTask
, if thesingleInstancePerTask
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:
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
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.
Top comments (0)