Compilación de diseños adaptables

La IU de tu app debería ser responsiva para los diferentes tamaños de pantalla, 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 y llenar espacio hasta cambios completos de diseño para aprovechar espacio adicional.

Se recomienda el uso de Jetpack Compose, el kit de herramientas declarativas de IU, para implementar y crear diseños adaptables a fin de renderizar contenido en función de los diferentes tamaños. Este documento contiene algunos lineamientos sobre el modo en que puedes usar Compose para que tu IU sea responsiva.

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 que muestra varios factores de forma de dispositivos: un teléfono, un dispositivo plegable, una tablet y una 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 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 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. Este grupo asocia los tamaños en buckets de tamaño estándar, que son puntos de interrupción diseñados a fin de equilibrar la simplicidad con la flexibilidad con el objetivo de optimizar la app para la mayoría de los casos únicos. Estas clases de tamaño 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 pantalla. Puedes pasar estas clases de tamaño 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.

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}
@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact

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

En lugar de dispersar la lógica del tamaño de la pantalla de la app por muchos lugares que deben mantenerse sincronizados, este enfoque en capas la limita a una sola ubicación. Esta 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 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 lo colocará en una ubicación determinada y con un tamaño específico, será más difícil volver a usarlo en otro lugar 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".

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

Captura de pantalla de una app que muestra dos paneles en paralelo

Figura 1: Captura de pantalla de una app que muestra un diseño típico de lista y detalles. 1 es el área de la lista y 2 es 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 para mostrar detalles adicionales si el espacio lo permite. Queremos ejecutar una lógica según el tamaño disponible, pero ¿en qué tamaño específicamente?

Ejemplos de dos tarjetas diferentes: una 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

Como vimos antes, debemos evitar el uso del tamaño real de la pantalla del dispositivo, ya que no se mostrará como corresponde 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 quieres cambiar dónde o cómo se muestra el contenido, puedes usar una colección de modificadores o un diseño personalizado para hacer 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 distintos elementos componibles según el espacio 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 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 infringe los principios del flujo de datos unidireccional, en el que los datos se pueden elevar y proporcionar a los elementos componibles a fin de que se procesen de forma correcta. Se deben brindar suficientes datos al elemento componible para que este siempre tenga lo necesario para mostrar contenido en cualquier tamaño, incluso si alguna parte de los datos 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 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 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, 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 el cambio de tamaño haga que el diseño pase de ocultar y mostrar la descripción como se muestra a continuació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