Activity embedding

Large screen devices enable users to see more, do more, experience more.

Large displays provide the opportunity to run multiple activities or multiple instances of the same activity simultaneously.

To take advantage of the extra display area of large screens, Jetpack WindowManager 1.0 Beta03 introduces activity embedding, which splits an application’s task window among activities.

Settings app with activities side by side.

Updating legacy codebases to support large screens can be challenging. Converting activity-based apps to multi-pane layouts using fragments requires significant refactoring and development resources.

Activity embedding requires little or no refactoring of your app. You determine how your app displays its activities—side by side or stacked—by creating an XML configuration file or making Jetpack WindowManager API calls.

Support for small screens is maintained automatically. When your app is on a device with a small screen, activities are stacked one on top of the other. On large screens, activities are displayed side by side. The system determines the presentation based on the configuration you’ve created—no branching logic required.

Activity embedding supports device orientation changes and works seamlessly on foldable devices, stacking and unstacking activities as the device folds and unfolds.

Modern android development uses a single-activity architecture with fragments, navigation components, and versatile layout managers like SlidingPaneLayout.

But if your app consists of multiple activities, activity embedding enables you to easily provide an enhanced user experience on tablets, foldables, and Chrome OS devices.

Split task window

Activity embedding splits the app task window into two containers: primary and secondary. The containers hold activities launched from the main activity or from other activities already in the containers.

Activities are stacked in the secondary container as they’re launched, and the secondary container is stacked on top of the primary container on small screens, so activity stacking and back navigation are consistent with the ordering of activities already built into your app.

Activity embedding enables you to display activities in a variety of ways. Your app can split the task window by launching two activities side by side simultaneously:

Figure 1. Two activities side by side.

Or, an activity that’s occupying the entire task window can create a split by launching a new activity alongside:

Figure 2. Activity A starts activity B to the side.

Note: In this initial version of activity embedding, only the activity that created the split can occupy the primary container. The secondary container can contain a stack of activities.

Activities that are already in a split and sharing a task window can launch other activities in the following ways:

  • To the side on top of another activity:

    Figure 3. Activity A starts activity C to the side over activity B.
  • To the side, and shift the split sideways, concealing the previous primary activity:

    Figure 4. Activity B starts activity C to the side and shifts the split sideways.
  • Launch an activity in place on top; that is, in the same activity stack:

    Figure 5. Activity B starts activity C with no extra intent flags.
  • Launch an activity full window in the same task:

    Figure 6. Activity A or activity B starts activity C which fills the task window.

Back navigation

Different types of applications can have different back navigation rules in a split task window state depending on the dependencies between activities or how users trigger the back event, for example:

  • Going together: If activities are related, and one should not be shown without the other, back navigation can be configured to finish both.
  • Going it alone: If activities are fully independent, back navigation on an activity does not affect the state of another activity in the task window.

The back event is sent to the last focused activity when using button navigation. With gesture based navigation, the back event is sent to the activity where the gesture occurred.

Multi-pane layout

Jetpack WindowManager 1.0 Beta03 enables you to build a multi-pane layout with activities on large-screen devices with 12L (API level Sv2) and some devices with earlier platform versions. Existing apps that are based on multiple activities rather than fragments or view-based layouts such as SlidingPaneLayout can provide an improved large screen user experience without significant refactoring.

One common example is a list-detail split. To ensure a quality presentation, the system starts the list activity, and then the application immediately starts the detail activity. The transition system waits until both activities are drawn, then displays them together. To the user, the two activities launch as one.

Figure 7. Two activities started simultaneously in a multi-pane layout.

Split ratio

Your application can specify how the task window is proportioned by the ratio attribute of a split configuration (see Split configuration below).

Figure 8. Two activity splits with different split ratios.

Placeholders

Placeholder activities are empty secondary activities that occupy an area of an activity split. They are ultimately meant to be replaced with another activity that contains content. For example, a placeholder activity could occupy the secondary side of an activity split in a list-detail layout until an item from the list is selected, at which point an activity containing the detail information for the selected list item replaces the placeholder.

Placeholders are shown only when there is enough space for a split. They are automatically finished when the display size changes to a width too small to display an activity split, but they are relaunched automatically (with a reinitialized state) when space permits.

Figure 9. Foldable device folding and unfolding. Placeholder activity is finished and recreated as display size changes.

Window size changes

When device configuration changes reduce the task window width so that it is not large enough for a multi-pane layout (for example, when a large screen foldable device folds from tablet size to phone size or the app window is resized in multi-window mode), the non-placeholder activities in the secondary pane of the task window are stacked on top of the activities in the primary pane.

Placeholder activities are shown only when there is enough display width for a split. On smaller screens, the placeholder is automatically dismissed. When the display area becomes large enough again, the placeholder is recreated. (See Placeholders above.)

Activity stacking is possible because WindowManager z-orders the activities in the secondary pane above activities in the primary pane.

Multiple activities in secondary pane

Activity B starts activity C in place with no extra intent flags:

Activity split containing activites A, B, and C with C stacked on
          top of B.

resulting in the following z-order of activities in the same task:

Secondary activity stack containing activity C stacked on top of B.
          Secondary stack is stacked on top of prmary activity stack
          containing activity A.

So, in a smaller task window, the application shrinks to a single activity with C at the top of the stack:

Small window showing only activity C.

Navigating back in the smaller window navigates through the activities stacked on top of each other.

If the task window configuration is restored to a larger size that can accommodate multiple panes, the activities are displayed side by side again.

Stacked splits

Activity B starts activity C to the side and shifts the split sideways:

Task window showing activities A and B, then activities B and C.

The result is the following z-order of activities in the same task:

Activities A, B, and C in a single stack. The activities are stacked
          in the following order from top to bottom: C, B, A.

In a smaller task window, the application shrinks to a single activity with C on top:

Small window showing only activity C.

Split configuration

Containers and splits can be created by the WindowManager library based on split rules. Configuring split rules involves several steps:

  1. Add the WindowManager library dependency to your build.gradle file:

    implementation("androidx.window:window:1.0.0-beta03")

  2. Create a resources file that does the following:

    • Defines which activities should be split using filters
    • Configures the split options for all activities that share a split
    • Specifies activities that should never be put in a split

    For example:

    <!-- The split configuration for activities. -->
    <resources
        xmlns:window="http://schemas.android.com/apk/res-auto">
    
        <!-- Automatically split the following activity pairs. -->
        <SplitPairRule
            window:splitRatio="0.3"
            window:splitMinWidth="600dp"
            window:finishPrimaryWithSecondary="true"
            window:finishSecondaryWithPrimary="true">
            <SplitPairFilter
                window:primaryActivityName=".SplitActivityList"
                window:secondaryActivityName=".SplitActivityDetail"/>
            <SplitPairFilter
                window:primaryActivityName="*"
                window:secondaryActivityName="*/*"
                window:secondaryActivityAction="android.intent.action.VIEW"/>
        </SplitPairRule>
    
        <!-- Automatically launch a placeholder for the list activity. -->
        <SplitPlaceholderRule
            window:placeholderActivityName=".SplitActivityListPlaceholder"
            window:splitRatio="0.3"
            window:splitMinWidth="600dp">
            <ActivityFilter
                window:activityName=".SplitActivityList"/>
        </SplitPlaceholderRule>
    
    </resources>
    
  3. Inform the library about the rule definitions.

    In this example we’re using the Jetpack Startup library to perform initialization before other components of the app load and activities start. To enable the startup functionality, add the library dependency in the app’s build file:

    implementation("androidx.startup:startup-runtime:1.1.0")

    and add the following entry in the app manifest:

    <!-- AndroidManifest.xml -->
    
    <provider android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">
        <!-- This entry makes ExampleWindowInitializer discoverable. -->
        <meta-data  android:name="androidx.window.sample.embedding.ExampleWindowInitializer"
            android:value="androidx.startup" />
    </provider>
    
  4. Finally, add the initializer class implementation.

    The rules are set by providing the ID of the xml resource file that contains the definitions (main_split_config) to SplitController.initialize():

    Kotlin

    class ExampleWindowInitializer : Initializer<SplitController> {
       override fun create(context: Context): SplitController {
           SplitController.initialize(context, R.xml.main_split_config)
           return SplitController.getInstance(context)
       }
    
       override fun dependencies(): List<Class<out Initializer<*>>> {
           return emptyList()
       }
    }
    

    Java

    class ExampleWindowInitializer extends Initializer<SplitController> {
       @Override
       SplitController create(Context context) {
           SplitController.initialize(context, R.xml.main_split_config);
           return SplitController.getInstance(context);
       }
    
       @Override
       List<Class<? extends Initializer<?>>> dependencies() {
           return emptyList();
       }
    }
    

Split examples

Split from full window

Figure 10. Activity A starts activity B to the side.

No refactoring required. You can define the configuration for the split statically or at runtime and then call Context#startActivity() without any additional parameters.

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split by default

When the landing page of an application is designed to be split into two containers on large screens, the user experience is best when both activities are created and presented simultaneously. However, content might not be available for the secondary container of the split until the user interacts with the activity in the primary container (for example, the user selects an item from a navigation menu). A placeholder activity can fill the void until content can be displayed in the secondary container of the split (see Placeholders above).

Figure 11. Split created by opening two activities simultaneously. One activity is a placeholder.

To create a split with a placeholder, create a placeholder and associate it with the primary activity:

<SplitPlaceholderRule
    window:placeholderIntentName=".Placeholder">
    <ActivityFilter
        window:activityName=".Main"/>
</SplitPlaceholderRule>

When an app receives an intent, the target activity can be shown as the secondary part of an activity split; for example, a request to show a detail screen with information about an item from a list. On small displays, the detail is shown in the full task window; on larger devices, beside the list.

Figure 12. Deep link detail activity shown alone on a small screen, but together with a list activity on a large screen.

The launch request should be routed to the main activity, and the target detail activity should be launched in a split. SplitController automatically chooses the correct presentation—stacked or side by side—based on the available display width.

Kotlin

override fun onCreate(savedInstanceState Bundle?) {
    …
    splitController.registerRule(SplitPairRule(newFilters))
    startActivity(Intent(this, DetailActivity::class.java))
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    …
    splitController.registerRule(new SplitPairRule(newFilters));
    startActivity(new Intent(this, DetailActivity.class));
}

The deep link destination might be the only activity that should be available to the user in the back navigation stack, and you might want to avoid dismissing the detail activity and leaving only the main activity:

Large display with list activity and detail activity side by side.
          Back navigation unable to dismiss detail activity and leave list
          activity on screen.

Small display with detail activity only. Back navigation unable to
          dismiss detail activity and reveal list activity.

Instead, you can finish both activities at the same time by using the finishPrimaryWithSecondary attribute:

<SplitPairRule
    window:finishPrimaryWithSecondary="true">
    <SplitPairFilter
        window:primaryActivityName=".List"
        window:secondaryActivityName=".Detail"/>
</SplitPairRule>

Multiple activities in split containers

Stacking multiple activities in a split container enables users to access deep content. For example, with a list-detail split, the user might need to go into a sub-detail section but keep the primary activity in place:

Figure 13. Activity opened in place in the secondary pane of the task window.

Kotlin

class DetailActivity {
    …
    fun onOpenSubDetail() {
      startActivity(Intent(this, SubDetailActivity::class.java))
    }
}

Java

public class DetailActivity {
    …
    void onOpenSubDetail() {
        startActivity(new Intent(this, SubDetailActivity.class));
    }
}

The sub-detail activity is placed on top of the detail activity, concealing it:

The user can then go back to the previous detail level by navigating back through the stack:

Figure 14. Activity removed from the top of the stack.

Stacking activities on top of each other is the default behavior when activities are launched from an activity in the same secondary container. Activities launched from the primary container within an active split also end up in the secondary container on the top of the activity stack.

Activities in a new task

When activities in a split task window start activities in a new task, the new task is separate from the task that includes the split and is displayed full window. The Recents screen will show two tasks: the task in the split and the new task.

Figure 15. Start activity C in a new task from activity B.

Activity replacement

Activities can be replaced in the secondary container stack; for example, when the primary activity is used for top-level navigation and the secondary activity is a selected destination. Each selection from the top-level navigation should start a new activity in the secondary container and remove the activity or activities that were previously there.

Figure 16. Top-level navigation activity in the primary pane replaces destination activities in the secondary pane.

If the app doesn't finish the activity in the secondary container when the navigation selection changes, back navigation might be confusing when the split is collapsed (when the device is folded). For example, if you have a menu in the primary pane and screens A and B stacked in the secondary pane, when the user folds the phone, B is on top of A, and A is on top of the menu. When the user navigates back from B, A appears instead of the menu.

Screen A must be removed from the back stack in such cases.

The default behavior when launching to the side in a new container over an existing split is to put the new secondary containers on top and retain the old ones in the back stack. You can configure the splits to clear the previous secondary containers with clearTop and launch new activities normally.

<SplitPairRule
    window:clearTop="true">
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenA"/>
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenB"/>
</SplitPairRule>

Kotlin

class MenuActivity {
    …
    fun onMenuItemSelected(selectedMenuItem: Int) {
        startActivity(Intent(this, classForItem(selectedMenuItem)))
    }
}

Java

public class MenuActivity {
    …
    void onMenuItemSelected(int selectedMenuItem) {
        startActivity(new Intent(this, classForItem(selectedMenuItem)));
    }
}

Alternatively, use the same secondary activity, and from the primary (menu) activity send new intents that resolve to the same instance but trigger a state or UI update in the secondary container.

Multiple splits

Apps can provide multi-level deep navigation by launching additional activities to the side.

When an activity in a secondary container launches a new activity to the side, a new split is created over top of the existing split.

Figure 17. Activity B starts activity C to the side.

The back stack contains all activities that were previously opened, so users can navigate to the A/B split after finishing C.

Activities A, B, and C in a stack. The activities are stacked in
          the following order from top to bottom: C, B, A.

To create a new split, launch the new activity to the side from the existing secondary container. Declare the configurations for both the A/B and B/C splits and launch activity C normally from B:

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
    <SplitPairFilter
        window:primaryActivityName=".B"
        window:secondaryActivityName=".C"/>
</SplitPairRule>

Kotlin

class B {
    fun onOpenC() {
        startActivity(Intent(this, C::class.java))
    }
}

Java

public class B {
    …
    void onOpenC() {
        startActivity(new Intent(this, C.class));
    }
}

React to split state changes

Different activities in an app can have UI elements that perform the same function; for example, a control that opens a window containing account settings.

Figure 18. Different activities with functionally identical UI elements.

If two activities that have a UI element in common are in a split, it’s redundant and perhaps confusing to show the element in both activities.

Figure 19. Duplicate UI elements in activity split.

To know when activities are in a split, register a listener with SplitController for changes in the split state. Then, adjust the UI accordingly:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    splitController
        .addSplitListener(this, mainThreadExecutor, SplitInfoChangeCallback())
}

inner class SplitInfoChangeCallback : Consumer<List<SplitInfo>> {
    override fun accept(splitInfoList: List<SplitInfo>) {
        findViewById<View>(R.id.infoButton).visibility =
            if (!splitInfoList.isEmpty()) View.GONE else View.VISIBLE
    }
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    splitController
        .addSplitListener(this, mainThreadExecutor, SplitInfoChangeCallback());
}

class SplitInfoChangeCallback extends Consumer<List<SplitInfo>> {
    public void accept(List<SplitInfo> splitInfoList) {
        findViewById<View>(R.id.infoButton).visibility =
            !splitInfoList.isEmpty()) ? View.GONE : View.VISIBLE;
    }
}

Callbacks can be made in any lifecycle state, including when an activity is stopped. Listeners should usually be registered in onStart() and unregistered in onStop().

Full window modal

Some activities block users from interacting with the application until a specified action is performed; for example, a login screen activity, policy acknowledgement screen, or error message. Modal activities should be prevented from appearing in a split.

An activity can be forced to always fill the task window by using the expand configuration:

<ActivityRule
    window:alwaysExpand="true">
    <ActivityFilter
        window:activityName=".FullWidthActivity"/>
</ActivityRule>

Finishing activities

Users can finish activities on either side of the split by swiping from the edge of the display:

Figure 20. Swipe gesture finishing activity B.
Figure 21. Swipe gesture finishing activity A.

If the device is set up to use the back button instead of gesture navigation, the input is sent to the focused activity—the activity that was touched or launched last.

The result of finishing one of the activities in the split depends on the split configuration.

Default configuration

When one activity in the split is finished, the remaining activity occupies the entire window:

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split containing activities A and B. A is finished, leaving B to
          occupy the entire window.

Split containing activities A and B. B is finished, leaving A to
          occupy the entire window.

Finish activities together

Finish the primary activity automatically when the secondary activity is finished:

<SplitPairRule
    window:finishPrimaryWithSecondary="true">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split containing activities A and B. B is finished, which also
          finishes A, leaving the task window empty.

Split containing activities A and B. A is finished, leaving B alone
          in the task window.

Finish the secondary activity automatically when the primary activity is finished:

<SplitPairRule
    window:finishSecondaryWithPrimary="true">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split containing activities A and B. A is finished, which also
          finishes B, leaving the task window empty.

Split containing activities A and B. B is finished, leaving A alone
          in the task window.

Finish activities together when either the primary or secondary is finished:

<SplitPairRule
    window:finishPrimaryWithSecondary="true"
    window:finishSecondaryWithPrimary="true">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split containing activities A and B. A is finished, which also
          finishes B, leaving the task window empty.

Split containing activities A and B. B is finished, which also
          finishes A, leaving the task window empty.

Finish multiple activities in containers

If multiple activities are stacked in a split container, finishing an activity on the bottom of the stack does not automatically finish activities on top.

For example, if two activities are in the secondary container, C on top of B:

Secondary activity stack containing activity C stacked on top of B
          is stacked on top of the prmary activity stack containing activity
          A.

and the configuration of the split is defined by the configuration of activities A and B:

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

finishing the top activity retains the split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. C finishes, leaving A and B in the
          activity split.

Finishing the bottom (root) activity of the secondary container does not remove the activities on top of it; and so, also retains the split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. B finishes, leaving A and C in the
          activity split.

Any additional rules for finishing activities together, such as finishing the secondary activity with the primary, are also executed:

<SplitPairRule
    window:finishSecondaryWithPrimary="true">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split with activity A in primary container and activities B and C in
          secondary container, C stacked on top of B. A finishes, also
          finishing B and C.

And when the split is configured to finish primary and secondary together:

<SplitPairRule
    window:finishPrimaryWithSecondary="true"
    window:finishSecondaryWithPrimary="true">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. C finishes, leaving A and B in the
          activity split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. B finishes, leaving A and C in the
          activity split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. A finishes, also finishing B and
          C.

Change split properties at runtime

The properties of a currently active and visible split cannot be changed. Changing the split rules affects additional activity launches and new containers, but not existing and active splits.

To change the properties of active splits, finish the side activity or activities in the split and launch to the side again with a new configuration.

Extract an activity from a split to full window

Create a new configuration that will display the side activity full window, and then relaunch the activity with an intent that resolves to the same instance.

Check for split support at runtime

Activity embedding is an 12L (API level Sv2) feature, but it will be available on some devices with earlier platform versions. To check at runtime for the availability of the feature, use the SplitController.isSplitSupported() method:

Kotlin

val splitController = SplitController.Companion.getInstance()
if (splitController.isSplitSupported()) {
    // Device supports split activity features.
}

Java

SplitController splitController = SplitController.Companion.getInstance();
if (splitController.isSplitSupported()) {
  // Device supports split activity features.
}

If splits are not supported, activities are launched on top (following the regular model).

Limitations, restrictions, and caveats

  • Only the host app of the task, which is identified as the owner of the root activity in the task, can organize and embed other activities in the task. If activities that support embedding and splits run in a task that belongs to a different application, then embedding and splits will not work for those activities.
  • Activities can only be organized within a single task. Launching an activity in a new task always puts it in a new expanded window outside of any existing splits.
  • Only activities in the same process can be organized and put in a split. The SplitInfo callback only reports activities that belong to the same process, since there is no way of knowing about activities in different processes.
  • Each pair or singular activity rule applies only to activity launches that happen after the rule has been registered. There is currently no way to update existing splits or their visual properties.
  • The split pair filter configuration must match the intents used when launching activities completely. The matching occurs at the point when a new activity is started from the application process, so it might not know about component names that are resolved later in the system process when using implicit intents. If a component name is not known at the time of launch, a wildcard can be used instead (“*/*”) and filtering can be performed based on intent action.
  • There is currently no way to move activities between containers or in and out of splits after they were created. Splits are only created by the WindowManager library when new activities with matching rules are launched, and splits are destroyed when the last activity in a split container is finished.
  • Activities can be relaunched when the configuration changes, so when a split is created or removed and activity bounds change, the activity can go through complete destruction of the previous instance and creation of the new one. As a result, app developers should be careful with things like launching new activities from lifecycle callbacks.

Resources