Build a list-detail layout with activity embedding and Material Design

1. Introduction

Large displays enable you to create app layouts and UIs that enhance the user experience and increase user productivity. But if your app is designed for the small displays of non-foldable phones, it probably doesn't take advantage of the extra display space offered by tablets, foldables, and ChromeOS devices.

Updating an app to make the most of large displays can be time consuming and costly, especially for legacy apps based on multiple activities.

Activity embedding, introduced in Android 12L (API level 32), enables activity-based apps to display multiple activities simultaneously on large screens to create two-pane layouts such as list-detail. No Kotlin or Java re-coding required. You add some dependencies, create an XML configuration file, implement an initializer, and make a few additions to your app manifest. Or, if you prefer working in code, just add a few Jetpack WindowManager API calls to the onCreate() method of your app's main activity.

Prerequisites

To complete this codelab, you'll need experience in the following:

  • Building Android apps
  • Working with activities
  • Writing XML
  • Working in Android Studio, including virtual device setup

What you'll build

In this codelab, you'll update an activity-based app to support a dynamic two-pane layout that's similar to SlidingPaneLayout. On small screens, the app overlays (stacks) activities on top of one another in the task window.

Activities A, B, and C stacked in the task window.

On large screens, the app displays two activities on screen simultaneously, either side by side or top and bottom based on your specifications.

4b27b07b7361d6d8.png

What you'll learn

How to implement activity embedding two ways:

  • With an XML configuration file
  • Using Jetpack WindowManager API calls

What you'll need

  • Recent version of Android Studio
  • Android phone or emulator
  • Android small tablet or emulator
  • Android large tablet or emulator

2. Setup

Get the sample app

Step 1: Clone the repo

Clone the large screen codelabs Git repository:

git clone https://github.com/android/large-screen-codelabs

or download and unarchive the large screen codelabs zip file:

Step 2: Inspect the codelab source files

Navigate to the activity-embedding folder.

Step 3: Open the codelab project

In Android Studio, open the Kotlin or Java project

File list for the activity folder in the repo and zip file.

The activity-embedding folder in the repo and zip file contains two Android Studio projects: one in Kotlin, one in Java. Open the project of your choice. Codelab snippets are provided in both languages.

Create virtual devices

If you do not have an Android phone, small tablet, or large tablet at API level 32 or higher, open Device Manager in Android Studio and create any of the following virtual devices you require:

  • Phone — Pixel 6, API level 32 or higher
  • Small tablet — 7 WSVGA (Tablet), API level 32 or higher
  • Large tablet — Pixel C, API level 32 or higher

3. Run the app

The sample app displays a list of items. When the user selects an item, the app displays information about the item.

The app consists of three activities:

  • ListActivity — Contains a list of items in a RecyclerView
  • DetailActivity — Displays information about a list item when the item is selected from the list
  • SummaryActivity — Displays a summary of information when the Summary list item is selected

Behavior without activity embedding

Run the sample app to see how it behaves without activity embedding:

  1. Run the sample app on your large tablet or Pixel C emulator. The main (list) activity appears:

Large tablet with sample app running in portrait orientation. List activity full screen.

  1. Select a list item to launch a secondary (detail) activity. The detail activity overlays the list activity:

Large tablet with sample app running in portrait orientation. Detail activity full screen.

  1. Rotate the tablet to landscape orientation. The secondary activity still overlays the main activity and occupies the entire display:

Large tablet with sample app running in landscape orientation. Detail activity full screen.

  1. Select the back control (left-facing arrow in the app bar) to return to the list.
  2. Select the last item in the list, Summary, to launch a summary activity as a secondary activity. The summary activity overlays the list activity:

Large tablet with sample app running in portrait orientation. Summary activity full screen.

  1. Rotate the tablet to landscape orientation. The secondary activity still overlays the main activity and occupies the entire display:

Large tablet with sample app running in landscape orientation. Summary activity full screen.

Behavior with activity embedding

When you've completed this codelab, landscape orientation will display the list and detail activities side by side in a list-detail layout:

Large tablet with sample app running in landscape orientation. List and detail activities in list-detail layout.

However, you'll configure the summary to display full screen, even though the activity is launched from within a split. The summary will overlay the split:

Large tablet with sample app running in landscape orientation. Summary activity full screen.

4. Background

Activity embedding splits the app task window into two containers: primary and secondary. Any activity can initiate a split by launching another activity. The initiating activity occupies the primary container; the launched activity, the secondary.

The primary activity can launch additional activities in the secondary container. Activities in both containers can then launch activities in their respective containers. Each container can contain a stack of activities. For more information, see the Activity embedding developer guide.

You configure your app to support activity embedding by creating an XML configuration file or by making Jetpack WindowManager API calls. We'll start with the XML configuration approach.

5. XML configuration

Activity embedding containers and splits are created and managed by the Jetpack WindowManager library based on split rules that you create in an XML configuration file.

Add the WindowManager dependency

Enable the sample app to access the WindowManager library by adding the library dependency to the app's module-level build.gradle file, for example:

build.gradle

 implementation 'androidx.window:window:1.2.0'

Inform the system

Let the system know your app has implemented activity embedding.

Add the android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED property to the <application> element of the app manifest file, and set the value to true:

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <property
            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
            android:value="true" />
    </application>
</manifest>

Device manufacturers (OEMs) use the setting to enable custom capabilities for apps that support activity embedding. For example, devices can letterbox portrait-only activities (see android:screenOrientation) on landscape displays to orient the activities for a smooth transition to an activity embedding two-pane layout:

Activity embedding with portrait-only app on landscape display. Letterboxed, portrait-only activity A launches embedded activity B.

Create a configuration file

Create an XML resource file named main_split_config.xml in your app's res/xml folder with resources as the root element.

Change the XML namespace to:

main_split_config.xml

xmlns:window="http://schemas.android.com/apk/res-auto"

Split pair rule

Add the following split rule to the configuration file:

main_split_config.xml

<!-- Define a split for the named activity pair. -->
<SplitPairRule
    window:splitRatio="0.33"
    window:splitMinWidthDp="840"
    window:finishPrimaryWithSecondary="never"
    window:finishSecondaryWithPrimary="always">
  <SplitPairFilter
      window:primaryActivityName=".ListActivity"
      window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>

The rule does the following:

  • Configures split options for activities that share a split:
  • splitRatio — Specifies how much of the task window is occupied by the primary activity (33%), leaving the remaining space for the secondary activity.
  • splitMinWidthDp — Specifies the minimum display width (840) required for both activities to be on screen simultaneously. Units are display-independent pixels (dp).
  • finishPrimaryWithSecondary — Specifies whether activities in the primary split container finish (never) when all activities in the secondary container finish.
  • finishSecondaryWithPrimary — Specifies whether activities in the secondary split container finish (always) when all activities in the primary container activity finish.
  • Includes a split filter that defines the activities that share a task window split. The primary activity is ListActivity; the secondary is DetailActivity.

Placeholder rule

A placeholder activity occupies the secondary container of an activity split when no content is available for that container, for example, when a list-detail split opens but a list item has not yet been selected. (For more information, see Placeholders in the Activity embedding developer guide.)

Add the following placeholder rule to the configuration file:

main_split_config.xml

<!-- Automatically launch a placeholder for the detail activity. -->
<SplitPlaceholderRule
    window:placeholderActivityName=".PlaceholderActivity"
    window:splitRatio="0.33"
    window:splitMinWidthDp="840"
    window:finishPrimaryWithPlaceholder="always"
    window:stickyPlaceholder="false">
  <ActivityFilter
      window:activityName=".ListActivity"/>
</SplitPlaceholderRule>

The rule does the following:

  • Identifies the placeholder activity, PlaceholderActivity (we'll create this activity in the next step)
  • Configures options for the placeholder:
  • splitRatio — Specifies how much of the task window is occupied by the primary activity (33%), leaving the remaining space for the placeholder. Typically, this value should match the split ratio of the split pair rule with which the placeholder is associated.
  • splitMinWidthDp — Specifies the minimum display width (840) required for the placeholder to appear on screen with the primary activity. Typically, this value should match the minimum width of the split pair rule with which the placeholder is associated. Units are display-independent pixels (dp).
  • finishPrimaryWithPlaceholder — Specifies whether activities in the primary split container finish (always) when the placeholder finishes.
  • stickyPlaceholder — Indicates whether the placeholder should remain on screen (false) as the top activity when the display is resized down to a single-pane display from a two-pane display, for example, when a foldable device is folded.
  • Includes an activity filter that specifies the activity (ListActivity) with which the placeholder shares a task window split.

The placeholder stands in for the secondary activity of the split pair rule whose primary activity is the same as the activity in the placeholder activity filter (see "Split pair rule" in the "XML configuration" section of this codelab).

Activity rule

Activity rules are general purpose rules. Activities that you want to occupy the entire task window—that is, never be part of a split—can be specified with an activity rule. (For more information, see Full-window modal in the Activity embedding developer guide.)

We'll make the summary activity fill the entire task window, overlaying the split. Back navigation will return to the split.

Add the following activity rule to the configuration file:

main_split_config.xml

<!-- Activities that should never be in a split. -->
<ActivityRule
    window:alwaysExpand="true">
  <ActivityFilter
      window:activityName=".SummaryActivity"/>
</ActivityRule>

The rule does the following:

  • Identifies the activity that should be displayed full window (SummaryActivity).
  • Configures options for the activity:
  • alwaysExpand — Specifies whether or not the activity should expand to fill all available display space.

Source file

Your finished XML configuration file should look like this:

main_split_config.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:window="http://schemas.android.com/apk/res-auto">

    <!-- Define a split for the named activity pair. -->
    <SplitPairRule
        window:splitRatio="0.33"
        window:splitMinWidthDp="840"
        window:finishPrimaryWithSecondary="never"
        window:finishSecondaryWithPrimary="always">
      <SplitPairFilter
          window:primaryActivityName=".ListActivity"
          window:secondaryActivityName=".DetailActivity"/>
    </SplitPairRule>

    <!-- Automatically launch a placeholder for the detail activity. -->
    <SplitPlaceholderRule
        window:placeholderActivityName=".PlaceholderActivity"
        window:splitRatio="0.33"
        window:splitMinWidthDp="840"
        window:finishPrimaryWithPlaceholder="always"
        window:stickyPlaceholder="false">
      <ActivityFilter
          window:activityName=".ListActivity"/>
    </SplitPlaceholderRule>

    <!-- Activities that should never be in a split. -->
    <ActivityRule
        window:alwaysExpand="true">
      <ActivityFilter
          window:activityName=".SummaryActivity"/>
    </ActivityRule>

</resources>

Create a placeholder activity

You need to create a new activity to serve as the placeholder specified in the XML configuration file. The activity can be very simple—just something to indicate to users that content will appear here eventually.

Create the activity in the sample app's main source folder.

In Android Studio, do the following:

  1. Right-click (secondary button–click) the sample app source folder, com.example.activity_embedding
  2. Select New > Activity > Empty Views Activity
  3. Name the activity PlaceholderActivity
  4. Select Finish

Android Studio creates the activity in the sample app package, adds the activity to the app manifest file, and creates a layout resource file named activity_placeholder.xml in the res/layout folder.

  1. In the sample app's AndroidManifest.xml file, set the label for the placeholder activity to an empty string:

AndroidManifest.xml

<activity
    android:name=".PlaceholderActivity"
    android:exported="false"
    android:label="" />
  1. Replace the contents of the activity_placeholder.xml layout file in the res/layout folder with the following:

activity_placeholder.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="@color/gray"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".PlaceholderActivity">

  <TextView
      android:id="@+id/textViewPlaceholder"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/placeholder_text"
      android:textSize="36sp"
      android:textColor="@color/obsidian"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. Finally, add the following string resource to the strings.xml resource file in the res/values folder:

strings.xml

<string name="placeholder_text">Placeholder</string>

Create an initializer

The WindowManager RuleController component parses the rules defined in the XML configuration file and makes the rules available to the system.

A Jetpack Startup library Initializer enables RuleController to access the configuration file.

The Startup library performs component initialization at app startup. Initialization must occur before any activities start so that RuleController has access to the split rules and can apply them if necessary.

Add the Startup library dependency

To enable startup functionality, add the Startup library dependency to the sample app's module-level build.gradle file, for example:

build.gradle

implementation 'androidx.startup:startup-runtime:1.1.1'

Implement an initializer for RuleController

Create an implementation of the Startup Initializer interface.

In Android Studio, do the following:

  1. Right-click (secondary button–click) the sample app source folder, com.example.activity_embedding
  2. Select New > Kotlin Class/File or New > Java Class
  3. Name the class SplitInitializer
  4. Press Enter — Android Studio creates the class in the sample app package.
  5. Replace the contents of the class file with the following:

SplitInitializer.kt

package com.example.activity_embedding

import android.content.Context
import androidx.startup.Initializer
import androidx.window.embedding.RuleController

class SplitInitializer : Initializer<RuleController> {

  override fun create(context: Context): RuleController {
    return RuleController.getInstance(context).apply {
      setRules(RuleController.parseRules(context, R.xml.main_split_config))
    }
  }

  override fun dependencies(): List<Class<out Initializer<*>>> {
    return emptyList()
  }
}

SplitInitializer.java

package com.example.activity_embedding;

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.startup.Initializer;
import androidx.window.embedding.RuleController;
import java.util.Collections;
import java.util.List;

public class SplitInitializer implements Initializer<RuleController> {

   @NonNull
   @Override
   public RuleController create(@NonNull Context context) {
      RuleController ruleController = RuleController.getInstance(context);
      ruleController.setRules(
          RuleController.parseRules(context, R.xml.main_split_config)
      );
      return ruleController;
   }

   @NonNull
   @Override
   public List<Class<? extends Initializer<?>>> dependencies() {
       return Collections.emptyList();
   }
}

The initializer makes the split rules available to the RuleController component by passing the ID of the XML resource file that contains the definitions (main_split_config) to the parseRules() method of the component. The setRules() method adds the parsed rules to RuleController.

Create an initialization provider

A provider invokes the split rules initialization process.

Add androidx.startup.InitializationProvider to the <application> element of the sample app's manifest file as a provider, and reference SplitInitializer:

AndroidManifest.xml

<provider android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <!-- Make SplitInitializer discoverable by InitializationProvider. -->
    <meta-data android:name="${applicationId}.SplitInitializer"
        android:value="androidx.startup" />
</provider>

InitializationProvider initializes SplitInitializer, which in turn invokes the RuleController methods that parse the XML configuration file (main_split_config.xml) and add the rules to RuleController (see "Implement an initializer for RuleController" above).

InitializationProvider discovers and initializes SplitInitializer before the app's onCreate() method executes; and so, the split rules are in effect when the main app activity starts.

Source file

Here's the completed app manifest:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

  <application
      android:allowBackup="true"
      android:dataExtractionRules="@xml/data_extraction_rules"
      android:fullBackupContent="@xml/backup_rules"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:supportsRtl="true"
      android:theme="@style/Theme.Activity_Embedding"
      tools:targetApi="32">
    <activity
        android:name=".ListActivity"
        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=".DetailActivity"
        android:exported="false"
        android:label="" />
    <activity
        android:name=".SummaryActivity"
        android:exported="false"
        android:label="" />
    <activity
        android:name=".PlaceholderActivity"
        android:exported="false"
        android:label="" />
    <property
        android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
        android:value="true" />
    <provider
        android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">
      <!-- Make SplitInitializer discoverable by InitializationProvider. -->
      <meta-data
          android:name="${applicationId}.SplitInitializer"
          android:value="androidx.startup" />
    </provider>
  </application>

</manifest>

Initialization shortcut

If you're comfortable mixing XML configuration with WindowManager APIs, you can eliminate the Startup library initializer and the manifest provider for a much simpler implementation.

Once you've created your XML configuration file, do the following:

Step 1: Create a subclass of Application

Your application subclass will be the first class instantiated when the process for your app is created. You'll add the split rules to RuleController in the onCreate() method of your subclass to ensure the rules are in effect before any activities launch.

In Android Studio, do the following:

  1. Right-click (secondary button–click) the sample app source folder, com.example.activity_embedding
  2. Select New > Kotlin Class/File or New > Java Class
  3. Name the class SampleApplication
  4. Press Enter — Android Studio creates the class in the sample app package
  5. Extend the class from the Application supertype

SampleApplication.kt

package com.example.activity_embedding

import android.app.Application

/**
 * Initializer for activity embedding split rules.
 */
class SampleApplication : Application() {

}

SampleApplication.java

package com.example.activity_embedding;

import android.app.Application;

/**
 * Initializer for activity embedding split rules.
 */
public class SampleApplication extends Application {

}

Step 2: Initialize RuleController

Add the split rules from the XML configuration file to RuleController in the onCreate() method of your application subclass.

To add the rules to RuleController, do the following:

  1. Get a singleton instance of RuleController
  2. Use the Java static or Kotlin companion parseRules() method of RuleController to parse the XML file
  3. Add the parsed rules to RuleController with the setRules() method

SampleApplication.kt

override fun onCreate() {
  super.onCreate()
  RuleController.getInstance(this)
    .setRules(RuleController.parseRules(this, R.xml.main_split_config))
}

SampleApplication.java

@Override
public void onCreate() {
  super.onCreate();
  RuleController.getInstance(this)
    .setRules(RuleController.parseRules(this, R.xml.main_split_config));
}

Step 3: Add your subclass name to the manifest

Add the name of your subclass to the <application> element of the app manifest:

AndroidManifest.xml

<application
    android:name=".SampleApplication"
    . . .

Run it!

Build and run the sample app.

On a non-foldable phone, the activities are always stacked—even in landscape orientation:

Detail (secondary) activity stacked on top of list (main) activity on phone in portrait orientation. Detail (secondary) activity stacked on top of list (main) activity on phone in landscape orientation.

On Android 13 (API level 33) and lower, activity embedding is not enabled on non-foldable phones regardless of split minimum width specifications.

Support for activity embedding for non-foldable phones on higher API levels is dependent on whether the device manufacturer has enabled activity embedding.

On a small tablet or the 7 WSVGA (Tablet) emulator, the two activities are stacked in portrait orientation, but they appear side by side in landscape orientation:

List and detail activities stacked in portrait orientation on small tablet. List and detail activities side by side in landscape orientation on small tablet.

On a large tablet or the Pixel C emulator, the activities are stacked in portrait orientation (see "Aspect ratio" below), but appear side by side in landscape orientation:

List and detail activities stacked in portrait orientation on large tablet. List and detail activities side by side in landscape on large tablet.

The summary displays full screen in landscape even though it's launched from within a split:

Summary activity overlaying split in landscape on large tablet.

Aspect ratio

The activity splits are controlled by display aspect ratio in addition to split minimum width. The splitMaxAspectRatioInPortrait and splitMaxAspectRatioInLandscape attributes specify the maximum display aspect ratio (height:width) for which activity splits are displayed. The attributes represent the maxAspectRatioInPortrait and maxAspectRatioInLandscape properties of SplitRule.

If the aspect ratio of a display exceeds the value in either orientation, splits are disabled regardless of the width of the display. The default value for portrait orientation is 1.4 (see SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT), which prevents tall, narrow displays from including splits. By default, splits are always allowed in landscape orientation (see SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT).

The PIxel C emulator has a portrait display width of 900dp, which is wider than the splitMinWidthDp setting in the sample app XML configuration file, so the emulator should show an activity split. But the aspect ratio of the Pixel C in portrait is greater than 1.4, which prevents activity splits from displaying in portrait orientation.

You can set the maximum aspect ratio for portrait and landscape displays in the XML configuration file in the SplitPairRule and SplitPlaceholderRule elements, for example:

main_split_config.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:window="http://schemas.android.com/apk/res/android">

  <!-- Define a split for the named activity pair. -->
  <SplitPairRule
      . . .
      window:splitMaxAspectRatioInPortrait="alwaysAllow"
      window:splitMaxAspectRatioInLandscape="alwaysDisallow"
      . . .
 </SplitPairRule>

  <SplitPlaceholderRule
      . . .
      window:splitMaxAspectRatioInPortrait="alwaysAllow"
      window:splitMaxAspectRatioInLandscape="alwaysDisallow"
      . . .
  </SplitPlaceholderRule>

</resources>

On a large tablet with portrait display width greater than or equal to 840dp or the Pixel C emulator, the activities are side by side in portrait orientation, but stacked in landscape orientation:

List and detail activities side by side in portrait orientation on large tablet. List and detail activities stacked in landscape orientation on large tablet.

Extra credit

Try setting the aspect ratio in the sample app as shown above for portrait and landscape orientations. Test the settings with your large tablet (if the portrait width is 840dp or greater) or the Pixel C emulator. You should see an activity split in portrait orientation but not in landscape.

Determine the portrait aspect ratio of your large tablet (the aspect ratio of the Pixel C is slightly greater than 1.4). Set splitMaxAspectRatioInPortrait to values higher and lower than the aspect ratio. Run the app, and see what results you get.

6. WindowManager API

You can enable activity embedding entirely in code with a single method called from within the onCreate() method of the activity that initiates the split. If you prefer working in code rather than XML, this is the way to go.

Add the WindowManager dependency

Your app needs access to the WindowManager library whether you're creating an XML-based implementation or using API calls. See the "XML configuration" section of this codelab for how to add the WindowManager dependency to your app.

Inform the system

Regardless of whether you use an XML configuration file or WindowManager API calls, your app must notify the system that the app has implemented activity embedding. See the "XML configuration" section of this codelab for how to inform the system of your implementation.

Create a class to manage splits

In this section of the codelab, you'll implement an activity split entirely within a single static or companion object method which you'll call from the sample app's main activity, ListActivity.

Create a class named SplitManager with a method named createSplit that includes a context parameter (some of the API calls require the parameter):

SplitManager.kt

class SplitManager {

    companion object {

        fun createSplit(context: Context) {
        }
}

SplitManager.java

class SplitManager {

    static void createSplit(Context context) {
    }
}

Call the method in the onCreate() method of a subclass of the Application class.

For details on why and how to subclass Application, see "Initialization shortcut" in the "XML configuration" section of this codelab.

SampleApplication.kt

package com.example.activity_embedding

import android.app.Application

/**
 * Initializer for activity embedding split rules.
 */
class SampleApplication : Application() {

  override fun onCreate() {
    super.onCreate()
    SplitManager.createSplit(this)
  }
}

SampleApplication.java

package com.example.activity_embedding;

import android.app.Application;

/**
 * Initializer for activity embedding split rules.
 */
public class SampleApplication extends Application {

  @Override
  public void onCreate() {
    super.onCreate();
    SplitManager.createSplit(this);
  }
}

Create a split rule

Required APIs:

SplitPairRule defines a split rule for a pair of activities.

SplitPairRule.Builder creates a SplitPairRule. The builder takes a set of SplitPairFilter objects as an argument. The filters specify when to apply the rule.

You register the rule with a singleton instance of the RuleController component, which makes the split rules available to the system.

To create a split rule, do the following:

  1. Create a split pair filter that identifies ListActivity and DetailActivity as the activities that share a split:

SplitManager.kt / createSplit()

val splitPairFilter = SplitPairFilter(
    ComponentName(context, ListActivity::class.java),
    ComponentName(context, DetailActivity::class.java),
    null
)

SplitManager.java / createSplit()

SplitPairFilter splitPairFilter = new SplitPairFilter(
    new ComponentName(context, ListActivity.class),
    new ComponentName(context, DetailActivity.class),
    null
);

The filter can include an intent action (third parameter) for the secondary activity launch. If you include an intent action, the filter checks for the action along with the activity name. For activities in your own app, you probably won't filter on intent action, so the argument can be null.

  1. Add the filter to a filter set:

SplitManager.kt / createSplit()

val filterSet = setOf(splitPairFilter)

SplitManager.java / createSplit()

Set<SplitPairFilter> filterSet = new HashSet<>();
filterSet.add(splitPairFilter);
  1. Create layout attributes for the split:

SplitManager.kt / createSplit()

val splitAttributes: SplitAttributes = SplitAttributes.Builder()
      .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
      .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
      .build()

SplitManager.java / createSplit()

SplitAttributes splitAttributes = new SplitAttributes.Builder()
  .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
  .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
  .build();

SplitAttributes.Builder creates an object containing layout attributes:

  • setSplitType: Defines how the available display area is allocated to each activity container. The ratio split type specifies the proportion of the display occupied by the primary container; the secondary container occupies the remaining display area.
  • setLayoutDirection: Specifies how the activity containers are laid out relative to one another, primary container first.
  1. Build a split pair rule:

SplitManager.kt / createSplit()

val splitPairRule = SplitPairRule.Builder(filterSet)
      .setDefaultSplitAttributes(splitAttributes)
      .setMinWidthDp(840)
      .setMinSmallestWidthDp(600)
      .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
      .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
      .setClearTop(false)
      .build()

SplitManager.java / createSplit()

SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
  .setDefaultSplitAttributes(splitAttributes)
  .setMinWidthDp(840)
  .setMinSmallestWidthDp(600)
  .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
  .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
  .setClearTop(false)
  .build();

SplitPairRule.Builder creates and configures the rule:

  • filterSet: Contains split pair filters that determine when to apply the rule by identifying activities that share a split. In the sample app, ListActivity and DetailActivity are specified in a split pair filter (see preceding steps).
  • setDefaultSplitAttributes: Applies layout attributes to the rule.
  • setMinWidthDp: Sets the minimum display width (in density-independent pixels, dp) that allows a split.
  • setMinSmallestWidthDp: Sets the minimum value (in dp) that the smaller of the two display dimensions must have to allow a split, regardless of the device orientation.
  • setFinishPrimaryWithSecondary: Sets how finishing all activities in the secondary container affects the activities in the primary container. NEVER indicates the system should not finish the primary activities when all activities in the secondary container finish. (See Finish activities.)
  • setFinishSecondaryWithPrimary: Sets how finishing all activities in the primary container affects the activities in the secondary container. ALWAYS indicates the system should always finish the activities in the secondary container when all activities in the primary container finish. (See Finish activities.)
  • setClearTop: Specifies whether all activities in the secondary container are finished when a new activity is launched in the container. False specifies that new activities are stacked on top of activities already in the secondary container.
  1. Get the singleton instance of the WindowManager RuleController and add the rule:

SplitManager.kt / createSplit()

val ruleController = RuleController.getInstance(context)
ruleController.addRule(splitPairRule)

SplitManager.java / createSplit()

RuleController ruleController = RuleController.getInstance(context);
ruleController.addRule(splitPairRule);

Create a placeholder rule

Required APIs:

SplitPlaceholderRule defines a rule for an activity that occupies the secondary container when no content is available for that container. To create a placeholder activity, see "Create a placeholder activity" in the "XML configuration" section of this codelab. (For more information, see Placeholders in the Activity embedding developer guide.)

SplitPlaceholderRule.Builder creates a SplitPlaceholderRule. The builder takes a set of ActivityFilter objects as an argument. The objects specify activities with which the placeholder rule is associated. If the filter matches a started activity, the system applies the placeholder rule.

You register the rule with the RuleController component.

To create a split placeholder rule, do the following:

  1. Create an ActivityFilter:

SplitManager.kt / createSplit()

val placeholderActivityFilter = ActivityFilter(
    ComponentName(context, ListActivity::class.java),
    null
)

SplitManager.java / createSplit()

ActivityFilter placeholderActivityFilter = new ActivityFilter(
    new ComponentName(context, ListActivity.class),
    null
);

The filter associates the rule with the sample app's main activity, ListActivity. So, when no detail content is available in the list-detail layout, the placeholder fills the detail area.

The filter can include an intent action (second parameter) for the associated activity launch (ListActivity launch). If you include an intent action, the filter checks for the action along with the activity name. For activities in your own app, you probably won't filter on intent action, so the argument can be null.

  1. Add the filter to a filter set:

SplitManager.kt / createSplit()

val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

SplitManager.java / createSplit()

Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
placeholderActivityFilterSet.add(placeholderActivityFilter);
  1. Create a SplitPlaceholderRule:

SplitManager.kt / createSplit()

val splitPlaceholderRule = SplitPlaceholderRule.Builder(
      placeholderActivityFilterSet,
      Intent(context, PlaceholderActivity::class.java)
    ).setDefaultSplitAttributes(splitAttributes)
     .setMinWidthDp(840)
     .setMinSmallestWidthDp(600)
     .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
     .build()

SplitManager.java / createSplit()

SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
  placeholderActivityFilterSet,
  new Intent(context, PlaceholderActivity.class)
).setDefaultSplitAttributes(splitAttributes)
 .setMinWidthDp(840)
 .setMinSmallestWidthDp(600)
 .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
 .build();

SplitPlaceholderRule.Builder creates and configures the rule:

  • placeholderActivityFilterSet: Contains activity filters that determine when to apply the rule by identifying activities with which the placeholder activity is associated.
  • Intent: Specifies launch of placeholder activity.
  • setDefaultSplitAttributes: Applies layout attributes to the rule.
  • setMinWidthDp: Sets the minimum display width (in density-independent pixels, dp) that allows a split.
  • setMinSmallestWidthDp: Sets the minimum value (in dp) that the smaller of the two display dimensions must have to allow a split, regardless of the device orientation.
  • setFinishPrimaryWithPlaceholder: Sets how finishing the placeholder activity affects the activities in the primary container. ALWAYS indicates the system should always finish the activities in the primary container when the placeholder finishes. (See Finish activities.)
  1. Add the rule to the WindowManager RuleController:

SplitManager.kt / createSplit()

ruleController.addRule(splitPlaceholderRule)

SplitManager.java / createSplit()

ruleController.addRule(splitPlaceholderRule);

Create an activity rule

Required APIs:

ActivityRule can be used to define a rule for an activity that occupies the entire task window, such as a modal dialog. (For more information, see Full-window modal in the Activity embedding developer guide.

SplitPlaceholderRule.Builder creates a SplitPlaceholderRule. The builder takes a set of ActivityFilter objects as an argument. The objects specify activities with which the placeholder rule is associated. If the filter matches a started activity, the system applies the placeholder rule.

You register the rule with the RuleController component.

To create an activity rule, do the following:

  1. Create an ActivityFilter:

SplitManager.kt / createSplit()

val summaryActivityFilter = ActivityFilter(
    ComponentName(context, SummaryActivity::class.java),
    null
)

SplitManager.java / createSplit()

ActivityFilter summaryActivityFilter = new ActivityFilter(
    new ComponentName(context, SummaryActivity.class),
    null
);

The filter specifies the activity for which the rule applies, SummaryActivity.

The filter can include an intent action (second parameter) for the associated activity launch (SummaryActivity launch). If you include an intent action, the filter checks for the action along with the activity name. For activities in your own app, you probably won't filter on intent action, so the argument can be null.

  1. Add the filter to a filter set:

SplitManager.kt / createSplit()

val summaryActivityFilterSet = setOf(summaryActivityFilter)

SplitManager.java / createSplit()

Set<ActivityFilter> summaryActivityFilterSet = new HashSet<>();
summaryActivityFilterSet.add(summaryActivityFilter);
  1. Create an ActivityRule:

SplitManager.kt / createSplit()

val activityRule = ActivityRule.Builder(summaryActivityFilterSet)
      .setAlwaysExpand(true)
      .build()

SplitManager.java / createSplit()

ActivityRule activityRule = new ActivityRule.Builder(
    summaryActivityFilterSet
).setAlwaysExpand(true)
 .build();

ActivityRule.Builder creates and configures the rule:

  • summaryActivityFilterSet: Contains activity filters that determine when to apply the rule by identifying activities that you want to exclude from splits.
  • setAlwaysExpand: Specifies whether or not the activity should expand to fill all available display space.
  1. Add the rule to the WindowManager RuleController:

SplitManager.kt / createSplit()

ruleController.addRule(activityRule)

SplitManager.java / createSplit()

ruleController.addRule(activityRule);

Run it!

Build and run the sample app.

The app should behave the same as it does when customized using an XML configuration file.

See "Run it!" in the "XML configuration" section of this codelab.

Extra credit

Try setting the aspect ratio in the sample app using the setMaxAspectRatioInPortrait and setMaxAspectRatioInLandscape methods of SplitPairRule.Builder and SplitPlaceholderRule.Builder. Specify values with the properties and methods of the EmbeddingAspectRatio class, for example:

SplitPairRule.Builder(filterSet)
  . . .
  .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
  . . .
.build()

Test the settings with your large tablet or the Pixel C emulator.

Determine the portrait aspect ratio of your large tablet (the aspect ratio of the Pixel C is a little greater than 1.4). Set the maximum aspect ratio in portrait to values higher and lower than the aspect ratio of your tablet or the Pixel C. Try the ALWAYS_ALLOW and ALWAYS_DISALLOW properties.

Run the app, and see what results you get.

For more information see "Aspect ratio" in the "XML configuration" section of this codelab.

7. Material Design navigation

Material Design guidelines specify different navigation components for different screen sizes—a navigation rail for screens wider than or equal to 840dp, a bottom navigation bar for screens less than 840dp.

fb47462060f4818d.gif

With activity embedding, you can't use the WindowManager methods getCurrentWindowMetrics() and getMaximumWindowMetrics() to determine screen width because the window metrics returned by the methods describe the display pane that contains the embedded activity that called the methods.

To get accurate dimensions of your activity embedding app, use a split attributes calculator and SplitAttributesCalculatorParams.

Delete the following lines if you added them in a previous section.

main_split_config.xml

<SplitPairRule 
    . . .
    window:splitMaxAspectRatioInPortrait="alwaysAllow" // Delete this line.
    window:splitMaxAspectRatioInLandscape="alwaysDisallow" // Delete this line.
    . . .>
</SplitPairRule>

<SplitPlaceholderRule
    . . .

    window:splitMaxAspectRatioInPortrait="alwaysAllow" // Delete this line.
    window:splitMaxAspectRatioInLandscape="alwaysDisallow" // Delete this line.
    . . .>
<SplitPlaceholderRule/>

Flexible navigation

To dynamically switch navigation components based on screen size, use a SplitAttributes calculator. The calculator detects changes in device orientation and window size and recalculates the display dimensions accordingly. We'll integrate a calculator with a SplitController to trigger navigation component changes in response to screen size updates.

Create navigation layout

First, create a menu that we'll use to populate the navigation rail and navigation bar.

In the res/menu folder, create a new menu resource file named nav_menu.xml. Replace the contents of the menu file with the following:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/navigation_home"
        android:title="Home" />
    <item
        android:id="@+id/navigation_dashboard"
        android:title="Dashboard" />
    <item
        android:id="@+id/navigation_settings"
        android:title="Settings" />
</menu>

Next, add a navigation bar and navigation rail to your layout. Set their visibility to gone so they are initially hidden. We'll make them visible based on layout dimensions later.

activity_list.xml

<com.google.android.material.navigationrail.NavigationRailView
     android:id="@+id/navigationRailView"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     app:layout_constraintStart_toStartOf="parent"
     app:layout_constraintTop_toTopOf="parent"
     app:menu="@menu/nav_menu"
     android:visibility="gone" />

<com.google.android.material.bottomnavigation.BottomNavigationView
   android:id="@+id/bottomNavigationView"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   app:menu="@menu/nav_menu"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintEnd_toEndOf="parent"
   android:visibility="gone" />

Write a function to handle the switch between the navigation bar and navigation rail.

ListActivity.kt / setWiderScreenNavigation()

private fun setWiderScreenNavigation(useNavRail: Boolean) {
   val navRail = findViewById(R.id.navigationRailView)
   val bottomNav = findViewById(R.id.bottomNavigationView)
   if (useNavRail) {
       navRail.visibility = View.VISIBLE
       bottomNav.visibility = View.GONE
   } else {
       navRail.visibility = View.GONE
       bottomNav.visibility = View.VISIBLE
   }
}

ListActivity.java / setWiderScreenNavigation()

private void setWiderScreenNavigation(boolean useNavRail) {
   NavigationRailView navRail = findViewById(R.id.navigationRailView);
   BottomNavigationView bottomNav = findViewById(R.id.bottomNavigationView);
   if (useNavRail) {
       navRail.setVisibility(View.VISIBLE);
       bottomNav.setVisibility(View.GONE);
   } else {
       navRail.setVisibility(View.GONE);
       bottomNav.setVisibility(View.VISIBLE);
   }
}

Split attributes calculator

SplitController gets information about the currently active activity splits and provides interaction points to customize the splits and form new splits.

In previous sections, we set the default attributes for splits by specifying splitRatio and other attributes in the <SplitPairRule> and <SplitPlaceHolderRule> tags in XML files or by using the SplitPairRule.Builder#setDefaultSplitAttributes() and SplitPlaceholderRule.Builder#setDefaultSplitAttributes() APIs.

The default split attributes are applied if the parent container's WindowMetrics satisfy the SplitRule dimension requirements, which are minWidthDp, minHeightDp and minSmallestWidthDp.

We'll set a split attributes calculator to replace the default split attributes. The calculator updates existing split pairs after a change in the window or device state, such as orientation changes or folding state changes.

This allows developers to learn device or window states and set different split attributes in different scenarios, including portrait and landscape orientation and tabletop posture.

When creating a split attributes calculator, the platform passes a SplitAttributesCalculatorParams object to the setSplitAttributesCalculator() function. The parentWindowMetrics property provides application window metrics.

In the following code, the activity checks whether the default constraints are satisfied, that is, width > 840dp and smallest width > 600dp. When the conditions are satisfied, the activities are embedded in a dual-pane layout and the app uses a navigation rail instead of a bottom navigation bar. Otherwise, the activities are displayed full screen with a bottom navigation bar.

ListActivity.kt / setSplitAttributesCalculator()

SplitController.getInstance(this).setSplitAttributesCalculator 
       params ->

   if (params.areDefaultConstraintsSatisfied) {
       // When default constraints are satisfied, use the navigation rail.
       setWiderScreenNavigation(true)
       return@setSplitAttributesCalculator params.defaultSplitAttributes
   } else {
       // Use the bottom navigation bar in other cases.
       setWiderScreenNavigation(false)
       // Expand containers if the device is in portrait or the width is less than 840 dp.
       SplitAttributes.Builder()
           .setSplitType(SPLIT_TYPE_EXPAND)
           .build()
   }
}

ListActivity.java / setSplitAttributesCalculator()

SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
   if (params.areDefaultConstraintsSatisfied()) {
       // When default constraints are satisfied, use the navigation rail.
       setWiderScreenNavigation(true);
       return params.getDefaultSplitAttributes();
   } else {
       // Use the bottom navigation bar in other cases.
       setWiderScreenNavigation(false);
       // Expand containers if the device is in portrait or the width is less than 600 dp.
       return new SplitAttributes.Builder()
               .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
               .build();
   }
});

Good job, your activity embedding app now follows Material Design navigation guidelines!

8. Congratulations!

Well done! You optimized an activity-based app to a list-detail layout on large screens and added Material Design navigation.

You learned two ways of implementing activity embedding:

  • Using an XML configuration file
  • Making Jetpack API calls
  • Implement flexible navigation with Activity Embedding

And you didn't rewrite any of the app's Kotlin or Java source code.

You're ready to optimize your production apps for large screens with activity embedding!

9. Learn more