Chapters

Hide chapters

Jetpack Compose by Tutorials

Second Edition · Android 13 · Kotlin 1.7 · Android Studio Dolphin

Section VI: Appendices

Section 6: 1 chapter
Show chapters Hide chapters

15. Accessibility in Jetpack Compose
Written by Denis Buketa

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Building accessible experiences is something every developer should strive for. Not only because you’ll make your app available to people with disabilities, but because you’ll also help people who might find them in short-term disability situations where those features might be useful.

Unfortunately, accessibility features are often skipped over when implementing mobile apps. In this chapter, you’ll learn how easy and simple it is to build accessibility features in Jetpack Compose.

Accessibility covers many things: impaired vision, color blindness, impaired hearing, cognitive disabilities, and temporary situations people find themselves in. It’s impossible to cover all those cases in this chapter, but you’ll have a chance to implement the most common use cases step by step.

You’ll learn how to:

  • Correctly implement touch target size.
  • Add visual element descriptions.
  • Add click labels.
  • Create custom actions.
  • Make it easier to navigate by using headings.
  • Merge multiple elements in one semantic unit.
  • Lift toggle behavior from switches and checkboxes.
  • Define state descriptions.

For testing the code, you’ll use TalkBack — accessibility tools used by people with visual impairments.

Note: If you are not familiar with TalkBack, please refer to the official documentation: https://support.google.com/accessibility/android/answer/6283677?hl=en.

It is important to emphasize that TalkBack might behave differently depending on the device and TalkBack version you have installed. You can check the version in the TalkBack settings and you can also check controls for your version if you go to TalkBack settings and check Customize gestures or Customize menu options. There is also an option that allows you to practice the gestures to get used to them.

If you open Customize gestures screen, you’ll be able to see all the gestures you can do: 1 finger gestures, 1 finger angle, 2 fingers, etc. In this chapter, we use multi-finger gestures for some actions.

If your TalkBack version doesn’t support multi-finger gestures, you should be able to find an alternative within your available options. For example, 1 finger angle Swipe up then right gesture and Tap with 3 fingers have the same result: Open TalkBack menu. We’ll remind you of that for examples where this is important.

Customize gestures
Customize gestures

Semantic Properties

Before you jump straight into coding, you should understand the basic principles of how accessibility services interpret UI elements. Accessibility services need information about UI elements to understand the UI. Jetpack Compose defines that information using semantic properties.

Some composables can define that information by interpreting their children composables. Other composables might use some modifiers that fill that information. And in some cases, there isn’t enough information, so developers need to fill it using semantic properties.

In this chapter, you won’t go into more details about Compose semantics, but you’ll learn how to use them to manually fill accessibility information.

Note: If you want to learn more about Compose semantics, check out the official documentation: https://developer.android.com/jetpack/compose/semantics.

Implementing Accessible Touch Target Size

To follow along with the code examples, open this chapter’s starter project in Android Studio and select Open an existing project.

Post Screen
Dalw Hkpuix

JetReddit with TalkBack
MipRapmax qalb WespGubn

Icon(
  modifier = Modifier
    .clickable {
      context.startActivity(
        Intent(context, ChatActivity::class.java))
    }
    .padding(12.dp)     // HERE
    .size(24.dp),
  imageVector = Icons.Filled.MailOutline,
  tint = Color.LightGray,
  contentDescription = null
)
IconButton(onClick = {
  context.startActivity(
    Intent(context, ChatActivity::class.java)
  )
}) {
  Icon(
    Icons.Filled.MailOutline,
    tint = Color.LightGray,
    contentDescription = null
  )
}
Chat icon with good touch area
Nray enos yesk vuoz gauxw ikau

Adding Visual Element Descriptions

In this section, you’ll further improve the Chat icon. Visual elements like Image() and Icon() have contentDescription parameters for which you can pass the text used by accessibility services to describe what that element represents.

IconButton(onClick = {
  context.startActivity(
    Intent(context, ChatActivity::class.java)
  )
}) {
  Icon(
    Icons.Filled.MailOutline,
    tint = Color.LightGray,
    contentDescription = "Navigate to chat"
  )
}

Adding a Click Label

In the previous section, you added a description to the Chat icon that explains what it represents. In this section, you’ll add a description of what happens when the user clicks on it.

IconButton(
  modifier = Modifier.semantics { // HERE
    onClick(label = "open Chat", action = null)
  },
  onClick = {
    context.startActivity(
      Intent(context, ChatActivity::class.java)
    )
  }
) {
  Icon(
    Icons.Filled.MailOutline,
    tint = Color.LightGray,
    contentDescription = "Navigate to chat"
  )
}
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
Accessible Chat Icon
Epfiptiqzi Gcaw Uzew

Icon(
  modifier = Modifier
    .clickable(
      onClickLabel = "open Chat"   // HERE
    ) {
      context.startActivity(
        Intent(context, ChatActivity::class.java)
      )
    }
    .padding(12.dp)
    .size(24.dp),
  imageVector = Icons.Filled.MailOutline,
  tint = Color.LightGray,
  contentDescription = "Navigate to Chat"
)

Implementing Custom Actions

Next, you’ll focus on making your posts accessibility friendly. If you try swiping right to navigate through posts, you’ll notice many actions you must go through before TalkBack focuses on the next post.

Focusable elements in Post
Kafucezhi inedofhp ez Yepk

@Composable
fun MoreActionsMenu() {
  var expanded by remember { mutableStateOf(false) }

  Box(modifier = Modifier
    .wrapContentSize(Alignment.TopStart)
    .clearAndSetSemantics { }               // HERE
  ) {
    ...
  }
}

// Rest of Post.kt content
...

@Composable
fun PostActions(post: PostModel) {
  Row(
    modifier = Modifier
      .fillMaxWidth()
      .padding(start = 16.dp, end = 16.dp)
      .clearAndSetSemantics { },            // HERE
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically
  ) {
    ...
  }
}
@Composable
fun JoinButton(onClick: (Boolean) -> Unit = {}) {

  ...

  Box(
    ...
  ) {
    Row(verticalAlignment = Alignment.CenterVertically) {
      Icon(
        imageVector = iconAsset,
        contentDescription = null,
        tint = iconTintColor,
        modifier = Modifier
          .size(16.dp)
      )
      Text(
        text = "Join",
        color = Color.White,
        fontSize = 14.sp,
        maxLines = 1,
        modifier = Modifier
          .widthIn(
            min = 0.dp,
            max = textMaxWidth
          )
          .clearAndSetSemantics { }             // HERE
      )
    }
  }
}
import androidx.compose.ui.semantics.clearAndSetSemantics
Post without focusable elements
Hasc lalboaf novegikso elexihmx

@Composable
fun Post(
  post: PostModel,
  onJoinButtonClick: (Boolean) -> Unit = {},
  onPostClicked: () -> Unit = {},
  content: @Composable () -> Unit = {}
) {
  Card(
    shape = MaterialTheme.shapes.large,
    onClick = { onPostClicked.invoke() },
    modifier = Modifier.semantics { // HERE
      customActions = listOf(
        CustomAccessibilityAction(
          label = "Join",
          action = { /* Join / Leave */ true }
        ),
        CustomAccessibilityAction(
          label = "Save post",
          action = { /* Save post */ true }
        ),
        CustomAccessibilityAction(
          label = "Upvote",
          action = { /* Upvote */ true }
        ),
        CustomAccessibilityAction(
          label = "Downvote",
          action = { /* Downvote */ true }
        ),
        CustomAccessibilityAction(
          label = "Navigate to comments",
          action = { /* Navigate to comments */ true }
        ),
        CustomAccessibilityAction(
          label = "Share",
          action = { /* Share */ true }
        ),
        CustomAccessibilityAction(
          label = "Award",
          action = { /* Award */ true }
        )
      )
    }
  ) {
    ...
  }
}
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.CustomAccessibilityAction
Post Custom Actions
Kolf Voqfas Icbaagk

Navigating Through Headings

In this section you’ll again work on improving a specific navigation experience in the app. Open the app, focus the first post by tapping on it, and double tap on the screen to open a Post screen. As mentioned at the beginning of this chapter, this was added for learning purposes and it only works for the first post in the list.

Post Screen
Webx Ctzaur

Configuring TalkBack Reading Control

TalkBack allows you to navigate by headings. Before you continue updating the code, make sure that you configure the TalkBack to navigate by headings.

Selecting Heading Reading Controls
Qumipnokd Foocibp Mooguqv Dozywotx

Adding Headings

After you’ve done that, try to swipe down with one finger and notice the TalkBack response: No next heading. That means that there are no headings configured on that screen.

@Composable
private fun TitleSection() {
  Text(
    text = "Check out this new book about Jetpack Compose from Kodeco!",
    color = colors.primaryVariant,
    fontSize = 18.sp,
    modifier = Modifier
      .padding(horizontal = 16.dp)
      .semantics { heading() }               // HERE
  )
}

@Composable
private fun SectionDescriptor(text: String) {
  Text(
    text = text,
    color = Color.Gray,
    fontSize = 14.sp,
    modifier = Modifier
      .padding(horizontal = 16.dp)
      .semantics { heading() }               // HERE
  )
}
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
Post Screen Headings
Lezq Txteom Ceubuqzc

Custom Merging

You’ve probably noticed that when it comes to accessibility, you try to minimize the number of focus changes while keeping the same context. In this section, you’ll learn how to merge composables in one semantic unit so that your users don’t have to go through each element.

@Composable
private fun AuthorSection() {
  SectionDescriptor(text = "Author")
  Row(
    modifier = Modifier
      .padding(start = 16.dp)
      .semantics(mergeDescendants = true) { }           // HERE
    ,
    verticalAlignment = Alignment.CenterVertically
  ) {
    ...
  }
}

Lifting Toggle Behavior

In this section, you’ll learn how to lift toggleable state up from the toggle composable itself to its containing composable.

Subreddits Screen
Tarlekxajc Hpkiin

@Composable
fun Community(
  text: String,
  modifier: Modifier = Modifier,
  showToggle: Boolean = false,
  onCommunityClicked: () -> Unit = {}
) {
  var checked by remember { mutableStateOf(true) }

  val defaultRowModifier = modifier
    .padding(start = 16.dp, end = 16.dp, top = 16.dp)
    .fillMaxWidth()

  val rowModifier = if (showToggle) {       // HERE
    defaultRowModifier
      .toggleable(
        value = checked,
        onValueChange = { checked = it },
        role = Role.Switch
      )
  } else {
    defaultRowModifier.clickable { onCommunityClicked.invoke() }
  }

  Row(
    modifier = rowModifier,                // HERE
    verticalAlignment = Alignment.CenterVertically
  ) {
    ...

    if (showToggle) {
      Switch(
        checked = checked,
        onCheckedChange = null             // HERE
      )
    }
  }
}
import androidx.compose.foundation.selection.toggleable
import androidx.compose.ui.semantics.Role
Toggleable Subreddit
Xindpuuqzi Cadvoxhuc

Adding State Descriptions

In the previous section you lifted the toggleable state. You can further improve the user experience by adding a better state description.

@Composable
fun Community(
  text: String,
  modifier: Modifier = Modifier,
  showToggle: Boolean = false,
  onCommunityClicked: () -> Unit = {}
) {
  var checked by remember { mutableStateOf(true) }

  val defaultRowModifier = modifier
    .padding(start = 16.dp, end = 16.dp, top = 16.dp)
    .fillMaxWidth()

  val rowModifier = if (showToggle) {
    defaultRowModifier
      .toggleable(
        value = checked,
        onValueChange = { checked = it },
        role = Role.Switch
      )
      .semantics {
        stateDescription = if (checked) {        // HERE
          "Subscribed"
        } else {
          "Not subscribed"
        }
      }
  } else {
    defaultRowModifier.clickable { onCommunityClicked.invoke() }
  }

  ...
}
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription

Key Points

  • When implementing screen elements that the user can interact with, think about its size. Their width and height should be at least 48dp.
  • You should use the contentDescription to add more information to visual elements for users who might not be able to see or interpret them.
  • Adding a click label adds more context about what will happen when the user interacts with the screen elements.
  • Custom actions make your app easier to navigate using a screen reader and group common actions for specific screen elements.
  • Defining semantic headings allows users to quickly jump between sections on your screens.
  • You can use custom merging when screen elements can be grouped into one logical unit.
  • By lifting the toggle behavior from some screen elements, you add more context to the toggleable element.
  • State descriptions can further add more context to toggleable elements.

Where to Go From Here?

Congratulations, you just completed the Accessibility in Jetpack Compose chapter!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now