Cómo compilar un diseño de lista-detalles con incorporación de actividades

1. Introducción

Las pantallas grandes te permiten crear IUs y diseños de apps que mejoran la experiencia del usuario y aumentan su productividad. Sin embargo, si tu app se diseñó para pantallas pequeñas de teléfonos no plegables, es probable que no aproveche el espacio de pantalla adicional que ofrecen las tablets, los dispositivos plegables y los dispositivos ChromeOS.

La actualización de una app para aprovechar al máximo las pantallas grandes puede llevar tiempo y ser costoso, en especial para las apps heredadas que se basan en varias actividades.

La incorporación de actividades, que se introducen en Android 12L (nivel de API 32), permite que las apps que se basan en actividades muestren varias de ellas simultáneamente en pantallas grandes para crear diseños de dos paneles, como lista-detalles. No es necesario volver a programar Kotlin ni Java. Solo debes agregar algunas dependencias, crear un archivo de configuración XML, implementar un inicializador y agregar algunos elementos al manifiesto de tu app. O bien, si prefieres trabajar en código, agrega algunas llamadas a la API de Jetpack WindowManager al método onCreate() de la actividad principal de tu app.

Requisitos previos

Para completar este codelab, necesitarás experiencia en lo siguiente:

  • Compilación de apps para Android
  • Saber trabajar con actividades
  • Saber escribir en formato XML
  • Saber trabajar en Android Studio, incluida la configuración de dispositivos virtuales

Qué compilarás

En este codelab, actualizarás una app que se basa en actividades para admitir un diseño dinámico de dos paneles similar a SlidingPaneLayout. En pantallas pequeñas, la app superpone (apila) actividades una sobre otra en la ventana de tareas.

Actividades A, B y C apiladas en la ventana de tareas.

En pantallas grandes, la app muestra dos actividades simultáneamente en la pantalla, una al lado de la otra o una arriba y otra abajo, según tus especificaciones.

7533496502081c0c.png

Qué aprenderás

Cómo implementar la incorporación de actividades de dos maneras:

  • Con un archivo de configuración XML
  • Con llamadas a la API de Jetpack WindowManager

Requisitos

  • Versión reciente de Android Studio
  • Emulador o teléfono Android
  • Emulador o tablet pequeña de Android
  • Emulador o tablet grande de Android

2. Configuración

Obtén la app de ejemplo

Paso 1: Clona el repositorio

Clona el repositorio de Git de codelabs de pantallas grandes:

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

Otra opción es descargar y desarchivar el archivo ZIP de codelabs de pantallas grandes:

Descargar código fuente

Paso 2: Inspecciona los archivos fuente del codelab

Navega a la carpeta de incorporación de actividades.

Paso 3: Abre el proyecto del codelab

En Android Studio, abre el proyecto de Kotlin o Java.

Lista de archivos de la carpeta de actividades en el repositorio y el archivo ZIP.

La carpeta de incorporación de actividades en el repositorio y el archivo ZIP contiene dos proyectos de Android Studio: uno en Kotlin y otro en Java. Abre el proyecto que prefieras. Los fragmentos de Codelab se proporcionan en ambos lenguajes.

Crea dispositivos virtuales

Si no tienes un teléfono Android, una tablet pequeña o una grande con un nivel de API 32 o superior, abre el Administrador de dispositivos en Android Studio y crea cualquiera de los siguientes dispositivos virtuales que necesites:

  • Teléfono: Pixel 6, nivel de API 32 o superior
  • Tablet pequeña: 7 WSVGA (tablet), nivel de API 32 o superior
  • Tablet grande: Pixel C, nivel de API 32 o superior

3. Ejecuta la app

La app de ejemplo muestra una lista de elementos. Cuando el usuario selecciona un elemento, la app muestra información sobre este.

La app consta de tres actividades:

  • ListActivity: Contiene una lista de elementos en un RecyclerView.
  • DetailActivity: Muestra información sobre un elemento de lista cuando el elemento se selecciona de esta.
  • SummaryActivity: Muestra un resumen de la información cuando se selecciona el elemento de la lista de resumen.

Comportamiento sin incorporación de actividades

Ejecuta la app de ejemplo para comprobar cómo se comporta sin incorporar la actividad:

  1. Ejecuta la app de ejemplo en tu tablet grande o emulador de Pixel C. Aparecerá la actividad principal (lista):

Tablet grande con la app de ejemplo ejecutándose en orientación vertical. Pantalla completa de la actividad de lista.

  1. Selecciona un elemento de la lista para iniciar una actividad secundaria (detalle). La actividad de detalles se superpone a la actividad de lista:

Tablet grande con la app de ejemplo ejecutándose en orientación vertical. Pantalla completa de la actividad de detalles.

  1. Rota la tablet a la orientación horizontal. La actividad secundaria aún se superpone a la actividad principal y ocupa toda la pantalla:

Tablet grande con la app de ejemplo ejecutándose en orientación horizontal. Pantalla completa de la actividad de detalles.

  1. Selecciona el control de retroceso (flecha hacia la izquierda en la barra de la aplicación) para regresar a la lista.
  2. Selecciona el último elemento de la lista, Resumen, para iniciar una actividad de resumen como actividad secundaria. La actividad de resumen se superpone a la actividad de lista:

Tablet grande con la app de ejemplo ejecutándose en orientación vertical. Pantalla completa de la actividad de resumen.

  1. Rota la tablet a la orientación horizontal. La actividad secundaria aún se superpone a la actividad principal y ocupa toda la pantalla:

Tablet grande con la app de ejemplo ejecutándose en orientación horizontal. Pantalla completa de la actividad de resumen.

Comportamiento con incorporación de actividades

Cuando completes este codelab, la orientación horizontal mostrará las actividades de lista y de detalles una al lado de la otra, en un diseño de lista-detalles:

Tablet grande con la app de ejemplo ejecutándose en orientación horizontal. Actividades de lista y detalles en un diseño de lista-detalles.

Sin embargo, configurarás que el resumen se muestre en pantalla completa, aunque la actividad se inicie dentro de una división. El resumen se superpondrá a la división:

Tablet grande con la app de ejemplo ejecutándose en orientación horizontal. Pantalla completa de la actividad de resumen.

4. Fondo

La incorporación de actividades divide la ventana de tareas de la app en dos contenedores: uno principal y otro secundario. Cualquier actividad puede iniciar una división con el inicio de otra actividad. La actividad de inicio ocupa el contenedor principal; la actividad iniciada, el secundario.

La actividad principal puede iniciar actividades adicionales en el contenedor secundario. Luego, las actividades en ambos contenedores pueden iniciar actividades en sus respectivos contenedores. Cada contenedor puede contener una pila de actividades. Si deseas obtener más información, consulta la guía para desarrolladores sobre Incorporación de actividades.

Para configurar que tu app admita la incorporación de actividades, crea un archivo de configuración XML o realiza llamadas a la API de Jetpack WindowManager. Comenzaremos con el enfoque de configuración XML.

5. Configuración XML

La biblioteca de WindowManager de Jetpack crea y administra contenedores y divisiones para la incorporación de actividades en función de las reglas de división que crees en un archivo de configuración XML.

Agrega la dependencia WindowManager

Habilita la app de ejemplo para acceder a la biblioteca de WindowManager. Para ello, agrega la dependencia de la biblioteca al archivo build.gradle de nivel de módulo de la app, por ejemplo:

build.gradle

 implementation 'androidx.window:window:1.1.0'

Informa al sistema

Infórmale al sistema que tu app implementó la incorporación de actividades.

Agrega la propiedad android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED al elemento <application> del archivo de manifiesto de la app y establece el valor en "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>

Los fabricantes de dispositivos (OEMs) usan el parámetro de configuración para habilitar capacidades personalizadas para las apps que admiten la incorporación de actividades. Por ejemplo, los dispositivos pueden aplicar el formato letterbox a actividades solo en orientación vertical (consulta android:screenOrientation) en pantallas horizontales para orientar las actividades para una transición fluida a un diseño de dos paneles para incorporar actividades:

Incorporación de actividades con app solo en orientación vertical en pantalla horizontal. La actividad en formato letterbox y solo en orientación vertical A inicia la actividad B incorporada.

Crea un archivo de configuración

Crea un archivo de recursos XML con el nombre main_split_config.xml en la carpeta res/xml de la app con resources como elemento raíz.

Cambia el espacio de nombres XML a:

main_split_config.xml

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

Regla de par dividido

Agrega la siguiente regla de divisiones al archivo de configuración:

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>

La regla hace lo siguiente:

  • Configura las opciones de división para actividades que comparten una:
  • splitRatio: Especifica qué porcentaje de la ventana de tareas ocupa la actividad principal (33%) y deja el espacio restante para la actividad secundaria.
  • splitMinWidthDp: Especifica el ancho de pantalla mínimo (840) necesario para que ambas actividades aparezcan en pantalla de manera simultánea. Las unidades son píxeles independientes de la pantalla (dp).
  • finishPrimaryWithSecondary: Especifica si las actividades del contenedor dividido principal finalizan (nunca) cuando terminan todas las actividades del contenedor secundario.
  • finishSecondaryWithPrimary: Especifica si las actividades del contenedor secundario finalizan (siempre) cuando terminan todas las actividades del contenedor principal.
  • Incluye un filtro de división que define las actividades que comparten una división de la ventana de tareas. La actividad principal es ListActivity; la secundaria es DetailActivity.

Regla de marcador de posición

Una actividad de marcador de posición ocupa el contenedor secundario de una división de actividad cuando no hay contenido disponible para ese contenedor, por ejemplo, cuando se abre una división de lista-detalles, pero aún no se seleccionó un elemento de la lista (para obtener más información, consulta Marcadores de posición en la guía para desarrolladores sobre Incorporación de actividad).

Agrega la siguiente regla de marcador de posición al archivo de configuración:

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>

La regla hace lo siguiente:

  • Identifica la actividad de marcador de posición, PlaceholderActivity (crearemos esta actividad en el siguiente paso).
  • Configura las opciones del marcador de posición:
  • splitRatio: Especifica qué porcentaje de la ventana de tareas ocupa la actividad principal (33%) y deja el espacio restante para el marcador de posición. Por lo general, este valor debe coincidir con la proporción de división de la regla de par dividido con la que está asociado el marcador de posición.
  • splitMinWidthDp: Especifica el ancho de pantalla mínimo (840) necesario para que el marcador de posición aparezca en pantalla con la actividad principal. Por lo general, este valor debe coincidir con el ancho mínimo de la regla de par dividido con la que está asociado el marcador de posición. Las unidades son píxeles independientes de la pantalla (dp).
  • finishPrimaryWithPlaceholder: Especifica si las actividades del contenedor del contenedor principal dividido finalizan (siempre) cuando lo hace el marcador de posición.
  • stickyPlaceholder: Indica si el marcador de posición debe permanecer en la pantalla (falso) como actividad en la parte superior cuando se cambia el tamaño de la pantalla de dos paneles a una de uno solo, por ejemplo, cuando se pliega un dispositivo plegable.
  • Incluye un filtro de actividad que especifica la actividad (ListActivity) con la que el marcador de posición comparte la división de una ventana de tareas.

El marcador de posición representa la actividad secundaria de la regla de par dividido cuya actividad principal es la misma que la actividad en el filtro de actividad del marcador de posición (consulta "Regla de par dividido" en la sección "Configuración XML" de este codelab).

Regla de actividad

Las reglas de actividad son de uso general. Las actividades que deseas que ocupen toda la ventana de tareas (es decir, que nunca formen parte de una división) se pueden especificar con una regla de actividad (para obtener más información, consulta Ventana modal completa en la guía para desarrolladores sobre la Incorporación de actividades).

Haremos que la actividad de resumen ocupe toda la ventana de la tarea y se superponga a la división. La navegación hacia atrás regresará a la división.

Agrega la siguiente regla de actividad al archivo de configuración:

main_split_config.xml

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

La regla hace lo siguiente:

  • Identifica la actividad que se debe mostrar en la ventana completa (SummaryActivity).)
  • Configura las opciones para la actividad:
  • alwaysExpand: Especifica si se debe expandir la actividad para que ocupe todo el espacio disponible de la pantalla.

Archivo de origen

El archivo de configuración XML finalizado debería verse de la siguiente manera:

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>

Crea una actividad de marcador de posición

Debes crear una actividad nueva para que funcione como el marcador de posición que se especifica en el archivo de configuración XML. La actividad puede ser muy sencilla: solo una para indicarles a los usuarios que el contenido aparecerá aquí más adelante.

Crea la actividad en la carpeta de origen principal de la app de ejemplo.

En Android Studio, haz lo siguiente:

  1. Haz clic con el botón derecho (clic con el botón secundario) en la carpeta de origen de la app de ejemplo, com.example.activity_embedding.
  2. Selecciona New > Activity > Empty Views Activity.
  3. Asígnele el nombre PlaceholderActivity a la actividad.
  4. Selecciona Finish.

Android Studio crea la actividad en el paquete de la app de ejemplo, la agrega al archivo de manifiesto de la app y crea un archivo de recursos de diseño con el nombre activity_placeholder.xml en la carpeta res/layout.

  1. En el archivo AndroidManifest.xml de la app de ejemplo, establece la etiqueta para la actividad del marcador de posición en una cadena vacía:

AndroidManifest.xml

<activity
    android:name=".PlaceholderActivity"
    android:exported="false"
    android:label="" />
  1. Reemplaza el contenido del archivo de diseño activity_placeholder.xml en la carpeta res/layout por lo siguiente:

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. Por último, agrega el siguiente recurso de cadenas al archivo de recursos strings.xml en la carpeta res/values:

strings.xml

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

Crea un inicializador

El componente RuleController de WindowManager analiza las reglas que se definen el archivo de configuración XML y las pone a disposición del sistema.

Un inicializador de la bibliotecaStartup de Jetpack le permite a RuleController acceder al archivo de configuración.

La biblioteca Startup realiza la inicialización de componentes cuando se inicia la app. La inicialización debe ocurrir antes de que comiencen las actividades para que RuleController tenga acceso a las reglas de división y pueda aplicarlas si es necesario.

Agrega la dependencia de la biblioteca Startup

Para habilitar la funcionalidad de inicio, agrega la dependencia de la biblioteca Startup al archivo build.gradle de nivel de módulo de la app de ejemplo, por ejemplo:

build.gradle

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

Implementa un inicializador para RuleController

Crea una implementación de la interfaz del inicializador de Startup.

En Android Studio, haz lo siguiente:

  1. Haz clic con el botón derecho (clic con el botón secundario) en la carpeta de origen de la app de ejemplo.
  2. Selecciona New > Kotlin Class/File o New > Java Class
  3. Asígnele el nombre SplitInitializer a la clase.
  4. Presiona Intro: Android Studio creará la clase en el paquete de la app de ejemplo.
  5. Reemplaza el contenido del archivo de la clase por lo siguiente:

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();
   }
}

El inicializador pone a disposición las reglas de división para el componente RuleController pasando el ID del archivo de recursos XML que contiene las definiciones ((main_split_config) al método parseRules() del componente. El método setRules() agrega las reglas analizadas a RuleController.

Crea un proveedor de inicialización

Un proveedor invoca el proceso de inicialización de las reglas de división.

Agrega androidx.startup.InitializationProvider al elemento <application> del archivo de manifiesto de la app de ejemplo como proveedor y haz referencia a 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 inicializa SplitInitializer, que, a su vez, invoca los métodos RuleController que analizan el archivo de configuración XML (main_split_config.xml) y agrega las reglas a RuleController (consulta "Implementa un inicializador para RuleController" más arriba).

InitializationProvider detecta y luego inicializa SplitInitializer antes de que se ejecute el método onCreate() de la app; de esta manera, las reglas de división estarán vigentes cuando comience la actividad principal de la app.

Archivo de origen

Este es el manifiesto completo de la app:

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>

Acceso directo de inicialización

Si deseas integrar la configuración XML con las APIs de WindowManager, puedes eliminar el inicializador de la biblioteca Startup y el proveedor de manifiestos para lograr una implementación mucho más sencilla.

Una vez que hayas creado tu archivo de configuración XML, haz lo siguiente:

Paso 1: Crea una subclase de Application

La subclase de tu aplicación será la primera instancia de clase que se genera cuando se crea el proceso de tu app. Agregarás las reglas de división a RuleController en el método onCreate() de tu subclase para garantizar que las reglas estén vigentes antes de que se inicien las actividades.

En Android Studio, haz lo siguiente:

  1. Haz clic con el botón derecho (clic con el botón secundario) en la carpeta de origen de la app de ejemplo.
  2. Selecciona New > Kotlin Class/File o New > Java Class
  3. Asígnale el nombre SampleApplication a la clase.
  4. Presiona Intro: Android Studio creará la clase en el paquete de la app de ejemplo.
  5. Extiende la clase del supertipo Application.

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 {

}

Paso 2: Inicializa RuleController

Agrega las reglas de división del archivo de configuración XML a RuleController en el método onCreate() de la subclase de tu aplicación.

Para agregar reglas a RuleController, haz lo siguiente:

  1. Obtén una instancia singleton de RuleController
  2. Usa el método parseRules() estático de Java o complementario de Kotlin de RuleController para analizar el archivo en formato XML.
  3. Agrega las reglas analizadas a RuleController con el método setRules().

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));
}

Paso 3: Agrega el nombre de tu subclase al manifiesto

Agrega el nombre de tu subclase al elemento <application> del manifiesto de la app:

AndroidManifest.xml

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

¡Ejecútalo!

Compila y ejecuta la app de ejemplo.

En un teléfono no plegable, las actividades siempre se apilan, incluso en orientación horizontal:

Actividad de detalle (secundaria) apilada sobre la actividad de lista (principal) en un teléfono con orientación vertical. Actividad de detalle (secundaria) apilada sobre la actividad de lista (principal) en un teléfono con orientación horizontal.

En Android 13 (nivel de API 33) y versiones anteriores, la incorporación de actividades no está habilitada en teléfonos no plegables, independientemente de las especificaciones de ancho mínimo de la división.

La compatibilidad con la incorporación de actividades en teléfonos no plegables en niveles superiores de API depende de si el fabricante del dispositivo habilitó esta incorporación.

En una tablet pequeña o en el emulador de 7 WSVGA (tablet), las dos actividades se apilan en orientación vertical, pero aparecen una al lado de la otra en orientación horizontal:

Actividades de lista y detalle apiladas en orientación vertical en una tablet pequeña. Actividades de lista y detalle una al lado de la otra en orientación horizontal en una tablet pequeña.

En una tablet grande o en el emulador de Pixel C, las actividades se apilan en orientación vertical (consulta la sección "Relación de aspecto" más abajo), pero aparecen una al lado de la otra en orientación horizontal:

Actividades de lista y detalle apiladas en orientación vertical en una tablet grande. Actividades de lista y detalle una al lado de la otra en orientación horizontal en una tablet grande.

El resumen se muestra en pantalla completa en orientación horizontal aunque se haya iniciado dentro de una división:

Actividad de resumen que se superpone a la división en orientación horizontal en tablets grandes.

Relación de aspecto

Las divisiones de actividad se controlan mediante la relación de aspecto de la pantalla, además del ancho mínimo de la división. Los atributos splitMaxAspectRatioInPortrait y splitMaxAspectRatioInLandscape especifican la relación de aspecto máxima de la pantalla (altura:ancho) para la que se muestran las divisiones de actividad. Los atributos representan las propiedades maxAspectRatioInPortrait y maxAspectRatioInLandscape de SplitRule.

Si la relación de aspecto de una pantalla supera el valor en cualquiera de las orientaciones, se inhabilitan las divisiones, independientemente del ancho de la pantalla. El valor predeterminado para la orientación vertical es 1.4 (consulta SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT), lo que evita que las pantallas altas y angostas incluyan divisiones. De forma predeterminada, las divisiones siempre se permiten en orientación horizontal (consulta SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT).

El emulador de Pixel C tiene un ancho de pantalla vertical de 900 dp, que es más ancho que la configuración de splitMinWidthDp del archivo de configuración XML de la app de ejemplo, por lo que el emulador debería mostrar una división de actividad. Sin embargo, la relación de aspecto del Pixel C en posición vertical es superior a 1.4, por lo que no se muestran divisiones de actividad en orientación vertical.

Puedes establecer la relación de aspecto máxima para las pantallas verticales y horizontales en el archivo de configuración XML en los elementos SplitPairRule y SplitPlaceholderRule, por ejemplo:

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>

En una tablet grande con un ancho de pantalla vertical superior o igual a 840 dp o en el emulador de Pixel C, las actividades están una al lado de la otra en orientación vertical, pero se apilan en orientación horizontal:

Actividades de lista y detalle una al lado de la otra en orientación vertical en una tablet grande. Actividades de lista y detalle apiladas en orientación horizontal en una tablet grande.

Crédito extra

Intenta configurar la relación de aspecto en la app de ejemplo, como se muestra más arriba para las orientaciones vertical y horizontal. Prueba la configuración con tu tablet grande (si el ancho vertical es de 840 dp o superior) o con el emulador de Pixel C. Deberías ver una actividad dividida en orientación vertical, pero no en orientación horizontal.

Determina la relación de aspecto vertical de tu tablet grande (la relación de aspecto del Pixel C es ligeramente superior a 1.4). Establece splitMaxAspectRatioInPortrait en valores superior e inferiores a la relación de aspecto. Ejecuta la app y observa los resultados que obtienes.

6. API de WindowManager

Puedes habilitar la incorporación de actividades por completo en el código con un solo método que se llame desde el método onCreate() de la actividad que inicia la división. Si prefieres trabajar en el código en lugar de XML, esta es la manera de hacerlo.

Agrega la dependencia WindowManager

Tu app necesita acceso a la biblioteca de WindowManager, ya sea que crees una implementación basada en XML o uses llamadas a la API. Consulta la sección "Configuración XML" de este codelab para obtener información sobre cómo agregar la dependencia WindowManager a tu app.

Informa al sistema

Independientemente de que uses un archivo de configuración XML o llamadas a la API de WindowManager, tu app debe notificar al sistema que se implementó la incorporación de actividades. Consulta la sección "Configuración XML" de este codelab para descubrir cómo informar al sistema sobre tu implementación.

Crea una clase para administrar las divisiones

En esta sección del codelab, implementarás una actividad dividida completamente dentro de un solo método de objeto estático o complementario al que llamarás desde la actividad principal de la app de ejemplo, ListActivity.

Crea una clase llamada SplitManager con un método llamado createSplit que incluya un parámetro context (algunas de las llamadas a la API requieren este parámetro):

SplitManager.kt

class SplitManager {

    companion object {

        fun createSplit(context: Context) {
        }
}

SplitManager.java

class SplitManager {

    static void createSplit(Context context) {
    }
}

Llama al método en onCreate() de una subclase de Application.

Para obtener detalles sobre por qué y cómo crear una subclase de Application, consulta "Acceso directo de inicialización" en la sección "Configuración XML" de este 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);
  }
}

Crea una regla de división

APIs requeridas:

SplitPairRule define una regla de división para un par de actividades.

SplitPairRule.Builder crea un SplitPairRule. El compilador toma un conjunto de objetos SplitPairFilter como argumento. Los filtros especifican cuándo aplicar la regla.

Registras la regla con una instancia singleton del componente RuleController, que pone las reglas de división a disposición del sistema.

Para crear una regla de división, haz lo siguiente:

  1. Crea un filtro de par dividido que identifique ListActivity y DetailActivity como las actividades que comparten una división:

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
);

El filtro puede incluir una acción de intent (tercer parámetro) para el inicio de la actividad secundaria. Si incluyes una acción de intent, el filtro verifica la acción junto con el nombre de la actividad. Para las actividades de tu propia app, es probable que no filtres la acción de intent, por lo que el argumento puede ser nulo.

  1. Agrega el filtro a un conjunto de filtros:

SplitManager.kt / createSplit()

val filterSet = setOf(splitPairFilter)

SplitManager.java / createSplit()

Set<SplitPairFilter> filterSet = new HashSet<>();
filterSet.add(splitPairFilter);
  1. Crea atributos de diseño para la división:

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 crea un objeto que contiene atributos de diseño:

  • setSplitType: Define cómo se asigna el área de visualización disponible a cada contenedor de actividades. El tipo de división de proporción especifica la proporción de visualización que ocupa el contenedor principal; el contenedor secundario ocupa el resto del área de visualización.
  • setLayoutDirection: Especifica cómo se distribuyen los contenedores de actividades uno respecto del otro (el contenedor principal se distribuye primero).
  1. Compila una regla de par dividido:

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 crea y configura la regla:

  • filterSet: Contiene filtros de par dividido que determinan cuándo aplicar la regla mediante la identificación de actividades que comparten una división. En la app de ejemplo, ListActivity y DetailActivity se especifican en un filtro de par dividido (consulta los pasos anteriores).
  • setDefaultSplitAttributes: Aplica los atributos de diseño a la regla.
  • setMinWidthDp: Establece el ancho de pantalla mínimo (en píxeles independientes de la densidad, dp) que permite una división.
  • setMinSmallestWidthDp: Establece el valor mínimo (en dp) que debe tener la menor de las dos dimensiones de pantalla para permitir una división, independientemente de la orientación del dispositivo.
  • setFinishPrimaryWithSecondary: Indica cómo el hecho de finalizar las actividades del contenedor secundario afecta las actividades del contenedor principal. NEVER indica que el sistema no debe finalizar las actividades principales cuando terminan todas las actividades del contenedor secundario (consulta Finalizar actividades).
  • setFinishSecondaryWithPrimary: Indica cómo el hecho de finalizar las actividades del contenedor principal afecta las actividades del contenedor secundario. ALWAYS indica que el sistema siempre debe finalizar las actividades del contenedor secundario cuando terminan todas las actividades del contenedor principal (consulta Finalizar actividades).
  • setClearTop: Determina si todas las actividades del contenedor secundario finalizan cuando se inicia una nueva actividad en el contenedor. "False" especifica que las actividades nuevas se apilan sobre las que ya se encuentran en el contenedor secundario.
  1. Obtén la instancia singleton de WindowManager RuleController y agrega la regla:

SplitManager.kt / createSplit()

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

SplitManager.java / createSplit()

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

Crea una regla de marcador de posición

APIs requeridas:

SplitPlaceholderRule define una regla para una actividad que ocupa el contenedor secundario cuando no hay contenido disponible para ese contenedor. Para crear una actividad de marcador de posición, consulta "Crea una actividad de marcador de posición" en la sección "Configuración XML" de este codelab (para obtener más información, consulta Marcadores de posición en la guía para desarrolladores sobre Incorporación de actividad).

SplitPlaceholderRule.Builder crea un SplitPlaceholderRule. El compilador toma un conjunto de objetos ActivityFilter como argumento. Los objetos especifican actividades con las que está asociada la regla de marcador de posición. Si el filtro coincide con una actividad iniciada, el sistema aplica la regla de marcador de posición.

Registras la regla con el componente RuleController.

Para crear una regla de marcador de posición dividida, haz lo siguiente:

  1. Crea un 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
);

El filtro asocia la regla con la actividad principal de la app de ejemplo, ListActivity. Por lo tanto, cuando no hay contenido de detalles disponible en el diseño de lista-detalles, el marcador de posición ocupa el área de detalles.

El filtro puede incluir una acción de intent (segundo parámetro) para el inicio de la actividad asociada (inicio de ListActivity). Si incluyes una acción de intent, el filtro verifica la acción junto con el nombre de la actividad. Para las actividades de tu propia app, es probable que no filtres la acción de intent, por lo que el argumento puede ser nulo.

  1. Agrega el filtro a un conjunto de filtros:

SplitManager.kt / createSplit()

val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

SplitManager.java / createSplit()

Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
placeholderActivityFilterSet.add(placeholderActivityFilter);
  1. Crear un 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 crea y configura la regla:

  • placeholderActivityFilterSet: Contiene filtros de actividad que determinan cuándo aplicar la regla mediante la identificación de actividades con las que se asocia la actividad del marcador de posición.
  • Intent: Especifica el inicio de la actividad de marcador de posición.
  • setDefaultSplitAttributes: Aplica los atributos de diseño a la regla.
  • setMinWidthDp: Establece el ancho de pantalla mínimo (en píxeles independientes de la densidad, dp) que permite una división.
  • setMinSmallestWidthDp: Establece el valor mínimo (en dp) que debe tener la menor de las dos dimensiones de pantalla para permitir una división, independientemente de la orientación del dispositivo.
  • setFinishPrimaryWithPlaceholder: Establece cómo el hecho de finalizar la actividad del marcador de posición afecta a las actividades del contenedor principal ALWAYS indica que el sistema siempre debe finalizar las actividades del contenedor principal cuando termina el marcador de posición (consulta Finalizar actividades).
  1. Agrega la regla a RuleController de WindowManager:

SplitManager.kt / createSplit()

ruleController.addRule(splitPlaceholderRule)

SplitManager.java / createSplit()

ruleController.addRule(splitPlaceholderRule);

Crea una regla de actividad

APIs requeridas:

Se puede usar ActivityRule para definir una regla para una actividad que ocupa toda la ventana de tareas, como un diálogo modal (para obtener más información, consulta Ventana modal completa en la guía para desarrolladores sobre la Incorporación de actividades).

SplitPlaceholderRule.Builder crea un SplitPlaceholderRule. El compilador toma un conjunto de objetos ActivityFilter como argumento. Los objetos especifican actividades con las que está asociada la regla de marcador de posición. Si el filtro coincide con una actividad iniciada, el sistema aplica la regla de marcador de posición.

Registras la regla con el componente RuleController.

Para crear una regla de actividad haz lo siguiente:

  1. Crea un 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
);

El filtro especifica la actividad para la que se aplica la regla, SummaryActivity.

El filtro puede incluir una acción de intent (segundo parámetro) para el inicio de la actividad asociada (inicio de SummaryActivity). Si incluyes una acción de intent, el filtro verifica la acción junto con el nombre de la actividad. Para las actividades de tu propia app, es probable que no filtres la acción de intent, por lo que el argumento puede ser nulo.

  1. Agrega el filtro a un conjunto de filtros:

SplitManager.kt / createSplit()

val summaryActivityFilterSet = setOf(summaryActivityFilter)

SplitManager.java / createSplit()

Set<ActivityFilter> summaryActivityFilterSet = new HashSet<>();
summaryActivityFilterSet.add(summaryActivityFilter);
  1. Crea un 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 crea y configura la regla:

  • summaryActivityFilterSet: Contiene filtros de actividad que determinan cuándo aplicar la regla mediante la identificación de actividades que deseas excluir de las divisiones.
  • setAlwaysExpand: Especifica si se debe expandir la actividad para que ocupe todo el espacio disponible de la pantalla.
  1. Agrega la regla a RuleController de WindowManager:

SplitManager.kt / createSplit()

ruleController.addRule(activityRule)

SplitManager.java / createSplit()

ruleController.addRule(activityRule);

¡Ejecútalo!

Compila y ejecuta la app de ejemplo.

La app debe comportarse de la misma manera que cuando se personaliza con un archivo de configuración XML.

Consulta "¡Ejecútalo!" en la sección "Configuración XML" de este codelab.

Crédito extra

Intenta configurar la relación de aspecto en la app de ejemplo con los métodos setMaxAspectRatioInPortrait y setMaxAspectRatioInLandscape de SplitPairRule.Builder y SplitPlaceholderRule.Builder. Especifica valores con las propiedades y los métodos de la clase EmbeddingAspectRatio, por ejemplo:

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

Prueba la configuración con tu tablet grande o con el emulador de Pixel C.

Determina la relación de aspecto vertical de tu tablet grande (la relación de aspecto del Pixel C es un poco superior a 1.4). Establece la relación de aspecto máxima en orientación vertical para valores superiores e inferiores a la relación de aspecto de tu tablet o Pixel C. Prueba las propiedades ALWAYS_ALLOW y ALWAYS_DISALLOW.

Ejecuta la app y observa los resultados que obtienes.

Para obtener más información, consulta "Relación de aspecto" en la sección "Configuración XML" de este codelab.

7. ¡Felicitaciones!

¡Bien hecho! Optimizaste una app que se basa en actividades para que tenga un diseño de lista-detalles en pantallas grandes.

Aprendiste dos formas de implementar la incorporación de actividades:

  • Con un archivo de configuración XML
  • Con llamadas a la API de Jetpack

Tampoco volviste a escribir el código fuente de Kotlin ni Java de la app.

Ya puedes optimizar tus apps de producción para pantallas grandes con incorporación de actividades.

8. Más información