DEV Community

loading...

Best practices to build accessible apps with Jetpack Compose

Fanny Demey
Freelance mobile dev (android & web) #tech4good #a11y / @GDGLille leader / @DevfestLille organizer / @TechEthicFR podcast host / french android group organizer
Updated on ・6 min read

Hello!
Here you are! You have finally decided to take a look at what it takes to make your android application accessible for people with visual impairment. First, that's great news ! You'll see that it's a rewarding path that you are about to take.

Today I'm going to share with you a list of best practices to make your app accessible specifically for people who are blind. I will show you how to do that with Jetpack Compose.

How does Talkback work ?

Talkback is the screen reader on android used by blind or low vision users.

Here are several gestures that you can perform anywhere on the screen, that allows you to navigate through each elements:

➡️: swipe to the right to go to next element

⬅️: swipe to the left to go to previous element

↕️: swipe up and down on the screen or swipe down and up, will open a menu. In this menu you can configure swipe up / swipe down gesture, for example: Navigation between headers, speech rate, read letter by letter (useful to read a password for example).

⬇️ or ⬆️: swipe up or down allows you to perform an action that you configured with the previous gesture.

👆: single tap on an element on the screen will give the focus to this element.

👆👆: double tap to activate an element for example a button.

When an element is focused, Talkback will give user informations about it. For example:

  1. its content
  2. its value: for example for an Edit text.
  3. Its type: "Button" "Edit box", "Tab", "List: 9 of 11 items".
  4. Its state: "Selected" for a tab for example.
  5. The action you can perform with it: "activate", "toggle", etc...

Best practices list

1. Add content description to images

Let's take for example a shopping app. In the following screen, we have an image showing a black t-shirt. If we do nothing, blind users would not know what this element is unless they perform 8 swipes to the right, to reach its label "T-shirt with cut-outs tied". So we should add a description to this image. (and all the other images on this screen)

Screenshot of a sample shopping application. Showing a black tee shirt but also buttons to pick another color. Then its price and a button Add to cart

Here is how to do that with Jetpack Compose.

Image( painter = rememberCoilPainter(product.url),
       contentDescription = product.contentDescription)
Enter fullscreen mode Exit fullscreen mode

The good news with Jetpack Compose is that contentDescription is mandatory!

2. Hide decorative images

When an element is purely decorative and doesn't bring any additional information to the user (for example a background, an image to illustrate a text that doesn't add any meaningful information to it), you can hide it so that Talkback will not give the focus on this element.

Image( painter = painterResource(R.drawable.background),
       contentDescription = null)
Enter fullscreen mode Exit fullscreen mode

3. Improve element’s description

In some situations the way that Talkback will read the element content will not be user friendly. Specifically in situations when we don't say something out-loud the same way that it's written.

For example: the daily rate for a car rental: $16/day

We will never say out loud: "Dollar sixteen slash day", but if we do nothing in our app, Talkback will. That's not a very nice user experience.

By overriding the text attribute of our Text composable using the Modifier.semantics property, we are able to improve the information read by Talkback.

@Composable
fun DailyRate(rate: Int){
   Text(text = “$rate/day,
        modifier = Modifier.semantics {
            text = AnnotatedString(“$rate dollars per day)
        }
   )
}
Enter fullscreen mode Exit fullscreen mode

4. Group elements together

In some situations, it can really improve the user experience to group element together. Imagine a screen with lots of elements on it. Imagine now that you have to go through every single element by swiping right. It's exhausting.

Sample of a mobile app showing the profile of a pet sitter

For example on this pet sitter profile screen. We can group together information like "no pets, home, car owner" to be focused and read all together. We can perform that by setting mergeDescendants to true in the Modifier.semantics properties of the main row.

Row (modifier = Modifier.semantics(mergeDescendants = true){}){
   Column {
       Icon(painter = painterResource(R.drawable.ic_pets),
           contentDescription = null)
       Text(text = stringResource(R.string.no_pet))
   }
   Column {
       Icon(painter = painterResource(R.drawable.ic_car),
           contentDescription = null)
       Text(text = stringResource(R.string.car_owner))
   }
   Column(/*...*/)
}
Enter fullscreen mode Exit fullscreen mode

5. Change reading order

Sometimes, it makes sense to influence the reading order of the element on the screen to improve the user experience and sometimes avoid having a feature completely inaccessible because the element is unreachable.
For example every time you design something on the z-index, let's say with a floating action button like the "Compose" button in gmail. If you do nothing, the focus to this button will be given after going through all the elements of the emails list behind (if the list is not infinite...).

image

There is no way to fix that with Jetpack Compose yet 😔... Here is an issue about that: A11y - Need a way to change manually focus order. Please comment or star the issue if you also think that it's important for the users.

To fix that without Jetpack Compose, you can use android:accessibilityTraversalBefore and android:accessibilityTraversalAfter properties to specify the element before and after the FloatingActionButton.

<com.google.android.material.floatingactionbutton.FloatingActionButton
   android:id="@+id/fabButton"  
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:src="@drawable/ic_pencil"
   android:contentDescription="@string/create_new_email"
   android:layout_gravity="bottom|right"
   android:accessibilityTraversalBefore="@id/mailList"
   android:accessibilityTraversalAfter="@id/profilePicture"
   />
Enter fullscreen mode Exit fullscreen mode

6. Facilitate navigation with headings

On the web, screen reader users are used to navigate quickly between headings (<h1>, <h2>, <h3>...). It gives them a quick overview of the content and help them to be more efficient.
Starting from Talkback 9.1, it's now also possible to do that in android apps, as long as the developer specifies it.

A good example to set up headings is when you have sections in a list. Like here in this settings screen:

Screenshot of the android settings to manage sound and vibration of the device. This screen has a list with different sections like Volume, Do not Disturb, Earphone

To specify that sections of that list are headings, we have to add heading() inside the Modifier.semantics property.

@Composable
private fun Section(text: String) {
     Text(text = text,
       modifier = Modifier.semantics { heading() }
   )
}
Enter fullscreen mode Exit fullscreen mode

7. Disable element and describe element state

Last situation I want to illustrate is the following: you have a list of items, and a switch button on each row of the list.

screenshot of an app. Setting's screen. List of setting, with on each row a text and a switch button at the end

If we do nothing Talkback will behave on each row like this:

  1. Focus the entire row and say "Profile button"
  2. Focus on the switch button: "On - Switch - Double tap to toggle"

This can be exhausting to go through a long list of configurable rows if Talkback stops twice per row.

The original composable will probably looks like this:

var checked by remember { mutableStateOf(true) }

Row{
   Text(text = "Profile details")
   Switch(checked = checked,
          onCheckedChange = { checked = !checked }
   )
}
Enter fullscreen mode Exit fullscreen mode

A better user experience, could be something like this on each row :

  1. Focus the entire row and say "Profile button - ON - Double tap to toggle"

We will do that in two step :

  1. Disable the focus on the Switch button
  2. Add toggle behaviour to the entire row

Let's go.

1) Disable the focus on the Switch button

We can do that by adding the Modifier.clearAndSetSemantics with nothing inside. This way talkback will ignore it.

var checked by remember { mutableStateOf(true) }

Row{
   Text(text = "Profile details")
   Switch(checked = checked,
          onCheckedChange = { checked = !checked },
          modifier = Modifier.clearAndSetSemantics {  }

   )
}
Enter fullscreen mode Exit fullscreen mode

Although using clearAndSetSemantics{} is the usual way to ask Talkback to ignore an element, keep in mind that in general when semantics properties can be changed by setting another default parameter of a composable, that should be preferred over using the lower level semantics modifier. In this situation, we can for example set null to onCheckChange callback, and the switch button will be ignored by Talkback too.

var checked by remember { mutableStateOf(true) }

Row{
   Text(text = "Profile details")
   Switch(checked = checked,
          onCheckedChange = null
   )
}
Enter fullscreen mode Exit fullscreen mode

2) Add toggle behaviour to the entire row

We add a toggleable Modifier to the row and perform in it the same action than on the switch.
We add in the semantics Modifier the stateDescription properties to configure what Talkback should say regarding the switch state.

var checked by remember { mutableStateOf(true) }

Row(modifier = Modifier
        .toggleable(checked) { checked = !checked }
        .semantics {  stateDescription = 
                       if(checked) "ON" else "OFF" }
){
   Text(text = "Profile details")
   Switch(checked = checked,
                   onCheckedChange = null

   )
}
Enter fullscreen mode Exit fullscreen mode

That's it

That's it for my list of best practices to make your app usable for blind users.
You can also check the Compose documentation for accessibility here : Jetpack Compose - Accessibility Documentation

Of course accessibility is not only about blind people, so I really encourage you to learn more about that. Here is where you can start : Mozilla website - what is accessibility.

I hope you enjoyed this article. Don't hesitate to reach out to me on Twitter if you have any questions : @FannyDemey

Discussion (5)

Collapse
giffesnaffen profile image
Trym Nilsen

Great article, Thanks. One experience I'd like to add to point 2 about decorative images that I have learned from interviews we have had with various users of accessibility services of our apps is to put the bar on what is deemed unimportant quite high. You add the illustrations for a reason, for example perhaps a happy one if an operation succeed, or a sad or empty illustration in your empty state. Some of the people we interviewed expressed that they also wanted the emotions these illustrations was intended to bring. The users with some, but very little vision could see that there was something there, but was left wonder what it was and also what it was illustrating.

Collapse
fannydemey profile image
Fanny Demey Author

Thanks for this really interesting feedback !

Collapse
zachklipp profile image
Zach Klippenstein

Great article, thanks!

FYI the google issue tracker link in the "Change reading order" section is broken (copy/pasting the URL in the text works though).

Collapse
fannydemey profile image
Fanny Demey Author

Thanks for noticing ! I'll fix that 🙂

Collapse
tkuenneth profile image
Thomas Künneth

Great article. Thanks for sharing.