LiveRegion — anunciar cambios automáticamente
Un LiveRegion es un área de la UI que TalkBack monitorea. Cuando el contenido cambia, TalkBack lo anuncia automáticamente, aunque el usuario no esté enfocado en ese elemento. Es esencial para contadores, mensajes de estado y cualquier información que cambia sin interacción directa.
// Compose — LiveRegion con liveRegion()
@Composable
fun ContadorCarrito(cantidad: Int) {
Text(
text = "$cantidad items en el carrito",
modifier = Modifier.semantics {
liveRegion = LiveRegionMode.Polite
// Polite: espera a que TalkBack termine de hablar antes de anunciar
// Assertive: interrumpe lo que TalkBack está diciendo (solo para urgente)
}
)
}
// Cada vez que `cantidad` cambie, TalkBack anuncia el nuevo valor automáticamente
// Para mensajes de estado que aparecen y desaparecen:
@Composable
fun MensajeExito(mensaje: String?) {
if (mensaje != null) {
Text(
text = mensaje,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.semantics {
liveRegion = LiveRegionMode.Polite
}
)
}
}
// En Views (XML):
// android:accessibilityLiveRegion="polite"
// o: android:accessibilityLiveRegion="assertive"
// Desde código:
ViewCompat.setAccessibilityLiveRegion(
binding.tvMensajeExito,
ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE
)
Estados de carga — comunicarlos correctamente
// ❌ Loading spinner sin descripción
CircularProgressIndicator()
// TalkBack dice "Progreso circular" — no informa qué está cargando
// ✓ Loading con descripción
CircularProgressIndicator(
modifier = Modifier.semantics {
contentDescription = "Cargando productos..."
}
)
// ✓ Anunciar cuando la carga termina:
@Composable
fun PantallaProductos(uiState: ProductosUiState) {
var estadoAnterior by remember { mutableStateOf(uiState) }
// Anunciar cuando la carga termina
if (estadoAnterior.isLoading && !uiState.isLoading) {
// El contenido cambió de "cargando" a "cargado"
// Usar un LiveRegion en el contenedor para anunciarlo
}
estadoAnterior = uiState
Box {
if (uiState.isLoading) {
CircularProgressIndicator(
modifier = Modifier.semantics {
contentDescription = "Cargando, por favor espera"
}
)
} else {
Column(
modifier = Modifier.semantics {
if (!uiState.isLoading) {
liveRegion = LiveRegionMode.Polite
contentDescription = "${uiState.productos.size} productos cargados"
}
}
) {
LazyColumn { /* items */ }
}
}
}
}
Snackbars y toasts accesibles
// Snackbar de Material3 — es accesible automáticamente
// TalkBack anuncia el mensaje al aparecer
SnackbarHost(hostState = snackbarHostState)
// Para mostrar un snackbar:
LaunchedEffect(key1 = mensajeError) {
if (mensajeError != null) {
snackbarHostState.showSnackbar(
message = mensajeError,
actionLabel = "Reintentar",
duration = SnackbarDuration.Long
// duration larga — dar tiempo al usuario de TalkBack para escuchar y actuar
)
}
}
// Toast — tiene accesibilidad básica pero limitaciones:
// → TalkBack lo anuncia pero desaparece rápido
// → Para mensajes importantes, preferir Snackbar con duration larga
// → Si usás Toast, asegurarte de que la información también esté disponible
// de otra forma (en la pantalla, como estado del composable)
Toast.makeText(context, "Guardado correctamente", Toast.LENGTH_LONG).show()
// Para mensajes temporales críticos — usar un LiveRegion en vez de Toast:
@Composable
fun MensajeTemporario(mensaje: String?, duracionMs: Long = 3000) {
var mensajeVisible by remember { mutableStateOf(mensaje) }
LaunchedEffect(mensaje) {
mensajeVisible = mensaje
delay(duracionMs)
mensajeVisible = null
}
mensajeVisible?.let {
Text(
text = it,
modifier = Modifier
.semantics { liveRegion = LiveRegionMode.Polite }
.alpha(if (mensajeVisible != null) 1f else 0f)
)
}
}
LazyColumn y listas accesibles
// Informar el tamaño de la lista y la posición del item:
@Composable
fun ListaAccesible(items: List<Item>) {
LazyColumn(
modifier = Modifier.semantics {
collectionInfo = CollectionInfo(rowCount = items.size, columnCount = 1)
}
) {
itemsIndexed(items, key = { _, item -> item.id }) { index, item ->
ItemCard(
item = item,
modifier = Modifier.semantics {
collectionItemInfo = CollectionItemInfo(
rowIndex = index,
rowSpan = 1,
columnIndex = 0,
columnSpan = 1
)
// TalkBack anuncia: "Item X de Y" cuando el usuario navega la lista
}
)
}
}
}
// Para secciones con headers:
LazyColumn {
items.groupBy { it.categoria }.forEach { (categoria, itemsCategoria) ->
item {
Text(
text = categoria,
modifier = Modifier.semantics { heading() }
// heading() hace que TalkBack anuncie "encabezado" y permite
// al usuario navegar entre encabezados con un gesto específico
)
}
items(itemsCategoria, key = { it.id }) { item ->
ItemCard(item)
}
}
}
Paginación accesible
// Con Paging 3 — anunciar cuando cargan más items
@Composable
fun ListaPaginada(pagingItems: LazyPagingItems<Producto>) {
LazyColumn {
items(pagingItems.itemCount, key = pagingItems.itemKey { it.id }) { index ->
val producto = pagingItems[index]
if (producto != null) {
ProductoCard(producto)
}
}
// Estado de carga al final de la lista
when (val loadState = pagingItems.loadState.append) {
is LoadState.Loading -> {
item {
CircularProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.semantics {
contentDescription = "Cargando más productos"
liveRegion = LiveRegionMode.Polite
}
)
}
}
is LoadState.Error -> {
item {
TextButton(
onClick = { pagingItems.retry() },
modifier = Modifier.semantics {
contentDescription = "Error al cargar. Toca para reintentar"
liveRegion = LiveRegionMode.Assertive
}
) {
Text("Error al cargar. Reintentar")
}
}
}
else -> {}
}
}
}
Contadores y badges accesibles
// Badge en ícono de navegación
@Composable
fun NavItemConBadge(
label: String,
icono: ImageVector,
contadorNotificaciones: Int,
onClick: () -> Unit
) {
NavigationBarItem(
icon = {
BadgedBox(
badge = {
if (contadorNotificaciones > 0) {
Badge { Text("$contadorNotificaciones") }
}
}
) {
Icon(icono, contentDescription = null)
}
},
label = { Text(label) },
selected = false,
onClick = onClick,
modifier = Modifier.semantics {
// Combinar el label con el contador en la descripción accesible
contentDescription = if (contadorNotificaciones > 0)
"$label, $contadorNotificaciones notificaciones"
else
label
}
)
}
// TalkBack anuncia: "Notificaciones, 3 notificaciones, tab"
// En lugar de: "Notificaciones, tab" + "3" (dos nodos separados)