Semántica en Compose

Una composición describe la IU de tu app y se produce mediante la ejecución de elementos componibles. La composición es una estructura de árbol con los elementos componibles que describen tu IU.

Junto a la composición, existe un árbol paralelo, llamado árbol semántico. En este árbol, se describe tu IU de una manera alternativa que pueden comprender los servicios de accesibilidad y el framework de pruebas. Los servicios de accesibilidad usan el árbol para describir la app a los usuarios con una necesidad específica. El framework de prueba usa el árbol para interactuar con tu app y realizar aserciones sobre ella. El árbol semántico no contiene la información para dibujar los elementos componibles, pero contiene información sobre el significado semántico de esos elementos.

Una jerarquía de IU típica y su árbol semántico
Figura 1: Una jerarquía de IU típica y su árbol semántico

Si la app se conforma de elementos que admiten composición y modificadores de la biblioteca base y Material de Compose, el árbol semántico se completará y generará automáticamente. Sin embargo, cuando agregas elementos componibles personalizados de bajo nivel, debes proporcionar su semántica de forma manual. También, en algunas situaciones, es posible que el árbol no represente de forma correcta o completa el significado de los elementos en la pantalla. En este caso, puedes adaptar el árbol.

Por ejemplo, ten en cuenta este calendario personalizado que admite composición:

Un calendario personalizado componible con elementos de día seleccionables
Figura 2: Calendario personalizado componible con elementos de día seleccionables.

En este ejemplo, todo el calendario se implementa como un solo elemento que admite composición de bajo nivel si se usa el objeto Layout que admite composición y se dibuja directamente en Canvas. Si no realizas ninguna otra acción, los servicios de accesibilidad no recibirán suficiente información sobre el contenido del elemento componible y la selección del usuario dentro del calendario. Por ejemplo, si un usuario hace clic en el día que contiene 17, el marco de trabajo de accesibilidad solamente recibe la información de descripción de todo el control de calendario. En este caso, el servicio de accesibilidad de TalkBack anunciaría "Calendario" o, solo un poco mejor, "Calendario de abril", y el usuario no se preguntaría qué día se seleccionó. Para que este elemento componible sea más accesible, deberás agregar información semántica de forma manual.

Propiedades semánticas

Todos los nodos en el árbol de IU con algún significado semántico tienen un nodo paralelo en el árbol semántico. El nodo en el árbol semántico incluye esas propiedades que transmiten el significado del elemento correspondiente que admite composición. Por ejemplo, el elemento Text componible contiene una propiedad semántica text, ya que ese es el significado de ese elemento. Un Icon contiene una propiedad contentDescription (si lo establece el desarrollador) que transmite, por medio de texto, cuál es el significado de Icon. Los elementos componibles y los modificadores que se compilan sobre la biblioteca base de Compose ya establecen las propiedades relevantes por ti. De manera opcional, configura o anula las propiedades por tu cuenta con los modificadores semantics y clearAndSetSemantics. Por ejemplo, agrega acciones de accesibilidad personalizadas a un nodo, proporciona una descripción de estado alternativa para un elemento que se puede activar o desactivar o indica que un determinado texto componible debe considerarse como un encabezado.

Para visualizar el árbol semántico, usa la herramienta Inspector de diseño o el método printToLog() dentro de las pruebas. Esto imprime el árbol semántico actual dentro de Logcat.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

El resultado de esta prueba sería el siguiente:

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

Considera cómo las propiedades semánticas transmiten el significado de un elemento componible. Considera un objeto Switch. El usuario lo verá de la siguiente manera:

Figura 3: Un interruptor en su estado "Activado" y "Desactivado"

Para describir el significado de este elemento, puedes decir lo siguiente: "Este es un interruptor, que es un elemento que se puede activar o desactivar en su estado 'Activado'. Puedes hacer clic en él para interactuar con él".

Las propiedades semánticas se usan exactamente para este fin. El nodo semántico de este elemento Switch incluye las siguientes propiedades, como se visualizan con el Inspector de diseño:

El Inspector de diseño muestra las propiedades semánticas de un elemento Switch componible.
Figura 4: El Inspector de diseño muestra las propiedades semánticas de un elemento Switch componible.

El Role indica el tipo de elemento. En StateDescription, se describe cómo se debe hacer referencia al estado "Activado". De forma predeterminada, esta es una versión localizada de la palabra "Activada", pero puede ser más específica (por ejemplo, "Habilitada") según el contexto. El objeto ToggleableState es el estado actual del interruptor. La propiedad OnClick hace referencia al método usado para interactuar con este elemento. Para obtener una lista completa de las propiedades semánticas, consulta el objeto SemanticsProperties. Para obtener una lista completa de las acciones de accesibilidad posibles, consulta el objeto SemanticsActions.

Hacer un seguimiento de las propiedades semánticas de cada elemento que admite composición en la app ofrece muchas posibilidades potentes. Estos son algunos ejemplos:

  • TalkBack usa las propiedades para leer en voz alta lo que se muestra en la pantalla y permite al usuario interactuar con ella de forma fluida. Para el elemento componible Switch, TalkBack podría decir: "Activado; interruptor; presiona dos veces para activar o desactivar". El usuario puede presionar dos veces la pantalla para desactivar el interruptor.
  • El marco de trabajo de prueba usa las propiedades para encontrar nodos, interactuar con ellos y realizar aserciones. Esta podría ser una prueba de ejemplo para el Switch:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Árbol semántico combinado y separado

Como se mencionó antes, es posible que cada elemento que admite composición en el árbol de IU no tenga propiedades semánticas establecidas o que sí las tenga. Cuando un elemento componible no tiene propiedades semánticas establecidas, no se incluye como parte del árbol semántico. De esa manera, el árbol semántico solamente incluye los nodos que, en realidad, tienen significado semántico. Sin embargo, con frecuencia, para transmitir el significado correcto de lo que se muestra en la pantalla, también es útil combinar subárboles determinados de nodos y tratarlos como uno solo. De esa manera, puedes razonar sobre un conjunto de nodos como un todo, en lugar de tratar cada nodo subordinado de manera individual. Como regla general, cada nodo de este árbol representa un elemento enfocable cuando se usan los servicios de accesibilidad.

Un ejemplo de este tipo de elemento es Button. Puedes razonar sobre un botón como un solo elemento, aunque pueda contener varios nodos secundarios:

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

En el árbol semántico, las propiedades de los elementos subordinados del botón se combinan y el botón se presenta como un solo nodo de hoja del árbol:

Se combinó la representación semántica de una sola hoja
Figura 5: Se combinó la representación semántica de una sola hoja.

Los elementos que admiten composición y los modificadores pueden indicar que quieren combinar las propiedades semánticas de sus elementos subordinados mediante una llamada a Modifier.semantics (mergeDescendants = true) {}. Establecer esta propiedad en true indica que se deben combinar las propiedades semánticas. En el ejemplo de Button, el elemento Button que admite composición usa internamente el modificador clickable que incluye este modificador semantics. Por lo tanto, los nodos subordinados del botón se combinan. Lee la documentación de accesibilidad para obtener más información sobre cuándo debes cambiar el comportamiento de combinación en el elemento componible.

Varios modificadores y elementos que admiten composición en las bibliotecas base y material de Compose tienen esta propiedad establecida. Por ejemplo, los modificadores clickable y toggleable combinarán automáticamente sus elementos subordinados. El elemento ListItem que admite composición también los combinará.

Examina los árboles

El árbol semántico es, de hecho, dos árboles diferentes. Hay un árbol semántico combinado que combina los nodos subordinados cuando mergeDescendants se establece en true. También hay un árbol semántico separado, que no aplica la combinación, pero mantiene intactos todos los nodos. Los servicios de accesibilidad usan el árbol separado y aplican sus propios algoritmos de combinación, teniendo en cuenta la propiedad mergeDescendants. El framework de prueba usa el árbol combinado de forma predeterminada.

Puedes inspeccionar ambos árboles con el método printToLog(). De forma predeterminada, y al igual que en los ejemplos anteriores, se registra el árbol combinado. Para imprimir el árbol separado, establece el parámetro useUnmergedTree del comparador onRoot() en true:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

El Inspector de diseño te permite mostrar el árbol semántico combinado y separado. Para ello, selecciona el que prefieras en el filtro de vista:

Opciones de vista del Inspector de diseño, que permiten mostrar el árbol semántico combinado y separado.
Figura 6: Opciones de vista del Inspector de diseño, que permiten mostrar el árbol semántico combinado y separado.

Para cada nodo del árbol, el Inspector de diseño muestra la semántica combinada y la que se establece en ese nodo en el panel de propiedades:

Se combinaron y configuraron propiedades semánticas
Figura 7: Se combinaron y configuraron las propiedades semánticas.

De forma predeterminada, los comparadores en el marco de trabajo de prueba usan el árbol semántico combinado. Es por eso que puedes interactuar con un Button si haces coincidir el texto que se muestra dentro de ella:

composeTestRule.onNodeWithText("Like").performClick()

Para anular este comportamiento, configura el parámetro useUnmergedTree de los comparadores en true, como con el comparador onRoot.

Comportamiento de combinación

Cuando un elemento que admite composición indica que sus elementos subordinados deben combinarse, ¿cómo ocurre exactamente la combinación?

Cada propiedad semántica tiene una estrategia de combinación definida. Por ejemplo, la propiedad ContentDescription agrega todos los valores subordinados de ContentDescription a una lista. Para comprobar la estrategia de combinación de una propiedad semántica, verifica su implementación de mergePolicy en SemanticsProperties.kt. Las propiedades pueden tomar el valor superior o secundario, combinar los valores en una lista o una cadena, no permitir la combinación y arrojar una excepción en su lugar, o cualquier otra estrategia de combinación personalizada.

Una nota importante es que los elementos subordinados que configuraron mergeDescendants = true por su cuenta no se incluyen en la combinación. Veamos un ejemplo:

Elemento de lista con imagen, algo de texto y un ícono de favoritos
Figura 8: Elemento de lista con imagen, algo de texto y un ícono de favoritos.

Este es un elemento de la lista en el que se puede hacer clic. Cuando el usuario presiona la fila, la app navega a la página de detalles del artículo, en la que el usuario puede leerlo. Dentro del elemento de lista, hay un botón para agregar a favoritos el artículo, lo que forma un elemento anidado en el que se puede hacer clic, de modo que el botón se muestre por separado en el árbol combinado. Se combina el resto del contenido en la fila:

El árbol combinado incluye varios textos en una lista dentro del nodo Row El árbol separado incluye nodos que no están combinados para cada elemento Text que admite composición.
Figura 9: El árbol combinado incluye varios textos en una lista dentro del nodo Row El árbol separado incluye nodos independientes para cada elemento Text componible.

Adapta el árbol semántico

Como se mencionó antes, puedes anular o borrar ciertas propiedades semánticas o cambiar el comportamiento de combinación del árbol. Esto resulta particularmente relevante cuando creas tus propios componentes personalizados. Sin configurar las propiedades y el comportamiento de combinación correctos, es posible que no se pueda acceder a tu app y que las pruebas se comporten de manera diferente a lo que esperas. Para obtener más información sobre algunos casos de uso comunes en los que debes adaptar el árbol semántico, lee la documentación de accesibilidad. Si deseas obtener más información sobre las pruebas, consulta la guía para pruebas.

Recursos adicionales