Cómo compilar apps adaptables con Jetpack Compose

1. Introducción

En este codelab, aprenderás a compilar apps adaptables para teléfonos, tablets y plegables, y cómo estas mejoran la accesibilidad con Jetpack Compose. También conocerás las prácticas recomendadas para usar temas y componentes de Material 3.

Antes de comenzar, es importante comprender qué entendemos por adaptabilidad.

Adaptabilidad

La IU de tu app debe ser responsiva para los diferentes tamaños de ventana, orientaciones y factores de forma. Un diseño adaptable cambia en función del espacio de pantalla disponible. Estos cambios varían desde simples ajustes de diseño para llenar espacio, con la elección de los estilos de navegación respectivos, hasta cambios completos de diseño para aprovechar espacio adicional.

Para obtener más información, consulta Diseño adaptable.

En este codelab, explorarás cómo usar la adaptabilidad y pensar en ella cuando uses Jetpack Compose. Compilarás una aplicación, llamada Reply, que te mostrará cómo implementar la adaptabilidad para todo tipo de pantallas, y cómo la adaptabilidad y la accesibilidad funcionan en conjunto para brindar a los usuarios una experiencia óptima.

Qué aprenderás

  • Cómo diseñar tu app para que se oriente a todos los tamaños de ventana con Jetpack Compose
  • Cómo segmentar tu app para diferentes dispositivos plegables
  • Cómo usar diferentes tipos de navegación para lograr una mejor accesibilidad
  • Cómo usar los componentes de Material 3 para proporcionar la mejor experiencia para cada tamaño de ventana

Qué necesitarás

En este codelab, usarás el emulador de tamaño variable, que te permite alternar entre diferentes tipos de dispositivos y tamaños de ventana.

Emulador de tamaño variable con opciones de teléfono, dispositivo desplegado, tablet y computadora de escritorio.

Si no estás familiarizado con Compose, antes de completar este codelab, considera realizar el codelab de los principios básicos de Jetpack Compose.

Qué crearás

  • Una app cliente de correo electrónico interactiva que usa prácticas recomendadas para diseños adaptables, diferentes navegaciones de Material y un uso óptimo del espacio de pantalla.

Demostración de la compatibilidad con varios dispositivos que obtendrás en este codelab

2. Prepárate

Para obtener el código necesario para este codelab, clona el repositorio de GitHub desde la línea de comandos:

git clone https://github.com/android/codelab-android-compose.git
cd codelab-android-compose/AdaptiveUiCodelab

También tienes la opción de descargar el repositorio como archivo ZIP:

Te recomendamos que comiences con el código de la rama main y sigas el codelab paso a paso a tu propio ritmo.

Abre el proyecto en Android Studio

  1. En la ventana Welcome to Android Studio, selecciona c01826594f360d94.pngOpen an Existing Project.
  2. Selecciona la carpeta <Download Location>/AdaptiveUiCodelab (asegúrate de seleccionar el directorio AdaptiveUiCodelab que contiene build.gradle).
  3. Cuando Android Studio haya importado el proyecto, prueba si puedes ejecutar la rama main.

Cómo explorar el código de inicio

El código de la rama main contiene el paquete ui. Trabajarás con los siguientes archivos en ese paquete:

  • MainActivity.kt: Es la actividad de punto de entrada donde inicias tu app.
  • ReplyApp.kt: Contiene elementos componibles de la IU de la pantalla principal.
  • ReplyHomeViewModel.kt: Proporciona los datos y el estado de la IU para el contenido de la app.
  • ReplyListContent.kt: Contiene elementos componibles para proporcionar pantallas de detalles y listas.

Primero, debes enfocarte en MainActivity.kt.

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )
        }
    }
}

Si ejecutas esta app en un emulador de tamaño variable y pruebas diferentes tipos de dispositivos, como un teléfono o una tablet, la IU simplemente se expande al espacio determinado en lugar de aprovechar el espacio de pantalla o proporcionar una ergonomía de accesibilidad.

Pantalla inicial del teléfono

Vista extendida inicial de la tablet

Lo actualizarás para aprovechar el espacio de la pantalla, aumentar la usabilidad y mejorar la experiencia general del usuario.

3. Cómo lograr que las apps sean adaptables

En esta sección, se explica qué significa hacer que las apps sean adaptables y qué componentes proporciona Material 3 para hacerlo más fácil. También abarca los tipos de pantallas y estados a los que te orientarás, incluidos teléfonos, tablets, tablets grandes y plegables.

Empezarás repasando los aspectos básicos de los tamaños de ventana, las posiciones de plegado y los diferentes tipos de opciones de navegación. Luego, puedes usar estas APIs en tu app para que se adapte mejor.

Tamaños de las ventanas

Los dispositivos Android pueden tener diferentes formas y tamaños, desde teléfonos y plegables hasta tablets y dispositivos ChromeOS. Para admitir tantos tamaños de ventana como sea posible, tu IU debe ser responsiva y adaptable. Para ayudarte a encontrar el umbral correcto en el que se debe cambiar la IU de tu app, definimos valores de puntos de interrupción que ayudan a clasificar los dispositivos en clases de tamaño predefinidas (compactas, medianas y expandidas), llamadas clases de tamaño de ventana. Estos son un conjunto de puntos de interrupción de viewports bien definidos que te ayudan a diseñar, desarrollar y probar diseños de aplicaciones responsivos y adaptables.

Las categorías se eligieron específicamente a los efectos de equilibrar la simplicidad del diseño, con la flexibilidad para optimizar tu app en casos únicos. El espacio de pantalla disponible para la app siempre determina la clase de tamaño de ventana, que puede no ser la pantalla física completa debido a la realización de varias tareas a la vez o a otras segmentaciones.

WindowWidthSizeClass para el ancho compacto, medio y expandido.

WindowHeightSizeClass para altura compacta, mediana y expandida.

El ancho y la altura se clasifican por separado, por lo que en cualquier momento, tu app tiene dos clases de tamaño de ventana: una para el ancho y otra para la altura. El ancho disponible suele ser más importante que la altura disponible debido a la ubicuidad del desplazamiento vertical, por lo que, en este caso, también usarás clases de tamaño de ancho.

Estados de plegado

Los dispositivos plegables presentan aún más situaciones a las que tu app puede adaptarse debido a sus diferentes tamaños y a la presencia de bisagras. Las bisagras pueden oscurecer parte de la pantalla, lo que hace que esa área no sea adecuada para mostrar contenido. También podrían separarse, lo que significa que hay dos pantallas físicas separadas cuando el dispositivo está desplegado.

Posiciones plegables, planas y semiabiertas

Además, el usuario podría estar mirando la pantalla interior mientras la bisagra está parcialmente abierta, lo que daría como resultado distintas posiciones físicas según la orientación del pliegue: la posición en la mesa (pliegue horizontal, que se muestra a la derecha en la imagen de arriba) y la posición de libro (pliegue vertical).

Obtén más información sobre las posturas y bisagras de plegado.

Todas estas opciones son aspectos que se deben tener en cuenta a la hora de implementar diseños adaptables compatibles con dispositivos plegables.

Obtén información adaptable

La biblioteca de adaptive de Material3 proporciona acceso conveniente a la información sobre la ventana en la que se ejecuta tu app.

  1. Agrega entradas para este artefacto y su versión al archivo del catálogo de versiones:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. En el archivo de compilación del módulo de la app, agrega la nueva dependencia de la biblioteca y, luego, realiza una sincronización de Gradle:

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

Ahora, en cualquier alcance componible, puedes usar currentWindowAdaptiveInfo() para obtener un objeto WindowAdaptiveInfo que contenga información como la clase de tamaño de ventana actual y si el dispositivo está en una posición plegable, como la de mesa.

Puedes probarlo ahora en MainActivity.

  1. En onCreate(), dentro del bloque ReplyTheme, obtén la información adaptable de la ventana y muestra las clases de tamaño en un elemento Text componible (puedes agregar esto después del elemento ReplyApp()):

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )

            val adaptiveInfo = currentWindowAdaptiveInfo()
            val sizeClassText =
                "${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
                "${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
            Text(
                text = sizeClassText,
                color = Color.Magenta,
                modifier = Modifier.padding(20.dp)
            )
        }
    }
}

Cuando se ejecute la app, se mostrarán las clases de tamaño de ventana impresas en el contenido de la app. No dudes en explorar qué más se proporciona en la información adaptable de la ventana. Luego, puedes quitar este Text, ya que cubre el contenido de la app y no será necesario para los siguientes pasos.

4. Navegación dinámica

Ahora, adaptarás la navegación de la app a medida que cambie el estado y el tamaño del dispositivo para mejorar la accesibilidad.

La accesibilidad es la capacidad de navegar o iniciar una interacción con una app sin requerir posiciones extremas de las manos ni cambiar la ubicación de las manos. Cuando los usuarios sostienen un teléfono, sus dedos suelen estar en la parte inferior de la pantalla. Cuando los usuarios sostienen un dispositivo plegable o una tablet abiertos, suelen tener los dedos cerca de los lados. Cuando diseñes tu app y decidas dónde colocar los elementos interactivos de la IU en el diseño, ten en cuenta las implicaciones ergonómicas de las diferentes regiones de la pantalla.

  • ¿Qué áreas son cómodas de alcanzar mientras se sostiene el dispositivo?
  • ¿A qué áreas se puede llegar solo extendiendo los dedos, lo que puede resultar molesto?
  • ¿Qué áreas son difíciles de alcanzar o están lejos del lugar donde el usuario sostiene el dispositivo?

La navegación es lo primero con lo que interactúan los usuarios y contiene acciones de gran importancia relacionadas con los recorridos críticos del usuario, por lo que debe colocarse en las áreas de más fácil acceso. Material proporciona varios componentes que te ayudan a implementar la navegación, según la clase de tamaño de ventana del dispositivo.

Navegación inferior

La navegación inferior es perfecta para tamaños compactos, ya que naturalmente sostenemos el dispositivo de modo que nuestro dedo pulgar pueda llegar con facilidad a todos los puntos táctiles de navegación inferior. Úsala siempre que tengas un tamaño de dispositivo compacto o plegable en ese estado.

Barra de navegación inferior con elementos

Para un tamaño de ventana mediano, el riel de navegación es ideal para la accesibilidad, ya que nuestro dedo pulgar cae naturalmente a un lado del dispositivo. También puedes combinar un riel de navegación con un panel lateral de navegación para mostrar más información.

Riel de navegación con elementos

El panel lateral de navegación proporciona una forma fácil de ver información detallada de las pestañas de navegación, y es fácil de acceder cuando usas tablets o dispositivos más grandes. Hay dos tipos de paneles laterales de navegación disponibles: uno modal y uno permanente.

Panel lateral de navegación modal

Puedes usar un panel lateral de navegación modal para teléfonos y tablets de tamaño compacto a mediano, ya que se puede ocultar o expandir como una superposición en el contenido. A veces, se puede combinar con un riel de navegación.

Panel lateral de navegación modal con elementos

Panel lateral de navegación permanente

Puedes usar un panel lateral de navegación permanente para la navegación fija en tablets, Chromebooks y computadoras de escritorio grandes.

Panel lateral de navegación permanente con elementos

Cómo implementar la navegación dinámica

Ahora, cambiarás entre los diferentes tipos de navegación a medida que cambien el estado y el tamaño del dispositivo.

Actualmente, la app siempre muestra un NavigationBar debajo del contenido de la pantalla, independientemente del estado del dispositivo. En su lugar, puedes usar el componente NavigationSuiteScaffold de Material para cambiar automáticamente entre los diferentes componentes de navegación según información como la clase de tamaño de ventana actual.

  1. Agrega la dependencia de Gradle para obtener este componente actualizando el catálogo de versiones y la secuencia de comandos de compilación de la app. Luego, realiza una sincronización de Gradle:

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0-beta01"

[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.navigation.suite)
}
  1. Busca la función de componibilidad ReplyNavigationWrapper() en ReplyApp.kt y reemplaza Column y su contenido por NavigationSuiteScaffold:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    NavigationSuiteScaffold(
        navigationSuiteItems = {
            ReplyDestination.entries.forEach {
                item(
                    selected = it == selectedDestination,
                    onClick = { /*TODO update selection*/ },
                    icon = {
                        Icon(
                            imageVector = it.icon,
                            contentDescription = stringResource(it.labelRes)
                        )
                    },
                    label = {
                        Text(text = stringResource(it.labelRes))
                    },
                )
            }
        }
    ) {
        content()
    }
}

El argumento navigationSuiteItems es un bloque que te permite agregar elementos con la función item(), similar a agregar elementos en una LazyColumn. Dentro de la expresión lambda final, este código llama al elemento content() que se pasa como argumento a ReplyNavigationWrapperUI().

Ejecuta la app en el emulador e intenta cambiar los tamaños entre teléfono, plegable y tablet. Verás que la barra de navegación cambia a un riel de navegación y viceversa.

En ventanas muy anchas, como en una tablet en orientación horizontal, es posible que quieras mostrar el panel lateral de navegación permanente. NavigationSuiteScaffold admite mostrar un panel lateral permanente, aunque no se muestra en ninguno de los valores actuales de WindowWidthSizeClass. Sin embargo, puedes hacerlo con un pequeño cambio.

  1. Agrega el siguiente código justo antes de la llamada a NavigationSuiteScaffold:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    val windowSize = with(LocalDensity.current) {
        currentWindowSize().toSize().toDpSize()
    }
    val layoutType = if (windowSize.width >= 1200.dp) {
        NavigationSuiteType.NavigationDrawer
    } else {
        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
            currentWindowAdaptiveInfo()
        )
    }

    NavigationSuiteScaffold(
        layoutType = layoutType,
        ...
    ) {
        content()
    }
}

Este código primero obtiene el tamaño de la ventana y lo convierte en unidades DP con currentWindowSize() y LocalDensity.current. Luego, compara el ancho de la ventana para decidir el tipo de diseño de la IU de navegación. Si el ancho de la ventana es de al menos 1200.dp, usa NavigationSuiteType.NavigationDrawer. De lo contrario, recurre al cálculo predeterminado.

Cuando vuelvas a ejecutar la app en tu emulador de tamaño variable y pruebes diferentes tipos, observa que cada vez que cambia la configuración de la pantalla o despliegas un dispositivo plegable, la navegación cambia al tipo adecuado para ese tamaño.

Se muestran los cambios de adaptabilidad para diferentes tamaños de dispositivos.

¡Felicitaciones! Aprendiste sobre diferentes tipos de navegación para admitir diferentes tipos de tamaños de ventana y estados.

En la siguiente sección, explorarás cómo aprovechar el área restante de la pantalla en lugar de estirar el mismo elemento de la lista de borde a borde.

5. Uso del espacio de la pantalla

No importa si ejecutas la app en una tablet pequeña, un dispositivo desplegado o una tablet grande, la pantalla se estirará para llenar el espacio restante. Asegúrate de que puedas aprovechar ese espacio en pantalla para mostrar más información, como esta app, que muestra el correo electrónico y las conversaciones a los usuarios en la misma página.

Material 3 define tres diseños canónicos y cada uno tiene configuraciones para clases de tamaño de ventana compacta, mediana y expandida. El diseño canónico de Detalles de lista es perfecto para este caso de uso y está disponible en Compose como ListDetailPaneScaffold.

  1. Para obtener este componente, agrega las siguientes dependencias y realiza una sincronización de Gradle:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.layout)
    implementation(libs.androidx.material3.adaptive.navigation)
}
  1. Busca la función de componibilidad ReplyAppContent() en ReplyApp.kt, que por el momento solo muestra el panel de lista llamando a ReplyListPane(). Reemplaza esta implementación por ListDetailPaneScaffold insertando el siguiente código. Dado que esta es una API experimental, también agregarás la anotación @OptIn en la función ReplyAppContent():

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            ReplyListPane(replyHomeUIState, onEmailClick)
        },
        detailPane = {
            ReplyDetailPane(replyHomeUIState.emails.first())
        }
    )
}

Este código primero crea un navegador con rememberListDetailPaneNavigator(). El navegador ofrece cierto control sobre qué panel se muestra y qué contenido debe representarse en ese panel, lo que se demostrará más adelante.

ListDetailPaneScaffold mostrará dos paneles cuando se expanda la clase de tamaño de ancho de ventana. De lo contrario, mostrará un panel o el otro según los valores proporcionados para dos parámetros: la directiva de Scaffold y el valor de Scaffold. Para obtener el comportamiento predeterminado, este código usa la directiva de andamiaje y el valor de andamiaje que proporciona el navegador.

Los parámetros obligatorios restantes son lambdas componibles para los paneles. ReplyListPane() y ReplyDetailPane() (que se encuentran en ReplyListContent.kt) se usan para completar los roles de los paneles de lista y de detalles, respectivamente. ReplyDetailPane() espera un argumento de correo electrónico, así que, por ahora, este código usa el primer correo electrónico de la lista de correos electrónicos en ReplyHomeUIState.

Ejecuta la app y cambia la vista del emulador a plegable o tablet (es posible que también debas cambiar la orientación) para ver el diseño de los dos paneles. ¡Esto ya se ve mucho mejor que antes!

Ahora, analicemos parte del comportamiento deseado de esta pantalla. Cuando el usuario presiona un correo electrónico en el panel de lista, este debería aparecer en el panel de detalles junto con todas las respuestas. Actualmente, la app no lleva un registro de qué correo electrónico se seleccionó, y presionar un elemento no hace nada. El mejor lugar para guardar esta información es con el resto del estado de la IU en ReplyHomeUIState.

  1. Abre ReplyHomeViewModel.kt y busca la clase de datos ReplyHomeUIState. Agrega una propiedad para el correo electrónico seleccionado, con un valor predeterminado de null:

ReplyHomeViewModel.kt

data class ReplyHomeUIState(
    val emails : List<Email> = emptyList(),
    val selectedEmail: Email? = null,
    val loading: Boolean = false,
    val error: String? = null
)
  1. En el mismo archivo, ReplyHomeViewModel tiene una función setSelectedEmail() a la que se llama cuando el usuario presiona un elemento de la lista. Modifica esta función para copiar el estado de la IU y registrar el correo electrónico seleccionado:

ReplyHomeViewModel.kt

fun setSelectedEmail(email: Email) {
    _uiState.update {
        it.copy(selectedEmail = email)
    }
}

Algo que se debe tener en cuenta es lo que sucede antes de que el usuario presione cualquier elemento y el correo electrónico seleccionado sea null. ¿Qué debería aparecer en el panel de detalles? Existen varias formas de manejar este caso, como mostrar el primer elemento de la lista de forma predeterminada.

  1. En el mismo archivo, modifica la función observeEmails(). Cuando se cargue la lista de correos electrónicos, si el estado anterior de la IU no tenía un correo electrónico seleccionado, configúralo en el primer elemento:

ReplyHomeViewModel.kt

private fun observeEmails() {
    viewModelScope.launch {
        emailsRepository.getAllEmails()
            .catch { ex ->
                _uiState.value = ReplyHomeUIState(error = ex.message)
            }
            .collect { emails ->
                val currentSelection = _uiState.value.selectedEmail
                _uiState.value = ReplyHomeUIState(
                    emails = emails,
                    selectedEmail = currentSelection ?: emails.first()
                )
            }
    }
}
  1. Regresa a ReplyApp.kt y usa el correo electrónico seleccionado, si está disponible, para propagar el contenido del panel de detalles:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    detailPane = {
        if (replyHomeUIState.selectedEmail != null) {
            ReplyDetailPane(replyHomeUIState.selectedEmail)
        }
    }
)

Vuelve a ejecutar la app y cambia el emulador al tamaño de tablet. Observa que presionar un elemento de la lista actualiza el contenido del panel de detalles.

Esto funciona muy bien cuando ambos paneles están visibles, pero cuando la ventana solo tiene espacio para mostrar un panel, parece que no sucede nada cuando presionas un elemento. Intenta cambiar la vista del emulador a un teléfono o dispositivo plegable en orientación vertical, y observa que solo se ve el panel de lista incluso después de presionar un elemento. Esto se debe a que, aunque el correo electrónico seleccionado está actualizado, ListDetailPaneScaffold se mantiene enfocado en el panel de lista en estos parámetros de configuración.

  1. Para solucionar este problema, inserta el siguiente código como la expresión lambda que se pasa a ReplyListPane:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    listPane = {
        ReplyListPane(
            replyHomeUIState = replyHomeUIState,
            onEmailClick = { email ->
                onEmailClick(email)
                navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
            }
        )
    },
    // ...
)

Esta lambda usa el navegador que se creó antes para agregar un comportamiento adicional cuando se hace clic en un elemento. Llama a la expresión lambda original que se pasó a esta función y, luego, también llama a navigator.navigateTo(), que especifica qué panel se debe mostrar. Cada panel del andamiaje tiene un rol asociado y para el panel de detalles es ListDetailPaneScaffoldRole.Detail. En ventanas más pequeñas, mostrará la apariencia de que la app ha navegado hacia adelante.

La app también necesita controlar lo que sucede cuando el usuario presiona el botón Atrás desde el panel de detalles, y este comportamiento será diferente dependiendo de si hay uno o dos paneles visibles.

  1. Agrega el siguiente código para admitir la navegación hacia atrás.

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    BackHandler(navigator.canNavigateBack()) {
        navigator.navigateBack()
    }

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            AnimatedPane {
                ReplyListPane(
                    replyHomeUIState = replyHomeUIState,
                    onEmailClick = { email ->
                        onEmailClick(email)
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
                    }
                )
            }
        },
        detailPane = {
            AnimatedPane {
                if (replyHomeUIState.selectedEmail != null) {
                    ReplyDetailPane(replyHomeUIState.selectedEmail)
                }
            }
        }
    )
}

El navegador conoce el estado completo de ListDetailPaneScaffold, si es posible hacer la navegación hacia atrás y qué hacer en todas estas situaciones. Este código crea un BackHandler que se habilita siempre que el navegador puede navegar hacia atrás y, dentro de la lambda, llama a navigateBack(). Además, para que la transición entre paneles sea mucho más fluida, cada panel se une en un elemento AnimatedPane() componible.

Vuelve a ejecutar la app en un emulador de tamaño variable para todos los diferentes tipos de dispositivos y observa que cada vez que cambia la configuración de la pantalla o despliegas un dispositivo plegable, la navegación y el contenido de la pantalla cambian de forma dinámica en respuesta a los cambios de estado del dispositivo. También intenta presionar los correos electrónicos en el panel de lista y observa cómo se comporta el diseño en diferentes pantallas mostrando ambos paneles uno al lado del otro o animando entre ellos de forma fluida.

Se muestran los cambios de adaptabilidad para diferentes tamaños de dispositivos.

Felicitaciones. Lograste que tu app se adapte correctamente a todo tipo de estados y tamaños de dispositivos. Puedes ejecutar la app en dispositivos plegables, tablets y otros dispositivos móviles.

6. Felicitaciones

¡Felicitaciones! Completaste correctamente este codelab y aprendiste a hacer apps adaptables con Jetpack Compose.

Aprendiste a verificar el tamaño y el estado de plegado de un dispositivo, y a actualizar la IU, la navegación y otras funciones de tu app adecuadamente. También aprendiste cómo la adaptabilidad mejora la accesibilidad y la experiencia del usuario.

¿Qué sigue?

Consulta los otros codelabs sobre la ruta de aprendizaje de Compose.

Apps de ejemplo

  • Las muestras de Compose son una colección de muchas apps que incorporan las prácticas recomendadas que se explican en codelabs.

Documentos de referencia