Using the new accessibility actions API (without shooting ourselves in the foot)

We learned about a new API dropping in App Compat 1.1.0 soon to make it easier to add accessibility actions to our Views. This surfaces actions that would otherwise be difficult or impossible to use by Accessibility Services.

List of movies, “The Shawshank Redemption” selected by TalkBack. TalkBack is reading the available actions, which includes “mark watched”

The issue is that the API is not idempotent. Calling it multiple times on the same View will add the same action, multiple times:

The “Mark watched” action is added 4 times to the same View

This is very likely to occur:

  • in a RecyclerView, as we scroll through the list, data is bound to Views that were previously used
  • in a reactive architecture, whenever we receive a new data set, we rebind to the existing View hierarchy (in many cases)

How can we solve this?

In addition to the API to add accessibility actions, we also got some to replace and remove them. These sound perfect! We want to reset our View state before we bind any new actions.

Unfortunately, these two APIs require either the AccessibilityActionCompat or the ID of the action, neither of which are returned to us when we add an action.

Instead, we can use this extension function:

fun View.removeAccessibilityActions() {
    for (action in getActionList()) {
        ViewCompat.removeAccessibilityAction(this, action.id)
    }
}

private fun View.getActionList(): List<AccessibilityActionCompat> {
    return AccessibilityNodeInfoCompat.obtain(this)
            .apply { ViewCompat.onInitializeAccessibilityNodeInfo(
                this@getActionList,
                this
            ) }
            .actionList
}

This will:

  • get a list of accessibility actions
  • remove them one-by-one

Now we can reset the actions before we add them:

fun bind(filmUiModel: FilmUiModel) {
    ...
    itemView.removeAccessibilityActions()

    ViewCompat.addAccessibilityAction(
        itemView,
        filmUiModel.actionLabel()
    ) { view, arguments ->
        filmUiModel.onClickWatch()
        true
    }
}

And it’s fixed!


(In this example, the accessibility action is redundant because View clicks are already accessible (“Double-tap to activate”). For a real-life example of when accessibility actions are useful, see “Single action elements” in a previous article.)

I really like this API. It used to be such a faff to do this before—it was necessary to create an AccessibilityDelegate or override accessibility methods in the View you wanted to modify.

I hope there’ll be updates in 1.2.0 that make it easier to remove/replace actions but until then, it’s not so bad adding extension functions.

Let me know what you think and please share this post with others if you’ve learned something new!