Accesibilidad en Jetpack Compose

En este codelab, aprenderás a usar Jetpack Compose para mejorar la accesibilidad de la app. Explicaremos varios casos de uso frecuentes y mejoraremos paso a paso una app de ejemplo. Abarcaremos los tamaños de los objetivos táctiles, las descripciones del contenido, las etiquetas de clics y mucho más.

Las personas con visión reducida, daltonismo, problemas de audición, trastornos de la motricidad, discapacidades cognitivas y muchas otras afecciones usan los dispositivos Android para completar tareas en su vida cotidiana. Desarrollar apps con la accesibilidad en mente mejora la experiencia de los usuarios, especialmente de los que tienen estas y otras necesidades de accesibilidad.

Durante este codelab, usaremos TalkBack para probar los cambios en el código de forma manual. TalkBack es un servicio de accesibilidad que usan principalmente las personas con discapacidad visual. Asegúrate de probar también los cambios que realices en el código con otros servicios de accesibilidad, como la Accesibilidad con interruptores.

Rectángulo de enfoque de TalkBack que se mueve por la pantalla principal de Jetnews. El texto que anuncia TalkBack se muestra en la parte inferior de la pantalla.

TalkBack en funcionamiento en la app de Jetnews.

Qué aprenderás

En este codelab, aprenderás lo siguiente:

  • Cómo satisfacer a los usuarios con trastornos de la motricidad mediante el aumento de los tamaños de los objetivos táctiles.
  • Qué son las propiedades semánticas y cómo se cambian.
  • Cómo brindarle información a los elementos que admiten composición a fin de que sean más accesibles.

Qué necesitarás

Qué compilarás

En este codelab, mejoraremos la accesibilidad de una app para leer noticias. Comenzaremos con una app a la que le faltan las funciones de accesibilidad fundamentales y aplicaremos lo que aprendemos a fin de lograr que nuestra app sea más útil para las personas con necesidades de accesibilidad.

En este paso, descargarás el código para este codelab, que comprende una app simple para leer noticias.

Qué necesitarás

Obtén el código

El código para este codelab se puede encontrar en el repositorio de GitHub de android-compose-codelabs. Para clonarlo, ejecuta lo siguiente:

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

Como alternativa, puedes descargar dos archivos ZIP:

Revisa la app de ejemplo

El código que acabas de descargar contiene código para todos los codelabs de Compose disponibles. Para completar este codelab, abre el proyecto AccessibilityCodelab en Android Studio.

Te recomendamos que comiences con el código de la rama main y sigas el codelab paso a paso a tu propio ritmo.

Configura TalkBack

Durante este codelab, usaremos TalkBack para verificar los cambios. Si usas un dispositivo físico a fin de realizar las pruebas, sigue estas instrucciones para activar TalkBack. En los emuladores, la app de TalkBack no está instalada de forma predeterminada. Elige un emulador que incluya Play Store y descarga el Suite de Accesibilidad Android.

Todos los elementos en la pantalla en los que se puede hacer clic, que se pueden tocar o con los que se puede interactuar de algún otro modo, deben ser lo suficientemente grandes para permitir una interacción confiable. Asegúrate de que estos elementos tengan una altura y un ancho mínimos de 48 dp.

Si el tamaño de estos controles se establece de forma dinámica o cambia según la dimensión del contenido, considera usar el modificador sizeIn para configurar un límite inferior en sus dimensiones.

Algunos componentes de Material establecen estos tamaños por ti. Por ejemplo, el elemento Button que admite composición tiene su MinHeight establecido en 36 dp y usa un padding vertical de 8 dp. Si se suman, se alcanza la altura requerida de 48 dp.

Cuando abramos nuestra app de ejemplo y ejecutemos TalkBack, notaremos que el ícono de cruz en las tarjetas de las publicaciones tiene un objetivo táctil muy pequeño. Queremos que este objetivo táctil tenga, como mínimo, 48 dp.

A continuación, te mostramos una captura de pantalla con la app original a la izquierda, en comparación con la solución mejorada a la derecha.

Comparación de un elemento de la lista que muestra un pequeño contorno del ícono de cruz a la izquierda y un contorno grande a la derecha.

Observemos la implementación y verifiquemos el tamaño de este elemento que admite composición. Abre PostCards.kt y busca el elemento PostCardHistory que admite composición. Como puedes ver, la implementación establece el tamaño del ícono del menú ampliado en 24 dp:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...

   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           Icon(
               imageVector = Icons.Default.Close,
               contentDescription = stringResource(R.string.cd_show_fewer),
               modifier = Modifier
                   .clickable { openDialog = true }
                   .size(24.dp)
           )
       }
   }
   // ...
}

Para aumentar el tamaño del objetivo táctil de Icon, podemos agregar padding:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           Icon(
               imageVector = Icons.Default.Close,
               contentDescription = stringResource(R.string.cd_show_fewer),
               modifier = Modifier
                   .clickable { openDialog = true }
                   .padding(12.dp)
                   .size(24.dp)
           )
       }
   }
   // ...
}

En nuestro caso de uso, hay una manera más sencilla de asegurarse de que el tamaño del objetivo táctil sea, como mínimo, 48 dp. Podemos usar el componente IconButton de Material que se encargará de esto por nosotros:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       // ...
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           IconButton(onClick = { openDialog = true }) {
               Icon(
                   imageVector = Icons.Default.Close,
                   contentDescription = stringResource(R.string.cd_show_fewer)
               )
           }
       }
   }
   // ...
}

Cuando navegues por la pantalla con TalkBack, ahora se muestra correctamente un objetivo táctil con un área de 48 dp. Además, IconButton también agrega una indicación de ripple, que le muestra al usuario que se puede hacer clic en el elemento.

Según la configuración predeterminada, los elementos en los que se puede hacer clic en la app no brindan información sobre lo que sucederá cuando se haga clic en ellos. Por lo tanto, los servicios de accesibilidad, como TalkBack, usarán una descripción predeterminada muy genérica.

A fin de ofrecerles la mejor experiencia a los usuarios con necesidades de accesibilidad, podemos brindar una descripción específica que explique qué sucederá cuando el usuario haga clic en este elemento.

En la app de Jetnews, los usuarios pueden hacer clic en las distintas tarjetas de las publicaciones para leerlas por completo. De forma predeterminada, se leerá el contenido del elemento en el que se puede hacer clic, seguido del texto "Presiona dos veces para activar". En cambio, queremos ser más específicos y usar la opción "Presiona dos veces para leer el artículo". Esta es la apariencia de la versión original en comparación con nuestra solución ideal:

Dos grabaciones de pantalla con TalkBack habilitado, en las que se presiona una publicación en una lista vertical y otra en un carrusel horizontal.

Se cambia la etiqueta de clic de un elemento que admite composición. Antes (a la izquierda) y después (a la derecha).

Los elementos que admiten composición, como Surface y Card, y el modificador clickable incluyen un parámetro en el que podemos configurar directamente esta etiqueta de clic.

Volvamos a observar la implementación de PostCardHistory:

@Composable
fun PostCardHistory(
   // ...
) {
   Row(
       Modifier.clickable { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

Como puedes ver, esta implementación usa el modificador clickable. Para establecer una etiqueta de clic, podemos configurar el parámetro onClickLabel:

@Composable
fun PostCardHistory(
   // ...
) {
   Row(
       Modifier.clickable(
               // R.string.action_read_article = "read article"
               onClickLabel = stringResource(R.string.action_read_article)
           ) {
               navigateToArticle(post.id)
           }
   ) {
       // ...
   }
}

Ahora TalkBack anuncia correctamente "Presiona dos veces para leer el artículo".

Las demás tarjetas de publicaciones en la pantalla principal tienen la misma etiqueta genérica de clic. Observemos la implementación del elemento PostCardPopular que admite composición y actualicemos su etiqueta de clic:

@Composable
fun PostCardPopular(
   // ...
) {
   Card(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier.size(280.dp, 240.dp),
       onClick = { navigateToArticle(post.id) }
   ) {
       // ...
   }
}

Ese elemento que admite composición usa Card que también la admite de manera interna, y tiene una sobrecarga que te permite pasar la etiqueta de clic:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PostCardPopular(
   post: Post,
   navigateToArticle: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   Card(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier.size(280.dp, 240.dp),
       onClick = { navigateToArticle(post.id) },
       onClickLabel = stringResource(id = R.string.action_read_article)
   ) {
       // ...
   }
}

Muchas apps muestran algún tipo de lista en la que cada uno de sus elemento contiene una o más acciones. Cuando se usa un lector de pantalla, navegar por esa lista puede resultar tedioso, ya que la misma acción se enfoca una y otra vez.

En su lugar, podemos agregar acciones personalizadas de accesibilidad a un elemento que admite composición. De esta manera, pueden agruparse las acciones relacionadas con el mismo elemento de la lista.

En la app de Jetnews, mostramos una lista de artículos que el usuario puede leer. Cada elemento de la lista incluye una acción para indicar que el usuario desea ver menos contenido sobre este tema. En esta sección, moveremos esta acción a una acción personalizada de accesibilidad, por lo que navegar por la lista será más sencillo.

A la izquierda, puedes ver la situación predeterminada en la que cada ícono de cruz es enfocable. A la derecha, puedes ver la solución en la que se incluye la acción en las acciones personalizadas de TalkBack:

Dos grabaciones de pantalla con TalkBack habilitado. A la izquierda de la pantalla, se muestra cómo se puede seleccionar el ícono de cruz en la publicación. Presionar dos veces abre un diálogo. A la derecha de la pantalla, se muestra un gesto de tres toques para abrir un menú de acciones personalizadas. Si presionas la acción "Mostrar menos de este contenido", se abrirá el mismo diálogo.

Se agrega una acción personalizada a un elemento de publicación. Antes (a la izquierda) y después (a la derecha).

Abramos PostCards.kt y observemos la implementación del elemento PostCardHistory que admite composición. Observa las propiedades en las que se puede hacer clic de Row y IconButton, con Modifier.clickable y onClick:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       Modifier.clickable(
           onClickLabel = stringResource(R.string.action_read_article)
       ) {
           navigateToArticle(post.id)
       }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
           IconButton(onClick = { openDialog = true }) {
               Icon(
                   imageVector = Icons.Default.Close,
                   contentDescription = stringResource(R.string.cd_show_fewer)
               )
           }
       }
   }
   // ...
}

De forma predeterminada, se puede hacer clic en los elementos Row y IconButton que admiten composición y, como resultado, TalkBack los enfocará. Esto sucede para cada elemento de nuestra lista, lo que implica mucho deslizamiento cuando se navega por esta. En cambio, queremos que la acción relacionada con IconButton se incluya como una acción personalizada en el elemento de la lista. Podemos indicarles a los servicios de accesibilidad que no interactúen con Icon mediante el modificador clearAndSetSemantics:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   Row(
       Modifier.clickable(
           onClickLabel = stringResource(R.string.action_read_article)
       ) {
           navigateToArticle(post.id)
       }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            IconButton(
                modifier = Modifier.clearAndSetSemantics { },
                onClick = { openDialog = true }
            ) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = stringResource(R.string.cd_show_fewer)
                )
            }
       }
   }
   // ...
}

Sin embargo, si se quita la semántica de IconButton, ya no es posible ejecutar la acción. En su lugar, podemos agregar la acción al elemento de la lista si añadimos una acción personalizada en el modificador semantics:

@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
   // ...
   val showFewerLabel = stringResource(R.string.cd_show_fewer)
   Row(
        Modifier
            .clickable(
                onClickLabel = stringResource(R.string.action_read_article)
            ) {
                navigateToArticle(post.id)
            }
            .semantics {
                customActions = listOf(
                    CustomAccessibilityAction(
                        label = showFewerLabel,
                        // action returns boolean to indicate success
                        action = { openDialog = true; true }
                    )
                )
            }
   ) {
       // ...
       CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            IconButton(
                modifier = Modifier.clearAndSetSemantics { },
                onClick = { openDialog = true }
            ) {
                Icon(
                    imageVector = Icons.Default.Close,
                    contentDescription = stringResource(R.string.cd_show_fewer)
                )
            }
       }
   }
   // ...
}

Ahora, podemos usar la ventana emergente de la acción personalizada en TalkBack para aplicar la acción. Esto se vuelve cada vez más relevante a medida que aumenta la cantidad de acciones dentro de un elemento de la lista.

No todos los usuarios de la app podrán ver o interpretar los elementos visuales que se muestran en esta, como los íconos y las ilustraciones. Tampoco hay manera de que los elementos visuales tengan sentido para los servicios de accesibilidad solo según sus píxeles. Por ello, como desarrollador, es necesario que les pases a los servicios de accesibilidad más información sobre los elementos visuales de la app.

Los elementos visuales que admiten composición, como Image y Icon, incluyen un parámetro contentDescription. En este, pasarás una descripción localizada de ese elemento visual, o null si el elemento es meramente decorativo.

En nuestra app, faltan algunas descripciones del contenido en la pantalla del artículo. Ejecutemos la app y seleccionemos el artículo superior para navegar a la pantalla del artículo.

Dos grabaciones de pantalla con TalkBack habilitado en las que se presiona el botón Atrás en la pantalla del artículo. En la pantalla de la izquierda, se anuncia "Botón; presiona dos veces para activar". En la pantalla de la derecha, se anuncia "Volver a la pantalla anterior; presiona dos veces para activar".

Se agrega una descripción del contenido visual. Antes (a la izquierda) y después (a la derecha).

Cuando no proporcionemos ningún tipo de información, el ícono de navegación en la parte superior izquierda solo se leerá "Botón; presiona dos veces para activar". No se le brinda al usuario ningún tipo de información sobre la acción que se llevará a cabo cuando se active ese botón. Abramos ArticleScreen.kt:

@Composable
fun ArticleScreen(
   // ...
) {
   // ...
   Scaffold(
       topBar = {
           InsetAwareTopAppBar(
               title = {
                   // ...
               },
               navigationIcon = {
                   IconButton(onClick = onBack) {
                       Icon(
                           imageVector = Icons.Filled.ArrowBack,
                           contentDescription = null
                       )
                   }
               }
           )
       }
   ) {
       // ...
   }
}

Agrega al ícono una descripción significativa del contenido:

@Composable
fun ArticleScreen(
   // ...
) {
   // ...
   Scaffold(
       topBar = {
           InsetAwareTopAppBar(
               title = {
                   // ...
               },
               navigationIcon = {
                   IconButton(onClick = onBack) {
                       Icon(
                           imageVector = Icons.Filled.ArrowBack,
                           contentDescription = stringResource(
                               R.string.cd_navigate_up
                           )
                       )
                   }
               }
           )
       }
   ) {
       // ...
   }
}

Otro elemento visual de este artículo es la imagen del encabezado. En este caso, esta imagen es meramente decorativa, no muestra nada que debamos transmitirle al usuario. Por lo tanto, la descripción del contenido se establece en null, y se omite el elemento cuando usamos un servicio de accesibilidad.

El último elemento visual en nuestra pantalla es la foto de perfil. En este caso, usaremos un avatar genérico, por lo que no es necesario agregar una descripción del contenido. Cuando usemos la foto de perfil real de este autor, le podríamos pedir que brinde una descripción del contenido adecuada para esta.

Cuando una pantalla incluye mucho texto, como la pantalla de un artículo, a los usuarios con dificultades visuales les resulta difícil encontrar con rapidez la sección que buscan. Para ayudar con esto, podemos indicar qué partes del texto son encabezados. De esta manera, los usuarios pueden navegar rápidamente por los diferentes encabezados si deslizan el dedo hacia arriba o hacia abajo.

De forma predeterminada, ningún elemento que admite composición se marca como encabezado, por lo que no habrá navegación posible. Queremos que la navegación en pantalla del artículo sea encabezado por encabezado:

Dos grabaciones de pantalla con TalkBack habilitado en las que se desliza hacia abajo para navegar por los encabezados. En la pantalla de la izquierda, se anuncia "No hay ningún encabezado posterior". En la pantalla de la derecha, se desplaza por los encabezados, y se lee cada uno en voz alta.

Se agregan encabezados. Antes (a la izquierda) y después (a la derecha).

Los encabezados de nuestro artículo se definen en PostContent.kt. Abramos ese archivo y desplacémonos hasta el elemento Paragraph que admite composición:

@Composable
private fun Paragraph(paragraph: Paragraph) {
   // ...
   Box(modifier = Modifier.padding(bottom = trailingPadding)) {
       when (paragraph.type) {
           // ...
           ParagraphType.Header -> {
               Text(
                   modifier = Modifier.padding(4.dp),
                   text = annotatedString,
                   style = textStyle.merge(paragraphStyle)
               )
           }
           // ...
       }
   }
}

Aquí, Header se define como un simple elemento Text que admite composición. Podemos establecer la propiedad semántica heading para indicar que este elemento que admite composición es un encabezado.

@Composable
private fun Paragraph(paragraph: Paragraph) {
   // ...
   Box(modifier = Modifier.padding(bottom = trailingPadding)) {
       when (paragraph.type) {
           // ...
           ParagraphType.Header -> {
               Text(
                   modifier = Modifier.padding(4.dp)
                     .semantics { heading() },
                   text = annotatedString,
                   style = textStyle.merge(paragraphStyle)
               )
           }
           // ...
       }
   }
}

Como se observó en los pasos anteriores, los servicios de accesibilidad, como TalkBack, navegan por un pantalla elemento por elemento. De forma predeterminada, recibe enfoque cada elemento de bajo nivel que admite composición en Jetpack Compose y configure, como mínimo, una propiedad semántica. Por ejemplo, un elemento Text que admite composición configura la propiedad semántica text y, por lo tanto, recibe enfoque.

Sin embargo, tener demasiados elementos enfocables en la pantalla puede generar confusión, ya que el usuario los navega uno por uno. En cambio, los elementos que admiten composición se pueden combinar mediante el modificador semantics con su propiedad mergeDescendants.

Veamos la pantalla de nuestro artículo. La mayoría de los elementos reciben el nivel correcto de enfoque. Sin embargo, en este momento, los metadatos del artículo se leen en voz alta como varios elementos separados. Se puede mejorar si combinas esos metadatos en una entidad enfocable:

Dos grabaciones de pantalla con TalkBack habilitado. En la pantalla de la izquierda, se muestran rectángulos verdes separados de TalkBack para los campos de autor y metadatos. En la pantalla de la derecha, se muestra un rectángulo alrededor de ambos campos, y se lee el contenido concatenado.

Se combinan elementos que admiten composición. Antes (a la izquierda) y después (a la derecha).

Abramos PostContent.kt y verifiquemos el elemento PostMetadata que admite composición:

@Composable
private fun PostMetadata(metadata: Metadata) {
   // ...
   Row {
       Image(
           // ...
       )
       Spacer(Modifier.width(8.dp))
       Column {
           Text(
               // ...
           )

           CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
               Text(
                   // ..
               )
           }
       }
   }
}

Podemos indicarle a la fila de nivel superior que combine sus elementos subordinados, lo que generará el comportamiento que deseamos:

@Composable
private fun PostMetadata(metadata: Metadata) {
   // ...
   Row(Modifier.semantics(mergeDescendants = true) {}) {
       Image(
           // ...
       )
       Spacer(Modifier.width(8.dp))
       Column {
           Text(
               // ...
           )

           CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
               Text(
                   // ..
               )
           }
       }
   }
}

Los elementos que se pueden activar o desactivar, como Switch y Checkbox, leen sus estados activados en voz alta a medida que TalkBack los selecciona. Sin contexto, puede ser difícil comprender a qué hacen referencia estos elementos que se pueden activar o desactivar. A fin de incluir contexto para un elemento que se puede activar o desactivar, subimos el estado de ese elemento, de modo que el usuario pueda activar o desactivar Switch o Checkbox presionando el elemento que admite composición, o bien la etiqueta que lo describe.

Podemos observar un ejemplo de esto en la pantalla Interests. Para navegar hasta allí, abre el panel lateral de navegación desde la pantalla principal. En la pantalla Interests, hay una lista de temas a los que el usuario puede suscribirse. De forma predeterminada, las casillas de verificación de esta pantalla se enfocan por separado de las etiquetas, lo que dificulta la comprensión de su contexto. Preferimos que se pueda activar y desactivar todo el objeto Row:

Dos grabaciones de pantalla con TalkBack habilitado en las que se muestra la pantalla Interests con una lista de temas que se pueden seleccionar. En la pantalla de la izquierda, TalkBack selecciona cada casilla de verificación por separado. En la pantalla de la derecha, TalkBack selecciona toda la fila.

Se trabaja con casillas de verificación. Antes (a la izquierda) y después (a la derecha).

Abramos InterestsScreen.kt y observemos la implementación del elemento TopicItem que admite composición:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = { onToggle() },
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

Como puedes ver aquí, Checkbox tiene una devolución de llamada onCheckedChange que controla la activación y desactivación del elemento. Podemos subir esta devolución de llamada hasta el nivel de todo el elemento Row:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

En el paso anterior, subimos el comportamiento de activación y desactivación de Checkbox al elemento Row superior. Podemos mejorar aún más la accesibilidad de este elemento si agregamos una descripción personalizada para el estado del elemento que admite composición.

De forma predeterminada, el estado Checkbox se lee como "Marcado" o "No marcado". Podemos reemplazar esta descripción con nuestra propia descripción personalizada:

Dos grabaciones de pantalla con TalkBack habilitado en las que se presiona un tema en la pantalla Interests. En la pantalla de la izquierda, se anuncia "No marcado", mientras que en la de la derecha, se anuncia "No suscrito".

Se agregan descripciones de estados. Antes (a la izquierda) y después (a la derecha).

Podemos continuar con el elemento TopicItem que admite composición que adaptamos en el último paso:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   Row(
       modifier = Modifier
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

Podemos agregar las descripciones personalizadas de estados mediante la propiedad stateDescription dentro del modificador semantics:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
   // ...
   val stateNotSubscribed = stringResource(R.string.state_not_subscribed)
   val stateSubscribed = stringResource(R.string.state_subscribed)
   Row(
       modifier = Modifier
           .semantics {
               stateDescription = if (selected) {
                   stateSubscribed
               } else {
                   stateNotSubscribed
               }
           }
           .toggleable(
               value = selected,
               onValueChange = { _ -> onToggle() },
               role = Role.Checkbox
           )
           .padding(horizontal = 16.dp, vertical = 8.dp)
   ) {
       // ...
       Checkbox(
           checked = selected,
           onCheckedChange = null,
           modifier = Modifier.align(Alignment.CenterVertically)
       )
   }
}

¡Felicitaciones! Completaste correctamente este codelab y aprendiste más sobre la accesibilidad en Compose. Aprendiste sobre objetivos táctiles, descripciones visuales de elementos y descripciones de estados. Agregaste etiquetas de clics, encabezados y acciones personalizadas. Sabes cómo agregar una combinación personalizada y cómo trabajar con interruptores y casillas de verificación. Si aplicas estos aprendizajes en tus apps, la accesibilidad mejorará de manera notable.

Consulta los otros codelabs sobre la ruta de Compose y otras muestras de código, como Jetnews.

Documentación

Para obtener más información y orientación sobre estos temas, consulta la siguiente documentación: