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 también la forma en la que 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 brindar la mejor experiencia en todos los tamaños de ventana

Requisitos

  • La versión estable más reciente de Android Studio
  • Un dispositivo virtual redimensionable de Android 13
  • Conocimientos sobre Kotlin
  • Conocimientos básicos sobre Compose (como la anotación @Composable)
  • Conocimientos básicos sobre diseños de Compose (p. ej., Row y Column)
  • Conocimientos básicos sobre modificadores (p. ej., Modifier.padding())

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 conoces Compose, antes de completar este codelab, considera realizar el codelab de los conceptos básicos de Jetpack Compose.

Qué compilarás

  • Una app cliente de correo electrónico interactiva llamada Reply, 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 de 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.

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

La actualizarás para aprovechar el espacio de la pantalla, aumentar la facilidad de uso 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 que eso resulte más fácil. También abarca los tipos de pantallas y estados objetivo, incluidos los teléfonos, las tablets, las tablets grandes y los plegables.

Comenzarás por analizar 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 hacerla más adaptable.

Tamaños de ventana

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 puede 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 un ancho compacto, mediano y expandido.

WindowHeightSizeClass para alturas compactas, medianas y expandidas.

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 de ventana.

Estados de plegado

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

Posiciones plegables, plana y semiabierta

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

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

Todos estos son aspectos que se deben tener en cuenta cuando se implementan diseños adaptativos que admiten dispositivos plegables.

Obtén información adaptable

La biblioteca adaptive de Material 3 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 de catálogo de versiones:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0"

[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 posición de mesa.

Puedes probar esta función 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(
                    WindowInsets.safeDrawing.asPaddingValues()
                )
            )
        }
    }
}

Si ejecutas la app ahora, se mostrarán las clases de tamaño de ventana impresas sobre el contenido de la app. Puedes 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 próximos 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 que sea más fácil de usar.

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 abierto o una tablet, sus dedos suelen estar cerca de los costados. Los usuarios deben poder navegar o iniciar una interacción con una app sin requerir posiciones extremas de las manos ni cambiar la ubicación de las manos.

A medida que diseñes tu app y decidas dónde colocar los elementos interactivos de la IU en tu diseño, considera las implicaciones ergonómicas de las diferentes regiones de la pantalla.

  • ¿A qué áreas es cómodo llegar mientras sostienes el dispositivo?
  • ¿A qué áreas se puede llegar solo extendiendo los dedos, lo que puede ser inconveniente?
  • ¿Qué áreas son difíciles de alcanzar o están lejos de 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 más fáciles de alcanzar. La biblioteca adaptable de 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

En el caso de un tamaño de ventana mediano, el riel de navegación es ideal para la accesibilidad, ya que nuestro pulgar se ubica naturalmente a lo largo del costado 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

Implementa la navegación dinámica

Ahora, alternarás entre diferentes tipos de navegación a medida que cambie el estado del dispositivo y su tamaño.

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. Para obtener este componente, actualiza el catálogo de versiones y la secuencia de comandos de compilación de la app, y, luego, realiza una sincronización de Gradle:

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0"

[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 un 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(), de manera similar a cómo se agregan elementos en un LazyColumn. Dentro de la expresión lambda final, este código llama al content() que se pasó como argumento a ReplyNavigationWrapperUI().

Ejecuta la app en el emulador y prueba 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 permanente, aunque no se muestra en ninguno de los valores de WindowWidthSizeClass actuales. Sin embargo, puedes hacer que lo haga 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 de DP con currentWindowSize() y LocalDensity.current, y, 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, se usa NavigationSuiteType.NavigationDrawer. De lo contrario, se 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 y estados de ventana.

En la siguiente sección, explorarás cómo aprovechar el área restante de la pantalla en lugar de estirar un 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, cada uno con configuraciones para clases de tamaño de ventana compactas, medianas y expandidas. El diseño canónico List Detail 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

[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 actualmente solo muestra el panel de lista llamando a ReplyListPane(). Reemplaza esta implementación por ListDetailPaneScaffold insertando el siguiente código. Como 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())
        }
    )
}

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

ListDetailPaneScaffold mostrará dos paneles cuando la clase de tamaño de ancho de ventana se expanda. De lo contrario, mostrará uno u otro panel según los valores proporcionados para dos parámetros: la directiva de andamiaje y el valor de andamiaje. Para obtener el comportamiento predeterminado, este código usa la directiva de andamio y el valor de andamio proporcionado por 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, por lo 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 dos paneles. Ya se ve mucho mejor que antes.

Ahora, abordemos parte del comportamiento deseado de esta pantalla. Cuando el usuario presiona un correo electrónico en el panel de lista, este debe mostrarse en el panel de detalles junto con todas las respuestas. Actualmente, la app no realiza un seguimiento del correo electrónico seleccionado, y presionar un elemento no hace nada. El mejor lugar para conservar 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)
    }
}

Un aspecto 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é se debe mostrar en el panel de detalles? Existen varias formas de controlar 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 como 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. Vuelve a ReplyApp.kt y usa el correo electrónico seleccionado, si está disponible, para completar 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 una tablet. Verás que, cuando presiones un elemento de la lista, se actualizará 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 a un dispositivo plegable en orientación vertical, y observa que solo el panel de la lista está visible incluso después de presionar un elemento. Esto se debe a que, aunque se actualiza el correo electrónico seleccionado, ListDetailPaneScaffold mantiene el enfoque en el panel de la lista en estas configuraciones.

  1. Para corregir eso, 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 creado anteriormente para agregar un comportamiento adicional cuando se hace clic en un elemento. Llamará a la expresión lambda original que se pasó a esta función y, luego, también llamará a navigator.navigateTo() para especificar qué panel se debe mostrar. Cada panel del diseño tiene un rol asociado, y, para el panel de detalles, es ListDetailPaneScaffoldRole.Detail. En ventanas más pequeñas, parecerá que la app navegó hacia adelante.

La app también debe controlar lo que sucede cuando el usuario presiona el botón Atrás desde el panel de detalles, y este comportamiento será diferente según 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 la navegación hacia atrás y qué hacer en todas estas situaciones. Este código crea un BackHandler que se habilita cada vez que el navegador puede volver atrás y, dentro de la expresión lambda, llama a navigateBack(). Además, para que la transición entre los paneles sea mucho más fluida, cada panel se incluye 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 se despliega 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 puedes probar presionar correos electrónicos en el panel de lista y ver cómo se comporta el diseño en diferentes pantallas, mostrando ambos paneles uno al lado del otro o animando entre ellos sin problemas.

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 optimiza la experiencia del usuario.

Próximos pasos

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

Apps de ejemplo

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

Documentos de referencia