Accessibility is important for your applications and your thought process as a developer. It sharpens the knife by forcing you to think through how your app is experienced by new form factors and engages you in user experience in ways that improve the overall experience for everyone. There is plenty of data that proves accessibility is valuable and here to stay, so as Android developers we need to grow our understanding and expertise of how to use the tools (or lack thereof) in the ecosystem to make our apps accessible.
Android accessibility is hard because the specifics of how to make something accessible for the phone and the code implementations can feel confusing or even counterintuitive at times. AccessibilityDelegate
overrides, api gated featuresets, list item announcements and complex actions have been a challenge to implement well, until now. Jetpack Compose treats accessibility as a first class citizen in its toolkit, and it makes making our applications accessible a breeze. Let’s take a look at what we used to do, versus what we can do now.
Old vs. New
As we know, Jetpack Compose is Android’s latest method for creating user interfaces. It is exceptionally different from the previous upgrades in the ecosystem, like the incremental upgrades of RelativeLayout
and ConstraintLayout
, because it is not written in XML. Compose is a declarative UI written entirely in code. This simplifies and accelerates app development in many ways, but today we’re focusing on how it improves accessibility minded development, because Compose is an accessibility-first language and it shows.
Content Description
Frequently, when writing an ImageButton, the AndroidStudio IDE gives developers a warning about how they’re missing the contentDescription property on the image.
A contentDescription
is what Talkback announces to users when Talkback focus scans over an ImageButton
. Without a contentDescription
, all that Talkback can announce is “button”, which is not helpful to non-sighted users. Obviously, developers should follow the linter warning’s advice and implement a contentDescription
, but developers can and do ignore warnings all of the time. If they ignore this warning, they have left many of their Talkback users confused about functionality in their application.
In Compose, if you implement an equivalent to the ImageButton
, an Icon, the contentDescription
property is mandatory. This is great because it puts accessibility first, as contentDescription
does not benefit non-accessibility technology users, and also because the mandatory implementation is flexible.
@Composable fun popupCloseButton(closePopUp: () -> Unit) { IconButton( onClick = { closePopUp() } ) { Icon ( imageVector = Icons.Filled.Close, contentDescription = stringResource(R.string.close_popup) ) } }
On top of enforcing an announcement from a string resource, developers can fall back on setting the contentDescription of their Icon to null. This reads like a problem, until you try to focus it with Talkback and the Icon is ignored in the same way a decorative image is ignored! Android has decorative images now! No need to write importantForAccessibility=no
, focusable=false
, and clickable=false
to achieve the same effect. Simply set the contentDescription
to null
and you’re done.
Icon ( imageVector = Icons.Filled.Close, contentDescription = null )
Accessibility Heading
Let’s say you’re targeting the maximum number of users and supporting api 21+, which nets you 92.3% of Android users as of today. This is great, but as we know it means you’re going to have to wrap some of your code in api specific annotations/if statements to maintain backwards compatibility with the older apis. How about when you want to take advantage of the snazzy new accessibilityHeading
property, though?
Accessibility heading is a wonderful feature that improves navigation for users by allowing them to scan through applications by heading only. This is a feature many accessibility technology users expect, because it has existed for them on the web and desktop for many years. To implement this in Android, the user’s phone must be running api version 28 and up. In xml, that means you need to create a duplicate layout file that has accessibilityHeading
properties on all of your headings. That’s not a huge maintenance problem, right? Except, what if you also need a foreground drawable? Now you need a layout-v23 folder because that feature isn’t supported below api 23, and you can see the growing problem. Every time you want to make a change to your layout with a header, you need to modify the file in every place.
It is difficult to maintain a complex net of user needs with xml file folders. Compose absolutely obliterates any need for that maintenance headache by bringing the view right into the code and wrapping the heading functionality with a backwards-compatible semantics function.
@Composable fun header() { Text( text = stringResource(R.string.header), modifier = Modifier.semantics { heading() } ) }
By invoking heading()
inside of the Semantics object, you’ve got your backwards compatible accessibility heading. No if statement wrappers, no api annotations, just an invocation and your app is now heading navigable.
Accessibility Actions
When using Talkback, “Double tap to activate” has to be the most common announcement in any given application. It is the default announcement a user gets on an interactive widget, providing the minimum information they need to interact. This announcement works, but can be vastly improved upon with modification by providing the announcement with more context specific string data. Let’s say you want a button to archive an email–you can write code to change “Double tap to activate” into “Double tap to archive email.”
class AccessibilityNodeInfoExt : AccessibilityDelegateCompat() { override fun onInitializeAccessibilityNodeInfo( host: View?, info: AccessibilityNodeInfoCompat? ) { super.onInitializeAccessibilityNodeInfo(host, info) val action = AccessibilityNodeInfoCompat.AccessibilityActionCompat( AccessibilityNodeInfoCompat.ACTION_CLICK, host?.context?.getString(R.string.archive_email) ) info?.addAction(action) } }
This function requires an overwritten AccessibilityDelegateCompat.onInitalizeAccessibilityInfo(...)
on an AccessibilityDelegate
, which must then be passed to the view in question as its delegate. In Compose, the process is a little simpler thanks to the onClickLabel
property.
Row( Modifier = Modifier .clickable( onClick = { onEmailClicked(email.id) }, onClickLabel = stringResource(R.string.archive_email) ) ) { … }
The previously arcane and mysterious action labels are now accessible through a simple semantics modifier onClickLabel
. This code accomplishes everything the old implementation above does in one line.
Grouping Announcements
We’ve all implemented a list of items with many little details that a user is probably going to ignore. Items like these really only deserve one announcement because the user prefers to glaze over them to get to a specific choice. This behavior is expected and fair with a scroll, but becomes far more difficult for the end-user when they are navigating with Talkback or SwitchAccess. Take the image below for example:
Should the above example email widget focus the “from” and “title” fields separately? I don’t think so, because that doubles the amount of interactions Talkback and SwitchAccess users are required to make in order to scroll through to find the email they actually care about. If developers leave each item focusable in a list view with dozens, hundreds, or infinite potential items, how can an assistive technology user be expected to scroll through the items in any reasonable amount of time?
The solution to make list scrolls usable with accessibility technologies is to take our item views individual widgets and combine them into sensible focusable groups. This has always been an effort in hacking the ecosystem to work across Linear, Relative, and Constraint layouts, but no longer. Android developers have an unbelievably easy solution to this problem now, referred to as merging by Compose, using the Semantics property mergeDescendants
.
@Composable fun emailRow(onEmailClicked: (Int) -> Unit) { Row( modifier = Modifier .semantics(mergeDescendants = true) {} ) { Text(text = stringResource(R.string.who_from)) Text(text = stringResource(R.string.email_title)) } }
The mergeDescendants
property being set to true groups all of the row’s children as one announcement automagically. It is a thing of beauty.
Buy our stuff
Compose is impressive, not because it is a new paradigm for creating UIs, but because it is clear that the development team behind the library deeply understands the problems Android developers frequently need to solve. I bet the old code solutions made your head ache a little, but at least you got to see them side by side with cleaner, more stable examples.
Let’s be honest here, did you know about the problems in this blog, the ones that I’m excited that Compose has solved? Working through the Android accessibility space can be pretty puzzling, but it is our lifeblood here. We’re excited about Compose because it is going to make all Android accessibility development easier, but it’ll also make our jobs easier because we can skip past the small things and get folks to total app accessibility much faster. If any of this information intrigues or excites you, there’s a whole world of it, and we’d be happy to help guide you through.
Fill out this form if you want one of my Sales colleagues to reach out to help advance your accessibility efforts. They will happily sell you tools, services or training – all of which are pretty excellent here. You can learn more about Android accessibility stuff on deque.com’s Android Accessibility page.