Cómo brindar compatibilidad con diferentes tamaños de pantalla

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

Para admitir tantos tamaños de pantalla como sea posible, los diseños de tu app deben ser 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 se adapte a teléfonos, tablets, plegables, dispositivos ChromeOS, con orientación vertical y horizontal, y configuraciones que pueden cambiar de tamaño, como el modo multiventana.

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 por completo un diseño por otro para que tu app pueda adaptarse mejor a los diferentes tamaños de pantalla (diseño adaptable).

Jetpack Compose es un kit de herramientas declarativas de IU, y es ideal para implementar y crear diseños que cambien dinámicamente para renderizar el contenido de manera diferente en una variedad de tamaños de pantalla.

Realiza cambios en diseños grandes para elementos componibles explícitos a nivel de la pantalla

Cuando usas Compose para diseñar una aplicación completa, los elementos componibles a nivel de la app y a nivel de la pantalla ocupan todo el espacio que tiene tu app para renderizar. En este nivel de tu diseño, podría ser útil cambiar el diseño general de una pantalla para aprovechar las de mayor tamaño.

Evita usar valores físicos de hardware 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 con el que puede trabajar tu IU.

Un diagrama en el que se muestran varios factores de forma de dispositivos diferentes, incluidos un teléfono, un dispositivo plegable, una tablet y una laptop.
Figura 1: Factores de forma de teléfonos, 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. 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 cómo mostrar 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 cómo usar WindowManager en una app de Compose, consulta la muestra de JetNews.

Si sigues este enfoque, tu app podrá adaptarse con más facilidad, ya que se comportará correctamente en todas las situaciones anteriores. Hacer que tus diseños se adapten al espacio de pantalla disponible también reduce la cantidad de manejo especial para plataformas compatibles, como ChromeOS, y factores de forma, como tablets y dispositivos plegables.

Una vez que observes el espacio relevante disponible para tu app, será útil convertir el tamaño sin procesar en una clase de tamaño significativo, como se describe en Clases de tamaño de ventana. De esta manera, se agrupan los tamaños en buckets de tamaño estándar, que son puntos de interrupción diseñados para equilibrar la simplicidad con la flexibilidad y así optimizar la app en la mayoría de los casos únicos. Estas clases de tamaño hacen referencia a la ventana general de tu app, así que úsalas para tomar decisiones de diseño que afecten el diseño general de la pantalla. Puedes pasar estas clases de tamaño como estado o puedes realizar una lógica adicional para crear un estado derivado y pasar a elementos componibles anidados.

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@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,
        /* ... */
    )
}

Este enfoque en capas limita la lógica del tamaño de la pantalla a una sola ubicación, en lugar de dispersarla por toda la app en muchos lugares que deben mantenerse sincronizados. Esta única ubicación produce un estado, que se puede pasar de forma explícita a otros elementos componibles, como lo harías con cualquier otro estado de la app. Este paso de estado de forma explícita simplifica los elementos componibles individuales, ya que serán funciones de componibilidad normales que toman la clase de tamaño o una 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 supone que siempre se colocará en una ubicación determinada con un tamaño específico, será más difícil volver a usarlo en otro lugar de una ubicación diferente o con una cantidad de espacio diferente disponible. Esto también significa que los elementos componibles individuales y reutilizables deben evitar depender implícitamente de la información de tamaño "global".

Considera el siguiente ejemplo: Imagina un elemento componible anidado que implementa un diseño de lista-detalles, que puede mostrar uno o dos paneles en paralelo.

Captura de pantalla de una app que muestra dos paneles, uno al lado del otro.
Figura 2: Captura de pantalla de una app que muestra un diseño típico de lista-detalles (1 es el área de la lista y 2, el área de detalles).

Queremos que esta decisión forme parte del diseño general de la app, por lo que la pasamos desde un elemento componible a nivel de la pantalla, como vimos con anterioridad:

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

¿Y si, en cambio, queremos que un elemento componible cambie su diseño de forma independiente según el espacio disponible? Por ejemplo, una tarjeta que quiere mostrar detalles adicionales si el espacio lo permite. Queremos realizar alguna lógica según el tamaño disponible, pero ¿en qué tamaño específicamente?

Ejemplos de dos tarjetas diferentes.
Figura 3: Tarjeta estrecha que muestra solo un ícono y un título, y una más amplia con el ícono, el título y la descripción breve.

Como vimos antes, debemos evitar intentar usar el tamaño real de la pantalla del dispositivo. Esto no será preciso en varias pantallas ni si la app no está en pantalla completa.

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

Por lo tanto, debemos usar el ancho por el que el elemento componible realmente se renderiza. Existen dos opciones para obtener ese ancho:

Si quieres cambiar dónde o cómo se muestra el contenido, puedes usar una colección de modificadores o un diseño personalizado para que el diseño sea responsivo. Esto podría ser tan simple como que algunos elementos secundarios llenen todo el espacio disponible o colocar elementos secundarios con varias columnas si hay suficiente espacio.

Si deseas cambiar qué es lo que muestras, puedes usar BoxWithConstraints como una alternativa más potente. Ese elemento proporciona restricciones de medición que puedes usar para llamar a diferentes elementos componibles según el espacio disponible. Sin embargo, esto conlleva algunos gastos, ya que BoxWithConstraints aplaza la composición hasta la fase de diseño, cuando se conocen estas restricciones, lo que genera 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

Cuando aproveches el espacio adicional de la pantalla, es posible que, en una de mayor tamaño, tengas más lugar disponible para mostrar más contenido al usuario que en una pequeña. Cuando implementas un elemento componible con este comportamiento, puede ser tentador ser eficiente y cargar los datos como un efecto secundario del tamaño actual.

Sin embargo, esto va en contra de los principios del flujo unidireccional de datos, en el que los datos se pueden elevar y proporcionar a los elementos componibles para que se rendericen de forma adecuada. Se deben proporcionar suficientes datos al elemento componible de modo que este siempre tenga lo que necesita para mostrarse en cualquier tamaño, incluso si una parte de los datos no se usa siempre.

@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 siempre pasamos el elemento description al Card. Aunque description solo se usa cuando el ancho permite que se muestre, Card siempre lo requiere, sin importar el ancho disponible.

Pasar datos hace que los diseños adaptables sean más sencillos, ya que los hace menos con estado y evita que se produzcan efectos secundarios cuando se cambia entre tamaños (lo que puede ocurrir debido al cambio de tamaño de la ventana, la orientación o el plegado y desplegado de un dispositivo).

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, podemos preservar el estado del usuario a medida que cambia el tamaño del diseño. Por ejemplo, podemos elevar una marca booleana showMore para que el estado del usuario se conserve cuando los cambios de tamaño hagan que el diseño cambie entre ocultar y mostrar la descripción:

@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

Si deseas obtener más información sobre diseños personalizados en Compose, consulta los siguientes recursos adicionales.

Apps de ejemplo

  • Los diseños canónicos de pantalla grande son un repositorio de patrones de diseño comprobados que proporcionan una experiencia del usuario óptima en dispositivos con pantallas grandes.
  • JetNews muestra cómo diseñar una app que adapte su IU para aprovechar el espacio disponible
  • Responde es un ejemplo adaptable para brindar compatibilidad con dispositivos móviles, tablets y plegables.
  • Now in Android es una app que usa diseños adaptables para admitir diferentes tamaños de pantalla.

Videos