Precios accesibles, nuestro aprendizaje desde la perspectiva Android

Cómo mejoramos la accesibilidad de nuestro componente de precio, y cómo nos marcó el camino hacia nuevos saberes para nuestro design system.

Juan Ignacio Unzurrunzaga
Mercado Libre Tech

--

Leer esta historia en inglés.

Intro

Este post surge a raíz de una necesidad de empezar a trabajar en accesibilidad digital para mejorar la experiencia de nuestras personas usuarias. Como bien nos cuenta Guille Paz en su post sobre nuestro trabajo en accesibilidad digital, esta iniciativa surge del análisis realizado por nuestro nuevo equipo de accesibilidad — equipo multidisciplinario formado por personas de UX y de desarrollo — en el afán de derribar las barreras que impiden el uso de los productos de Mercado Libre a través de nuestras diferentes aplicaciones.

Como continuación del post que realizó Martín Di Luzio sobre el aprendizaje al desarrollar precios accesibles en Web, creemos oportuno contar la experiencia que encontramos desde el lado de desarrollo nativo, y brindar contexto a algunas nociones importantes sobre la accesibilidad a nivel desarrollo en Android.

Lo que detectamos

Como parte de las primeras tareas que realizamos al iniciar el recorrido para mejorar la accesibilidad de nuestros productos surge el caso de nuestro componente de Precio. Creemos que este componente es un caso ejemplar para contar cómo trabajamos en nuestro análisis y posterior implementación dado que representa un elemento transversal a muchas de las experiencias de la persona usuaria de Mercado Libre, tanto por su longevidad e importancia como por la gran cantidad de usos entre muchas de nuestras verticales de negocio.

Entre los problemas que encontramos en las distintas implementaciones de Precio, mencionamos los siguientes:

  • Inconsistencia en la lectura de las currencies. Como ejemplo, para un precio con el signo “$” con el cual se desea representar Pesos, el lector interpreta y anuncia la palabra “Dólares”.
  • Falta de un contexto general que permita reconocer los distintos precios que conforman la variante “combo” como un único elemento compuesto (precio anterior + precio actual + descuento). Como consecuencia, se presenta una navegación del componente no optimizada (navegación por tres elementos “sueltos” a diferencia de uno solo con una única descripción general).
  • Feedback visual no respaldado con una alternativa para personas ciegas. Para el caso del precio tachado, que busca ser interpretado como un precio anterior, necesitamos que el anuncio leído refleje este mismo estado.
  • Lectura de separadores decimales y de miles. En este caso, detectamos que según diferentes configuraciones, el lector puede interpretar un número de manera errónea al tomar separadores de mil como separadores decimales, por lo cual, como ejemplo, puede producirse la lectura de un precio de “diez mil” (10.000) como “diez punto cero”, o alternativamente, “diez punto cero, cero, cero”.
Video de ejemplo con la implementación anterior del componente de precios, y la experiencia resultante al navegarlo con Talkback

Apoyándonos en el feedback de nuestro equipo de accesibilidad, en conjunto con la participación de referentes en la materia y de personas usuarias de lectores de pantalla, llegamos a una validación sobre la experiencia que queremos transmitir al navegar precios.

Como solución a todo lo planteado, decidimos crear un componente que unifique la experiencia mencionada, y que mantenga la consistencia visual y los estándares con los que manejamos nuestros componentes de UI en todo nuestro ecosistema. Este componente, al que decidimos llamar MoneyAmount, pasó a formar parte de Andes UI, nuestro design system, que cuenta con tres aristas mediante las cuales se dispone nuestro trabajo hacia todos nuestros equipos: Diseño (Figma), Web (React) y Nativo (Android y iOS).

Sobre las bases de accesibilidad en Android

El modelo de accesibilidad según Google

Como una pequeña introducción al trabajo que realizamos desde desarrollo cuando tratamos sobre accesibilidad en nuestros componentes, vemos necesario contar cómo funciona lo que Google llama el “modelo de accesibilidad”. Para las personas no usuarias de los servicios de accesibilidad, la manera de interactuar con una app es directa: la UI expone información, y la persona interactúa directamente con esta UI.

Interacción entre personas no usuarias de servicios de accesibilidad y la UI. La UI presenta información y la persona actúa sobre ella.
Interacción entre personas no usuarias de servicios de accesibilidad y la UI. La UI presenta información y la persona actúa sobre ella.

En cambio, para las personas que usan servicios de accesibilidad, el modelo de interacción cambia: la persona interactúa con el servicio, y éste último es el intermediario con la app.

Esto tiene un motivo principal: ya que los servicios son agentes externos a cada app, este modelo le permite a quien desarrolla trabajar de una manera agnóstica a éstos, y dejar en manos del framework la responsabilidad de interpretar la información pertinente para que cada servicio funcione correctamente.

Interacción entre personas usuarias y diferentes servicios de accesibilidad (Talkback, Switch Access, Braille Keyboard, entre otros). Estos servicios interactúan con el framework, que provee la información necesaria para que cada uno de ellos funcione correctamente.
Interacción entre personas usuarias y diferentes servicios de accesibilidad (Talkback, Switch Access, Braille Keyboard, entre otros). Estos servicios interactúan con el framework, que provee la información necesaria para que cada uno de ellos funcione correctamente.

Para conocer más sobre el tema, recomendamos este video.

De la UI al Android Framework: el árbol de accesibilidad

Teniendo en cuenta este modelo de accesibilidad, lo que contemplamos es que toda la información que el framework necesita para que los servicios funcionen correctamente está concentrada dentro del árbol de accesibilidad. El árbol de accesibilidad es una representación de la jerarquía de views con las que cuenta una pantalla, en donde cada View (o nodo del árbol) es representada por un objeto AccessibilityNodeInfo. A modo de ejemplo, en los siguientes gráficos exponemos un árbol de vistas y el árbol de accesibilidad que se desprende del primero (todos los datos son ficticios y se exponen a modo de referencia).

Ejemplo de árbol de views que representa una UI. Dentro tenemos varios viewGroups (layouts) que contienen otras views (imageViews para mostrar imágenes, textViews para texto plano, buttons, checkboxes).
Ejemplo de árbol de views que representa una UI. Dentro tenemos varios viewGroups (layouts) que contienen otras views (imageViews para mostrar imágenes, textViews para texto plano, buttons, checkboxes).
Árbol de accesibilidad equivalente, en donde cada view es representada por un node que contiene toda la información necesitada por los diferentes servicios de accesibilidad. Algunas de las propiedades son agregadas a modo de ejemplo.
Árbol de accesibilidad equivalente, en donde cada view es representada por un node que contiene toda la información necesitada por los diferentes servicios de accesibilidad. Algunas de las propiedades son agregadas a modo de ejemplo.

Dentro del AccessibilityNodeInfo se encuentra concentrada toda la información necesaria para que cada servicio pueda conocer la estructura y atributos varios de nuestra view (medidas de ancho, alto, estados, orden secuencial de navegación, eventos que soporta tales como clicks, long-clicks, gestos de scroll, etc). Lo importante de este punto es comprender que la información que el framework provee a cada servicio de accesibilidad se encuentra contenida en este objeto, y que no hay interacción directa entre las views de la app y el servicio correspondiente.

Lectores de pantalla en Android y en Web: un dato clave

Habiendo contado un poco sobre el backstage de la accesibilidad y el android framework, compartimos un punto importante con el que nos encontramos en la instancia de research.

Haciendo una comparativa sobre las dificultades encontradas en Web en cuanto a la diversidad de lectores de pantalla y navegadores (nuevamente les remito a la interesante y completísima historia que contó Martín Di Luzio) y la existencia de un único lector en Android (el Talkback), nos encontramos con una ventaja grande: desde Android no nos encontramos con las inconsistencias que sí ocurren en Web en cuanto a lectura de números, separadores decimales y textos con distintos styles (tales como textos tachados o superíndices). Esto nos asegura que la lectura del árbol de accesibilidad por parte del lector de pantalla será siempre consistente, y que la solución que se desarrolle será uniforme y congruente con los estándares que las personas usuarias esperan cuando interactúan con una aplicación robusta.

Sobre el desarrollo (Take me to the code!)

Cómo manejamos la accesibilidad en nuestros componentes

Antes de introducirnos en la implementación del MoneyAmount, vale la pena incluir una nota sobre cómo solemos manejar la accesibilidad en la gran mayoría de nuestros componentes. Dado que éstos suelen ser custom views, necesitamos ocuparnos de varios aspectos simultáneos que hacen a la buena experiencia de uso. Estos aspectos comprenden la correcta lectura de la descripción y rol del componente, la correcta exposición de los estados disponibles (habilitado/inhabilitado, error, activado/desactivado, etc), las acciones disponibles que se pueden hacer sobre el mismo (click, long-click, etc), el aviso ante eventos sobre el componente (avisos de carga cuando un botón pasa a un estado “loading”), entre otros.

Todos los componentes visuales de Android, nativos o custom, deben heredar directa o indirectamente de la clase View, y por lo tanto cuentan con un delegate heredado, de tipo View.AccessibilityDelegate, que es el objeto que manejará toda la interacción entre el componente y el servicio de accesibilidad. Además, se nos ofrece un setter que, de ser necesario, nos permite agregar un delegate custom.

Diagrama que representa la herencia de clases de nuestros componentes, donde se explica la herencia de View y la dependencia con el accessibility delegate. Dentro del delegate se muestran a modo de ejemplo los métodos que configurarán el nodeInfo (generar texto, manejar estado, generar acciones disponibles)
Diagrama que representa la herencia de clases de nuestros componentes, donde se explica la herencia de View y la dependencia con el accessibility delegate. Dentro del delegate se muestran a modo de ejemplo los métodos que configurarán el nodeInfo (generar texto, manejar estado, generar acciones disponibles)

Para mantener toda la lógica de manejo de cuestiones de accesibilidad más encapsulada (respetando el principio de Separation of Concerns y por consiguiente, mejorando la cohesión, legibilidad y testeabilidad del código), por cada componente creamos un delegate custom que hereda de View.AccessibilityDelegate, y que tendrá acceso a la info necesaria para aplicar la lógica correspondiente a cada aspecto a configurar. La estructura básica común de nuestros delegates es la siguiente:

Este delegate custom se setea en la clase principal al momento de la construcción.

Dentro de este custom delegate tenemos acceso al AccessibilityNodeInfo, objeto sobre el cual editaremos la información necesaria de acuerdo a la necesidad del componente.

Trabajando sobre este AccessibilityNodeInfo somos capaces de agregar un contentDescription (el texto alternativo, que muchos conocerán de la plataforma web), decir si el componente isEnabled o isChecked, agregar acciones tales como “expandir”, “cerrar”, o cualquier otra acción custom que decidamos agregar según el componente, entre otra gran cantidad de parámetros editables.

MoneyAmount

El componente que representa la versión más atómica de precio es el MoneyAmount. Este componente tiene tres variantes: POSITIVE, NEGATIVE y PREVIOUS.

Ejemplos de componente con type = MoneyAmountType.POSITIVE
Ejemplos de componente con type = MoneyAmountType.POSITIVE
Ejemplos de componente con type = MoneyAmountType.NEGATIVE
Ejemplos de componente con type = MoneyAmountType.NEGATIVE
Ejemplos de componente con type = MoneyAmountType.PREVIOUS
Ejemplos de componente con type = MoneyAmountType.PREVIOUS

Cuando comenzamos a ver la experiencia al focalizar el componente con Talkback, encontramos que el sistema infiere el tipo de moneda y la descripción de los decimales según valores externos a nuestro alcance, y que tienen que ver con la configuración propia de Talkback, tanto del sintetizador de voz como del idioma, e incluso del idioma del dispositivo. Además, el sistema no distingue diferencias entre precios tachados y no tachados al momento de la lectura.

Adicionalmente, debíamos contemplar la posibilidad de manejar más de una moneda en un mismo contexto, y que la responsabilidad de definir el texto de descripción correcto estuviera del lado de nuestra estructura del componente, y no de la implementación en el frontend. Para manejar estas variables vimos la necesidad de encapsular esa responsabilidad en una clase que contuviera esta información independientemente de los valores del Talkback y del device:

Asimismo, para poder manejar correctamente el importe (amount) y de esa manera convertirlo en texto legible, necesitamos saber los caracteres usados como separadores de miles y de decimales, valores que cambian según el país de cada moneda. Por este motivo, nos vimos en la necesidad de crear otra clase helper más:

Ahora, contando con estas clases, la lógica para la generación del contentDescription queda configurada de la siguiente manera:

Como nota de color, tener en cuenta que el método de generación de la descripción está anidado en un companion object, lo que permite accederlo manera estática. Esto será necesario para la creación de la descripción del componente combo, explicado más abajo.

MoneyAmountDiscount

Este componente corresponde al bloque de descuento.

Ejemplo de MoneyAmountDiscount
Ejemplo de MoneyAmountDiscount

Al igual que en el MoneyAmount, para manejar la descripción de este componente creamos un custom delegate que crea un string por medio de un resource con distintas opciones de traducción para español, portugués e inglés.

Ejemplos del string resultante en diferentes idiomas:

-“9 por ciento de descuento.”

-“9 percent off.”

Al igual que el componente anterior, el método de generación de la descripción es agregado como estático, para poder ser reutilizado en el componente combo.

MoneyAmountCombo

Este componente corresponde al bloque de precio que contiene un valor anterior, un valor actual y el porcentaje de descuento correspondiente.

Ejemplo de MoneyAmountCombo
Ejemplo de MoneyAmountCombo

Estructuralmente, aprovechamos el hecho de que tenemos disponibles los componentes por separado, y tomamos este componente como un contenedor de precios internos.

Para este caso, tomamos el conjunto como una vista semántica. Esto significa que, dado que los elementos que componen el conjunto cuentan con un significado común, es mejor presentarlos como un elemento único cuya descripción es la conjunción de las descripciones individuales. De esa manera reducimos la cantidad de ruido y logramos una navegación más directa y menos frustrante.

Desde el código, agrupamos todas las descripciones internas en una descripción general a asignar al componente, y le damos la capacidad de tomar foco. Así, el sistema se encarga automáticamente de “esconder” los nodos internos y de pasar el componente como un nodo único con su descripción propia. El único agregado al texto resultante de unir las tres descripciones internas es la adición del texto “ahora” al precio actual, que no forma parte de la descripción del MoneyAmount individual (en este caso, el que muestra $1.350). La descripción resultante para este ejemplo queda de la siguiente manera:

-“Ahora: 1350 pesos, 10 por ciento de descuento. Antes: 1500 pesos.”

-“Now: 1350 pesos, 10 percent off. Before: 1500 pesos.”

El código de la clase principal con la configuración de accesibilidad queda así:

El custom delegate aprovecha los métodos estáticos de generación de descripción que están presentes en los custom delegates de los componentes individuales:

A continuación exponemos un video con la experiencia definitiva del componente:

Video de ejemplo con la implementación actual del componente de precios, y la experiencia resultante al navegarlo con Talkback

Nuestro aprendizaje

A nivel de desarrollo

Nos parece relevante aclarar una vez más que el modelo de accesibilidad de Google nos permite trabajar la accesibilidad de nuestras aplicaciones de una manera integral, sin diferenciar hacia qué servicio puntual estamos trabajando sino creando una solución completa para todas las herramientas que permiten una experiencia perceptible, operable, entendible y robusta para todas las personas usuarias.

Encontramos que al desarrollar la accesibilidad de un componente visual es mucho más fácil respetar la separación de responsabilidades creando nuestros propios accessibility delegates que contendrán gran parte del código de la solución. Además, trabajar directamente sobre el nodeInfo nos permite agregar propiedades sin la necesidad de sobreescribir métodos nativos tales como setContentDescription. Por otra parte, agregar estas propiedades en el último paso antes de que pasen a ser inmutables nos permite evitar la sobreescritura de estos valores del lado del frontend que implementa el componente.

A nivel de experiencia en accesibilidad

El trabajo en conjunto para asuntos de accesibilidad es clave. Es necesaria la multiplicidad de voces, y principalmente la presencia de personas usuarias de tecnologías asistivas que nos ayuden a validar los comportamientos esperados.

Dentro de este trabajo en conjunto, encontramos que en los design systems es clave pensar en un approach accessible first, es decir, incluir el comportamiento accesible al momento de la concepción del componente. Siempre es mejor incluirlo en la etapa de diseño que tener que agregarlo como un fix tardío que elimina las barreras que nosotros mismos creamos. De esta manera, nos aseguramos de que la experiencia brindada será 100% accesible desde sus inicios, y que evitaremos retrabajos en el desarrollo que nos harán aumentar los tiempos de llegada a producción.

Desde una perspectiva más personal, creemos que el componente combo es un buen ejemplo de vista semántica. Podemos ver que aplicando este concepto logramos una mejor descripción de la vista mientras simplificamos la navegación de tres elementos a uno solo. Además, al tener que definir una descripción única para todo el elemento, podemos elegir mencionar primero el valor que está más jerarquizado visualmente dado su tamaño y color de texto, seguido de los valores menos jerarquizados, y de esa manera lograr una experiencia más uniforme entre personas usuarias con distintos grados de visión.

Últimos comentarios

La formación en accesibilidad es un camino que se construye constantemente. Sabemos que no hay fórmulas o recetas que derriben por sí solas las barreras de la accesibilidad, por lo cual debemos ser especialmente conscientes de que generar, buscar y compartir conocimiento a través del research y la experimentación, trabajando en equipo y con multiplicidad de voces es algo que siempre debe incentivarse, para que entre todas las personas afiancemos la accesibilidad digital como un punto clave en el contexto en el que vivimos.

--

--