Compatibilidad con diferentes tamaños de pantalla

La compatibilidad con diferentes tamaños de pantalla permite que la mayor variedad de dispositivos y la mayor cantidad de usuarios accedan a tu app.

Para admitir tantos tamaños de pantalla como sea posible, ya sean diferentes pantallas de dispositivos o diferentes ventanas de apps en el modo multiventana, diseña los diseños de tu app para que sean responsivos y adaptables. Los diseños responsivos o adaptables proporcionan una experiencia del usuario optimizada sin importar el tamaño de la pantalla, lo que permite que tu app funcione en teléfonos, tablets, dispositivos plegables y ChromeOS, en orientaciones verticales y horizontales, y en configuraciones de pantalla que pueden cambiar de tamaño, como el modo de pantalla dividida y las ventanas de escritorio.

Los diseños responsivos o adaptables cambian en función del espacio de visualización disponible. Los cambios varían desde pequeños ajustes de diseño que ocupan espacio (diseño responsivo) hasta reemplazar un diseño por otro por completo para que tu app se adapte mejor a diferentes tamaños de pantalla (diseño adaptable).

Como kit de herramientas declarativas de IU, Jetpack Compose es ideal para diseñar e implementar diseños que cambian de forma dinámica para renderizar contenido de manera diferente en diferentes tamaños de pantalla.

Realiza cambios en diseños grandes para elementos componibles explícitos a nivel del contenido

Los elementos componibles a nivel de la app y del contenido ocupan todo el espacio de visualización disponible para tu app. Para estos tipos de elementos componibles, podría ser conveniente cambiar el diseño general de tu app en pantallas grandes.

Evita usar valores de hardware físicos para tomar decisiones de diseño. Puede ser tentador tomar decisiones basadas en un valor fijo y tangible (¿el dispositivo es una tablet?, ¿la pantalla física tiene una relación de aspecto determinada?), pero las respuestas a estas preguntas pueden no ser útiles para determinar el espacio disponible para tu IU.

Figura 1: Factores de forma de teléfonos, dispositivos plegables, tablets y laptops

En las tablets, es posible que una app se ejecute en el modo multiventana, lo que significa que esta podría dividir la pantalla con otra app. En el modo de ventanas de escritorio o en ChromeOS, es posible que una app se ejecute en una ventana que puede cambiar de tamaño. Incluso puede haber más de una pantalla física, como sucede con un dispositivo plegable. En todos estos casos, el tamaño físico de la pantalla no es relevante para decidir la forma en la que se muestra el contenido.

En cambio, debes tomar decisiones según la parte real de la pantalla que se asigna a tu app, como las métricas de ventana actuales que proporciona la biblioteca WindowManager de Jetpack. Para ver un ejemplo de cómo usar WindowManager en una app de Compose, consulta la muestra de JetNews.

La creación de diseños adaptables al espacio de pantalla disponible también reduce la cantidad de manejo especial necesario para admitir plataformas como ChromeOS y factores de forma, como tablets y dispositivos plegables.

Cuando hayas determinado las métricas del espacio disponible para tu app, convierte el tamaño sin procesar en una clase de tamaño de ventana, como se describe en Cómo usar clases de tamaño de ventana. Las clases de tamaño de ventana son puntos de interrupción diseñados para equilibrar la simplicidad de la lógica de la app con la flexibilidad para optimizar la app para la mayoría de los tamaños de pantalla. Las clases de tamaño de ventana se refieren a la ventana general de tu app. Por lo tanto, úsalas para tomar decisiones sobre el diseño que afecten el diseño general de la app. Puedes pasar las clases de tamaño de ventana como un estado o bien puedes agregar una lógica adicional para crear un estado derivado y, de esta manera, pasar los elementos componibles anidados.

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

    // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Un enfoque en capas limita la lógica del tamaño de la pantalla a una sola ubicación en lugar de dispersarla en tu app en muchos lugares que deben mantenerse sincronizados. Una ubicación única produce un estado, que puede pasarse de manera explícita a otros elementos componibles, como lo harías con cualquier otro estado de app. Este paso de estado de forma explícita simplifica los elementos componibles individuales, ya que los elementos componibles toman la clase de tamaño de ventana o la configuración especificada junto con otros datos.

Los elementos componibles, flexibles y anidados son reutilizables

Los elementos componibles son más reutilizables cuando se pueden ubicar en varias ubicaciones. Si un elemento componible debe colocarse en una ubicación específica con un tamaño específico, es probable que no se pueda volver a usar en otros contextos. Esto también significa que los elementos componibles individuales y reutilizables deben evitar depender implícitamente de la información de tamaño de pantalla global.

Imagina un elemento componible anidado que implemente un diseño de lista de detalles, que puede mostrar uno o dos paneles en paralelo:

Una app que muestra dos paneles en paralelo.
Figura 2: App que muestra un diseño típico de lista-detalles: 1 es el área de la lista y 2 es el área de los detalles.

La decisión de lista-detalles debe ser parte del diseño general de la app, por lo que la decisión se pasa desde un elemento componible a nivel de contenido:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

¿Qué sucede si, en cambio, quieres que un elemento componible cambie su diseño de forma independiente según el espacio de visualización disponible, por ejemplo, una tarjeta que muestra detalles adicionales si el espacio lo permite? Quieres ejecutar una lógica según el tamaño de visualización disponible, pero ¿en qué tamaño específicamente?

Figura 3: Tarjeta angosta que muestra solo un ícono y un título, y una más amplia, con un ícono, un título y una descripción breve.

Evita usar el tamaño real de la pantalla del dispositivo. ya que no se mostrará como corresponde en diferentes tipos de pantallas ni tampoco si la app no está en pantalla completa.

Dado que el elemento componible no es un elemento de este tipo a nivel del contenido, no uses las métricas de ventana actuales directamente. Si el componente se coloca con padding (como las inserciones), o bien si la app incluye componentes como rieles de navegación o barras de la app, la cantidad de espacio de visualización disponible para el elemento componible puede diferir de forma significativa del espacio general que está disponible para la app.

Usa el ancho que se le asigna al elemento componible para renderizarse. Tienes dos opciones para obtener ese ancho:

  • Si quieres cambiar dónde o cómo se muestra el contenido, usa una colección de modificadores o un diseño personalizado para hacer que el diseño sea responsivo. Esto podría ser tan sencillo como que un elemento secundario ocupe todo el espacio disponible o que se coloquen elementos secundarios con varias columnas si hay suficiente espacio.

  • Si deseas cambiar qué es lo que muestras, usa BoxWithConstraints como una alternativa más potente. BoxWithConstraints proporciona restricciones de medición que puedes usar para llamar a distintos elementos componibles según el espacio de visualización disponible. Sin embargo, esto conlleva algunos gastos, ya que BoxWithConstraints difiere la composición hasta la fase de diseño, cuando se conocen estas restricciones, lo que genera que se realice más trabajo durante el diseño.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

Garantiza la disponibilidad de todos los datos para diferentes tamaños de pantalla

Cuando implementas un elemento componible que aprovecha el espacio de visualización adicional, puede ser tentador ser eficiente y cargar los datos como un efecto secundario del tamaño de visualización actual.

Sin embargo, esto va en contra del principio de flujo de datos unidireccional, en el que los datos se pueden elevar y proporcionar a los elementos componibles para que se rendericen de forma correcta. Se deben proporcionar suficientes datos al elemento componible para que este siempre tenga suficiente contenido para cualquier tamaño de pantalla, incluso si alguna parte del contenido no se usa en todas las instancias.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

Si compilas a partir del ejemplo Card, ten en cuenta que el description siempre se pasa al Card. Aunque description solo se usa cuando el ancho permite que se muestre, Card siempre lo requiere, sin importar el ancho disponible.description

Pasar siempre contenido suficiente simplifica los diseños adaptables, ya que los hace con menos estados y evita que haya efectos secundarios cuando se cambia de tamaño como resultado de un cambio en el tamaño de la ventana, la orientación, o bien si se muestra en un dispositivo plegable.

Este principio también permite preservar el estado en cambios de diseño. Si se eleva información que tal vez no se use en todos los tamaños de pantalla, puedes preservar el estado de la app a medida que cambia el tamaño del diseño. Por ejemplo, puedes elevar una marca booleana showMore para que el estado de la app se conserve cuando el cambio de tamaño de la pantalla haga que el diseño cambie entre ocultar y mostrar el contenido:

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

Más información

Para obtener más información sobre los diseños adaptables en Compose, consulta los siguientes recursos:

Apps de ejemplo

  • CanonicalLayouts es un repositorio de patrones de diseño comprobados que proporcionan una experiencia del usuario óptima en pantallas grandes.
  • JetNews muestra cómo diseñar una app que se adapta a la IU para aprovechar el espacio de visualización disponible.
  • Reply es un ejemplo adaptable para admitir dispositivos móviles, tablets y plegables.
  • Ahora en Android es una app que usa diseños adaptables para admitir diferentes tamaños de pantalla.

Videos