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

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

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 y 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, orientaciones vertical y horizontal, y a configuraciones que puedan 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 van 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 diferentes tamaños de pantalla (diseño adaptable).

Jetpack Compose es un kit de herramientas declarativas de IU ideal para implementar y diseñar diseños que cambian de manera dinámica para renderizar 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 la IU.

Un diagrama que muestra varios factores de forma de dispositivos, incluidos un teléfono, un dispositivo plegable, una tablet y una laptop.
Figura 1: Factores de forma de teléfono, dispositivo plegable, tablet y laptop

En las tablets, es posible que una app se ejecute en el modo multiventana, lo que significa que la app podría estar dividiendo 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 la forma en que se 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 control especial para admitir plataformas 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. Esto agrupa 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 optimizar tu app en la mayoría de los casos únicos. Estas clases de tamaño se refieren a la ventana general de tu app, por lo que debes usarlas para tomar decisiones de diseño que afecten el diseño general de la pantalla. Puedes pasar estas clases de tamaño como estados o puedes aplicar una lógica adicional para crear un estado derivado y pasarlos 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 la app en muchos lugares que deben mantenerse sincronizados. Esta única ubicación produce un estado, que se puede pasar 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 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 lista; 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(/* ... */)
    }
}

¿Qué sucede 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 desea mostrar detalles adicionales si el espacio lo permite. Queremos realizar una lógica según el tamaño disponible, pero ¿en qué tamaño específicamente?

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

Como vimos antes, debemos evitar intentar usar el tamaño de la pantalla real del dispositivo. Esto no será preciso en varias 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 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 deseas 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 componible proporciona restricciones de medición que puedes usar para llamar a diferentes elementos componibles según el espacio disponible. Sin embargo, esto genera algunos gastos, ya que BoxWithConstraints aplaza la composición hasta la fase de diseño, cuando se conocen estas restricciones, lo que hace 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

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 para que este siempre tenga lo que necesita mostrar en cualquier tamaño, incluso si es posible que parte de los datos no se use 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 simplifica los diseños adaptables, ya que los hace menos con estado, y evita que se produzcan efectos secundarios cuando se cambia de un tamaño a otro (lo que puede ocurrir debido a un cambio de tamaño de la ventana, un cambio de 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 se conserve el estado del usuario 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 pantalla grande.
  • JetNews muestra cómo diseñar una app que se adapta su IU para aprovechar el espacio disponible.
  • Responde es una muestra adaptable para admitir dispositivos móviles, tablets y dispositivos plegables.
  • Now in Android es una app que usa diseños adaptables para admitir diferentes tamaños de pantalla.

Videos