Cómo hacer que las apps sean adaptables y accesibles con Jetpack Compose

1. Introducción

En este codelab, aprenderás a compilar apps adaptables para teléfonos, tablets y dispositivos plegables, y a mantener la accesibilidad en su núcleo con Jetpack Compose. También aprenderás las prácticas recomendadas para usar temas y componentes de Material 3.

Antes de comenzar, es importante comprender qué queremos decir con "adaptabilidad" y "accesibilidad".

Adaptabilidad

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 hasta llenar el espacio, elegir los diferentes estilos de navegación y cambiar los diseños por completo para aprovechar espacio adicional.

Accesibilidad

Todos deberían poder usar las apps para Android, incluidas las personas con necesidades de accesibilidad. Las apps deben adaptarse a diferentes situaciones para proporcionar la mejor experiencia del usuario con contrastes de colores, accesibilidad y mucho más.

En este codelab, explorarás cómo usar y pensar en la adaptabilidad y accesibilidad cuando se usa Jetpack Compose. Compilarás una aplicación llamada REPLY que te muestre cómo implementar la adaptabilidad en todo tipo de pantallas. Verás cómo la adaptabilidad y la accesibilidad funcionan en conjunto para brindar a los usuarios una experiencia óptima.

Qué aprenderá

  • Cómo diseñar tu app para que se oriente a todos los tamaños de pantalla con Jetpack Compose
  • Cómo orientar tu app para diferentes dispositivos plegables
  • Cómo usar diferentes tipos de navegación para mejorar la accesibilidad y la accesibilidad
  • Cómo diseñar esquemas de colores de Material 3 y temas dinámicos para brindar una experiencia de accesibilidad óptima.
  • Cómo usar los componentes de Material 3 a fin de brindar la mejor experiencia para cada tamaño de pantalla

Requisitos

  • Android Studio Bumblebee.
  • 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).

Si no estás familiarizado con Compose, considera realizar el codelab de los principios básicos de Jetpack Compose antes de completarlo.

Qué compilarás

  • Una app cliente interactiva de Reply Email que usa las prácticas recomendadas para Material 3, temas dinámicos y diseños adaptables.

Presentación de compatibilidad con varios dispositivos que lograrás en este codelab

2. Prepárate

Para descargar la app de ejemplo, puedes optar por una de las dos opciones siguientes:

o clona el repositorio de GitHub desde la línea de comandos con este comando:

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ReplyAdaptabilityCodelab

En cualquier momento, puedes ejecutar cualquiera de los módulos en Android Studio realizando en la configuración de ejecución de la barra de herramientas.

b059413b0cf9113a.png

Abre el proyecto en Android Studio

  1. En la ventana Welcome to Android Studio, selecciona c01826594f360d94.png Open an Existing Project.
  2. Selecciona la carpeta [Download Location]/ReplyAdaptabilityCodelab (asegúrate de seleccionar el directorio ReplyAdaptabilityCodlab que contiene build.gradle).
  3. Cuando Android Studio haya importado el proyecto, prueba si puedes ejecutar los módulos start y finished.

Explora el código de inicio

El código de inicio contiene cuatro paquetes:

  • MainActivity: Actividad de punto de entrada donde inicias ReplyApp. Realizará los cambios en este archivo.
  • ui: contiene temas, componentes y ReplyApp donde se inicia la IU de redacción. Realizarás cambios en este paquete.
  • util: contiene código auxiliar para el proyecto. No es necesario que edites este paquete.

Este codelab se enfoca en los archivos del paquete reply. En el módulo start, hay varios archivos con los que debes familiarizarte.

Archivos que editarás en ui paquete

  • MainActivity.kt: actividad de Android que será el punto de partida donde iniciarámos ReplyApp y le pasaremos la información necesaria, como el estado de plegado, el tamaño y el diseño.
  • ReplyApp.kt: La estructura principal de la IU de la app se encuentra en el archivo ReplyApp.kt, en el que trabajarás.
  • ReplyAppContent.kt: La implementación de Compose del contenido de la app y los detalles de lista van aquí.

Enfoquémonos primero en MainActivity.kt. Para el módulo start, ya debería tener el código funcionando en su actividad.

MainActicity.kt.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   setContent {
       ReplyTheme {
           val uiState = viewModel.uiState.collectAsState().value
           ReplyApp(uiState)
       }
   }
}

Si ejecutas esta app en cualquier tamaño de dispositivo, verás la misma extensión de pantalla para completar el área máxima sin realizar cambios en los elementos de la IU. Configuración inicial de ReplyApp sin cambios.

Intentemos mejorarlo a fin de aprovechar el espacio de la pantalla, mejorar la experiencia del usuario y mantener la accesibilidad en el centro.

3. Cómo hacer que las apps sean adaptables

En esta sección, se explica qué significa adaptar las apps y qué componentes proporciona Material 3 para que sea más fácil para nosotros.

También hablaremos sobre los tipos de pantallas y estados de orientación, incluidos teléfonos, tablets, tablets de gran tamaño y plegables.

Cómo controlar los tamaños de ventana

Antes de llegar a la app de Reply, exploremos qué tipos de tamaños y dispositivos existen en el mercado para que los usuarios usen nuestras apps.

Tenemos teléfonos celulares de 4 pulgadas a 7 pulgadas. Luego, tenemos tablets, que van desde tablets más pequeñas hasta tablets casi del tamaño de las laptops.

Dividamos primero estos tamaños diferentes en 3 categorías según WIndowSizeClass. Las categorías se eligieron específicamente para equilibrar la simplicidad del diseño y la flexibilidad con el propósito de optimizar la app en casos únicos. La clase de tamaño de ventana siempre se determina por el espacio de pantalla disponible para la app, que puede no ser la pantalla física completa para varias tareas o diferentes tareas.

Distribución del tamaño del dispositivo según WindowSizeClass

WindowStateUtils**.kt**

enum class WindowSize { COMPACT, MEDIUM, EXPANDED }

fun getWindowSizeClass(windowDpSize: DpSize): WindowSize = when {
   windowDpSize.width < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative")
   windowDpSize.width < 600.dp -> WindowSize.COMPACT
   windowDpSize.width < 840.dp -> WindowSize.MEDIUM
   else -> WindowSize.EXPANDED
}

WindowStateUtils.kt proporciona rememberWindowSizeClass(),, que nos ayuda a obtener un estado recordado de Compose para que, cuando haya cambios de configuración en el tamaño, nuestro árbol de IU se renderice de nuevo según el tamaño nuevo.

WindowStateUtils.kt

fun Activity.rememberWindowSizeClass(): WindowSize {
   // Get the size (in pixels) of the window
   val windowSize = rememberWindowSize()

   // Convert the window size to [Dp]
   val windowDpSize = with(LocalDensity.current) {
       windowSize.toDpSize()
   }

   // Calculate the window size class
   return getWindowSizeClass(windowDpSize)
}

Para comenzar a admitir tamaños adaptables, solo debes agregar rememberWindowSizeClass() al comienzo de nuestra IU de Compose y pasarlo a ReplyApp. Ahora, puedes realizar cambios en MainActivity.kt para que se vea de esta forma.

MainActivity.kt

setContent {
   ReplyTheme(dynamicColor = false, darkTheme = false) {
       val windowSize = rememberWindowSizeClass()
       ReplyApp(windowSize, uiState)
   }
}

Con estos cambios, puede ver que ReplyApp tiene información sobre el tamaño más reciente de la ventana para usar correctamente el espacio.

4. Cómo controlar estados de plegado

También debes asegurarte de que tu app responda a los cambios de estado de plegado y no solo al tamaño de la pantalla. Puede haber muchos estados de plegado, pero comienza con este objetivo para orientarte a algunos casos. Ya están definidos en la clase de utilidad.

WindowStateUtils.kt

/**
* Information about the posture of the device
*/
sealed interface DevicePosture {
   object NormalPosture : DevicePosture

   data class TableTopPosture(
       val hingePosition: Rect
   ) : DevicePosture

   data class BookPosture(
       val hingePosition: Rect
   ) : DevicePosture
}

Asegúrate de que la IU reaccione cuando pasas de una posición plegada a una posición desplegada. También debes tener en cuenta BookPosture y TableTopPosture con la posición de la bisagra, ya que no quieres renderizar texto ni otra información útil en la bisagra.

Veamos la postura de la mitad superior con nuestro ciclo de vida de actividad. Agrega este código en el método onCreate() de la actividad antes de llamar a setContent().

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

    /* Flow of [DevicePosture] that emits every time there is a change in the windowLayoutInfo
    */
   val devicePostureFlow =  WindowInfoTracker.getOrCreate(this).windowLayoutInfo(this)
       .flowWithLifecycle(this.lifecycle)
       .map { layoutInfo ->
           val foldingFeature =
               layoutInfo.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
           when {
               isTableTopPosture(foldingFeature) ->
                   DevicePosture.TableTopPosture(foldingFeature.bounds)
               isBookPosture(foldingFeature) ->
                   DevicePosture.BookPosture(foldingFeature.bounds)
               isSeparating(foldingFeature) ->
                   DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)
               else -> DevicePosture.NormalPosture
           }
       }
       .stateIn(
           scope = lifecycleScope,
           started = SharingStarted.Eagerly,
           initialValue = DevicePosture.NormalPosture
       )

Ahora puedes observar el flujo de posición del dispositivo como un estado de Compose, lo que ayuda a nuestra IU a reaccionar ante los cambios de estado de plegado. Agrega estos cambios a setContent().

MainActivity.kt

setContent {
   ReplyTheme(dynamicColor = false, darkTheme = false) {
       val devicePosture = devicePostureFlow.collectAsState().value
       ReplyApp(windowSize, devicePosture, uiState)
   }
}

La IU de Compose ahora está lista para reaccionar a los cambios de tamaño del dispositivo y el estado de plegado. Puedes continuar desde aquí para diseñar tu IU en diferentes estados. Cuando se producen cambios en el estado de plegado, queremos que nuestra IU reaccione de esta manera.

Adaptación de la IU plegable

5. Navegación dinámica

En la última sección, hiciste que la IU reaccionara a los cambios de tamaño, configuración y plegado. Ahora, debes comprender cómo puedes adaptar la interacción del usuario con los dispositivos cuando atraviesan diferentes estados.

Comencemos con la navegación, ya que es lo primero con lo que interactuarán los usuarios. Ten en cuenta que los usuarios tienen distintos tipos de dispositivos. Observemos algunos componentes de Navigation de Material.

Navegación inferior

La navegación inferior es perfecta para tamaños compactos, ya que naturalmente sostenemos el dispositivo donde nuestro pulgar puede alcanzar fácilmente todos los puntos táctiles de navegación inferior. Puedes usarla siempre que tengas un dispositivo compacto o plegable.

Para un dispositivo de tamaño mediano o la mayoría de los teléfonos celulares en orientación horizontal, el riel de navegación es ideal para navegar y acceder con facilidad, ya que nuestro pulgar se encuentra naturalmente en la parte superior izquierda del dispositivo. También puedes usar el panel lateral de navegación, junto con el riel de navegación, para mostrar más información.

El panel lateral de navegación es una forma sencilla de ver información detallada de las pestañas de navegación y es fácil de acceder cuando utilizas tablets o dispositivos de mayor tamaño. Puedes usar el panel lateral de navegación junto con el riel de navegación, además de la barra de navegación inferior, y usar un panel lateral de navegación permanente para la navegación fija en dispositivos muy anchos.

Ahora, alternemos entre diferentes tipos de navegación a medida que cambie el estado del dispositivo y su tamaño, manteniendo al máximo la interacción y la accesibilidad del usuario.

Agreguemos navegación dinámica a la app. Abre ReplyApp.kt y agrega lo siguiente dentro del elemento que admite composición ReplyApp.

AnswerApp.kt

/**
* This will help us select type of navigation depending on window size and
* fold state of the device.
*/
val navigationType: ReplyNavigationType

when (windowSize) {
   WindowSize.COMPACT -> {
       navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
   }
   WindowSize.MEDIUM -> {
       navigationType = ReplyNavigationType.NAVIGATION_RAIL
   }
   WindowSize.EXPANDED -> {
       navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) {
           ReplyNavigationType.NAVIGATION_RAIL
       } else {
           ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
       }
   }
}

Como el panel lateral de navegación actúa como la IU del contenedor de ReplyAppContent,se une a un panel lateral de navegación permanente o modal según nuestro navigationType, de esta forma ,

AnswerApp.kt

if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
   PermanentNavigationDrawer(drawerContent = {                    NavigationDrawerContent(selectedDestination) }) {
       ReplyAppContent(navigationType, contentType, replyHomeUIState)
   }
} else {
   ModalNavigationDrawer(
       drawerContent = {
           NavigationDrawerContent(
               selectedDestination,
               onDrawerClicked = {
                   scope.launch {
                       drawerState.close()
                   }
               }
           )
       },
       drawerState = drawerState
   ) {
       ReplyAppContent(navigationType, contentType, replyHomeUIState,
           onDrawerClicked = {
               scope.launch {
                   drawerState.open()
               }
           }
       )
   }
}

Ahora tienes un NavigationType dinámico que puede usarse para cambiar la navegación cada vez que haya algún cambio en la configuración. Agreguemos navigationType a ReplyAppContent() para que la navegación sea dinámica.

AnswerApp.kt

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
           ReplyNavigationRail(
               onDrawerClicked = onDrawerClicked
           )
       }
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           // Reply List content

           AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
               ReplyBottomNavigationBar()
           }
       }
   }
}

Vuelve a ejecutar la app para probar la navegación dinámica

Cuando vuelvas a ejecutar la app, verás 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 adaptación para diferentes tamaños de dispositivos.

¡Felicitaciones! Aprendiste sobre diferentes tipos de navegación para admitir diferentes tipos de tamaños de pantalla y estados.

En la próxima sección, explorarás cómo aprovechar cualquier área restante de la pantalla en lugar de estirar el mismo elemento de una lista a otra.

6. Uso del espacio de la pantalla

En la app, puedes ver que la pantalla se estira para rellenar el espacio restante, sin importar si se trata de una tablet pequeña, un dispositivo desplegado o una tablet grande. Asegúrate de aprovechar el espacio de la pantalla para mostrar más información a los usuarios.

Al igual que con navigationType, crearás una contentType que nos ayude a decidir solo entre contenido de la lista o para mostrar una lista y un contenido detallado de manera dinámica en: cambios de estado de la pantalla,

AnswerApp.kt

val contentType: ReplyContentType
when (windowSize) {
   WindowSize.COMPACT -> {
       contentType = ReplyContentType.LIST_ONLY
   }
   WindowSize.MEDIUM -> {
       contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) {
           ReplyContentType.LIST_AND_DETAIL
       } else {
           ReplyContentType.LIST_ONLY
       }
   }
   WindowSize.EXPANDED -> {
       contentType = ReplyContentType.LIST_AND_DETAIL
   }
}

Ahora puedes pasar este tipo de contenido a ReplyAppContent, y, cuando haya cambios de configuración, se adaptará al diseño correcto para ti. También puedes tener en cuenta la posición de la bisagra y la bisagra a fin de decidir el posicionamiento de la lista y el diseño de la bisagra, para evitar que el contenido aparezca en esa posición.

AnswerApp.kt

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           if (contentType == ReplyContentType.LIST_AND_DETAIL) {
               ReplyListAndDetailContent(
                   replyHomeUIState = replyHomeUIState,
                   modifier = Modifier.weight(1f),
               )
           } else {
               ReplyListOnlyContent(replyHomeUIState = replyHomeUIState, modifier = Modifier.weight(1f))
           }
       }
   }
}

Vista final de ReplyApp después de agregar todos los cambios

Vuelve a ejecutar la app para probar la app completamente adaptable

Vuelve a ejecutar la app y observa que cada vez que cambia la configuración de la pantalla o se despliega un dispositivo plegable, el contenido de la pantalla y la navegación cambia dinámicamente en respuesta al estado del dispositivo. Jetpack Compose hace que estos tipos de cambios sean muy fáciles de escribir en un patrón declarativo.

Felicitaciones. Adaptaste correctamente tu app para que se adapte a todo tipo de estados y tamaños de dispositivos. Puedes continuar ejecutando la app en dispositivos plegables, tablets y otros dispositivos móviles.

En las próximas secciones, explorarás cómo estos cambios en la adaptabilidad también nos ayudan a definir la estructura de la accesibilidad.

7. Cómo mejorar la accesibilidad

Accesibilidad

La accesibilidad es la capacidad de navegar o usar un dispositivo sin necesidad de posiciones extremas ni cambiar la posición de las manos para iniciar cualquier interacción con una app.

En la app de Reply, en la sección Navegación dinámica, agregaste varios modos de navegación según el estado de la pantalla. Los componentes de Material, como la barra de navegación inferior, el riel de navegación y el panel lateral de navegación, permiten acceder fácilmente a la navegación según cómo sostienemos los dispositivos de diferentes factores de forma.

Demostración de accesibilidad que muestra el riel de navegación y el panel lateral de navegación para diferentes tamaños de tablet.

También agregamos un factor de forma de lista y detalles que permite a los usuarios alternar fácilmente entre subprocesos y desplazarse por ellos en dispositivos grandes con las manos izquierda y derecha sin cambiar las posiciones.

Contraste de colores

La app de Reply es compatible con temas dinámicos para Android 12 y versiones posteriores, en los que el esquema de colores se genera mediante la selección de fondos de pantalla y otras configuraciones de personalización. Los productos que usan color dinámico cumplen con los requisitos de accesibilidad porque las combinaciones algorítmicas que puede experimentar un usuario final están diseñadas para cumplir con esos estándares.

Para obtener más información, consulta Colores dinámicos.

Esquema de colores de Material 3 para el modo claro y oscuro

Para esta app, también usamos un esquema de colores de Material 3 que ya está diseñado a fin de cumplir con los estándares de accesibilidad para el contraste de color. El sistema de las paletas tonales es central para habilitar el acceso a cualquier esquema de colores de forma predeterminada.

Demostración de contraste de color con temas de Material 3.

La combinación de colores según la tonalidad, en lugar del valor hexadecimal o el matiz, es uno de los sistemas clave que hacen que cualquier resultado de color sea accesible. Siempre puedes crear el esquema de colores completo de Material 3 eligiendo el conjunto adecuado de colores primarios, secundarios y terciarios; y usando el creador de temas de Material, a fin de crear un esquema de colores de Material 3 para las variaciones claras y oscuras. La variación generada ya cumple con los estándares de accesibilidad para el contraste de color.

En Android 11 y versiones anteriores, cuando no están disponibles los temas dinámicos, se recurre a un esquema de color de Material 3 fijo generado con el compilador de temas Material.

Puedes probar nuevos temas de color con el Creador de temas de Material.

Puedes ver el color generado directamente en el archivo ui/theme/Color.kt para verlo en acción.

8. Felicitaciones

¡Felicitaciones! Completaste correctamente este codelab y aprendiste a diseñar apps adaptables y accesibles 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 según corresponda. También aprendiste a aprovechar el esquema de colores y la tipografía de Material 3 para mejorar la accesibilidad y la experiencia del usuario.

Próximos pasos

Consulta los otros codelabs sobre la ruta de Compose.

Apps de ejemplo

  • Las apps de muestra son un conjunto de muchas apps que incorporan las prácticas recomendadas explicadas en codelabs.

Documentos de referencia