Irbisa · Cheatsheet
Android Accessibility & Localization
TalkBack, contentDescription, Compose semantics, plurals, per-app language, RTL, font scaling
What does making an Android app accessible involve?
What is
contentDescription, and how do you label an icon-only button in Compose and in Views?// Wrong: no label at all. TalkBack focuses it and says just "button".IconButton(onClick = ::share) { Icon(Icons.Default.Share, contentDescription = null)} // Right: IconButton merges its children, so the icon's label becomes the button's…How do you localize an Android app, and what does a translation-ready app need from the start?
How do you hide a decorative element or a whole subtree from TalkBack, in Compose and in Views?
// Decoration in Compose: no description means no node, so TalkBack skips itImage(painterResource(R.drawable.header_gradient), contentDescription = null) // Hide one node from services but keep it addressable in testsText("3", Modifier.semantics { hideFromAccessibility() }.testTag("unreadBadge"))…What touch-target size and contrast ratios should Android UI meet, and how do you reach 48dp without redrawing the icon?
// Modifier order decides the target: padding AFTER clickable is inside itIcon( Icons.Default.Close, contentDescription = stringResource(R.string.close), modifier = Modifier .clickable(onClick = onClose)…In Compose, what do
mergeDescendants,clearAndSetSemantics,stateDescriptionandroleeach change in what TalkBack says?// literals for brevity; use stringResource in real code// One stop, one sentence instead of five starsRow( Modifier.semantics(mergeDescendants = true) { contentDescription = "Rating: 4 out of 5" },…What does TalkBack announce for this settings row, and how do you fix it?
@Composablefun WifiRow(checked: Boolean, onToggle: (Boolean) -> Unit) { Row( Modifier .fillMaxWidth() .clickable { onToggle(!checked) } .padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { Text("Wi-Fi", Modifier.weight(1f)) Switch(checked = checked, onCheckedChange = onToggle) }}How do you make swipe-to-delete and drag-to-reorder usable for TalkBack and Switch Access users?
// Views: actions the user reaches from TalkBack's actions menuViewCompat.addAccessibilityAction(holder.itemView, getString(R.string.delete)) { _, _ -> onDelete(item.id); true}ViewCompat.addAccessibilityAction(holder.itemView, getString(R.string.move_up)) { _, _ -> onMove(item.id, offset = -1); true…How do you tell a TalkBack user that something changed, like an upload finishing, and why is
announceForAccessibilitydeprecated?// Deprecated in Android 16 (API 36): interrupts and leaves nothing to go back tostatusView.announceForAccessibility(getString(R.string.upload_finished)) // A live region speaks when its own text changes; focus stays where it wasViewCompat.setAccessibilityLiveRegion( binding.status,…A custom filter sheet opens, TalkBack keeps reading the list behind it, and on close focus jumps to the top. How do you fix it?
// A real modal window hides what is behind it and is announced as a new pane.// On dismiss, hand focus back to the control that opened it.var open by remember { mutableStateOf(false) }val filterButton = remember { FocusRequester() }IconButton(onClick = { open = true }, modifier = Modifier.focusRequester(filterButton)) { Icon(Icons.Default.FilterList, contentDescription = stringResource(R.string.filters))…Why must text sizes be in
sp, and what changed with non-linear font scaling in Android 14?// Wrong: text sized in dp, in a box that cannot growRow(Modifier.fillMaxWidth().height(56.dp)) { // clips at 200% Text( item.title, fontSize = with(LocalDensity.current) { 16.dp.toSp() }, // cancels the font scale maxLines = 1,…How do you test accessibility on Android, and what do Accessibility Scanner and automated checks miss?
// View tests: one setup call turns ATF on for every Espresso interaction@Before fun enableChecks() { AccessibilityChecks.enable() .setRunChecksFromRootView(true) // the whole screen, not just the view .setSuppressingResultMatcher( // carry one tracked failure matchesViews(withId(R.id.legacy_banner)),…Find the bug: the like button works with a finger, but when a TalkBack user double-taps it, nothing happens.
// Like button: shrinks while pressed, toggles the like on releasebinding.likeButton.setOnTouchListener { view, event -> when (event.actionMasked) { MotionEvent.ACTION_DOWN -> view.animate().scaleX(0.85f).scaleY(0.85f) MotionEvent.ACTION_UP -> { view.animate().scaleX(1f).scaleY(1f) viewModel.toggleLike(post.id) } MotionEvent.ACTION_CANCEL -> view.animate().scaleX(1f).scaleY(1f) } true}How does TalkBack get its node tree, and how do you expose the parts of a canvas-drawn chart?
// Ten bars on one canvas are ONE node until you publish virtual onesclass ChartTouchHelper(private val view: ChartView) : ExploreByTouchHelper(view) { override fun getVirtualViewAt(x: Float, y: Float): Int = view.barIndexAt(x, y) ?: HOST_ID…What is wrong with these string resources once the app ships in Russian, and how do you fix them?
<!-- res/values/strings.xml --><string name="cart_count">%d items in cart</string><string name="moved">Moved %s to %s</string> <!-- res/values-ru/strings.xml --><string name="cart_count">%d товаров в корзине</string><string name="moved">В папку %s перемещён файл %s</string> <!-- call sites getString(R.string.cart_count, count) getString(R.string.moved, file.name, folder.name)-->You ship English in
values/and avalues-ru; a user's language list is Ukrainian, then Russian. Which strings do they get, and why?How do you implement an in-app language switcher with per-app language preferences on Android 13+ and on older versions?
// res/xml/locales_config.xml lists the shipped locales and is referenced by// <application android:localeConfig="@xml/locales_config">,// or generated by AGP: androidResources { generateLocaleConfig = true } // One call, every API level. 33+ hands it to LocaleManager and recreates the// activities; below 33 AppCompat stores and re-applies it (autoStoreLocales).…Find the bug: nearby search works on English phones but returns nothing when the phone language is Russian or German.
class PlacesRepository(private val http: HttpClient) { suspend fun nearby(lat: Double, lon: Double, radiusKm: Double): List<Place> { val url = "https://api.example.com/places?lat=%.6f&lon=%.6f&radius=%.1f" .format(lat, lon, radiusKm) return http.get(url).body() }}How do you support right-to-left languages, and what does Android not mirror for you?
// Mirrors for free: start/end everywhere, never left/rightRow(Modifier.padding(start = 16.dp, end = 8.dp)) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) Text(order.title, Modifier.weight(1f))}…How do you format dates, numbers and currency for the user's locale, and what goes wrong with hand-written patterns and
toDouble()?// Wrong: three bugs in three linesval date = SimpleDateFormat("dd/MM/yyyy").format(order.date) // hand-made patternval total = "$" + String.format("%.2f", order.amount) // always dollarsval parsed = input.toDouble() // throws on "1.234,50" // Right: the locale formats; the data decides what is being formatted…How would you set up a localization pipeline so that translations landing days before a release do not break it?