View non-AMP version at deque.com

Google recently came out with Jetpack Compose, a modern Android UI toolkit for building native Android UI faster and easier by using intuitive declarative Kotlin APIs. This means no more XML type of view coding but instead coding views using declarative APIs.

Jetpack Compose gives developers the ability to programmatically generate complex themes and UI components while avoiding the boilerplate that XML requires. With that being said Jetpack Compose comes with its own custom-built accessibility APIs. Here are some advantages of using Jetpack Compose:

  • Faster and easier Android UI development (especially for developers like me!)
  • Intuitive Kotlin APIs
  • Easy to catch bugs in UI
  • Less code (Who doesn’t like that?)
  • Built-in support for Material Design, Dark Theme, and Animation
  • Easy to create reusable UI components.

Imperative or XML type of view building:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button"
    android:text="Show Dialog"
    app:layout_constraintBottom_toTopOf="@+id/gone_btn"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

Declarative type of view using Jetpack Compose:

@Composable
fun HeadingLarge(text: String, color: Color = Color.Black, modifier: Modifier = Modifier) {
    Text(
     text = text,
     fontSize = 30.sp,
     fontFamily = FontFamily.Cursive,
     textAlign = TextAlign.Center,
     fontWeight = FontWeight.Bold,
     color = color,
     modifier = modifier
   )
}

In this part of the series, we will learn how Jetpack Compose behaves with Accessibility Service in Android and how it differs from how Imperative or XML type of view interacts with Accessibility Service.

How do Accessibility Services work in Android?

An Accessibility Service is a service in Android that, when turned ON, runs in the background and responds to the Accessibility Events like tap, focus, tap and hold, two finger swipe, swipe right, etc. The screen/window content is arranged in a tree and each node in the tree is represented as an AccessibilityNodeInfo. Each AccessibilityNodeInfo contains information like Name, Role, Value, and Action associated with that particular view.

Source:blog.intuit.com

How does the Imperative/XML type UI interact with Accessibility Services?

In Imperative/XML views, the accessibility service tries to gather accessibility-related information by visiting each and every element in the view hierarchy. For example:

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button"
    android:text="Show Dialog"
    app:layout_constraintBottom_toTopOf="@+id/gone_btn"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

For the above button, the Accessibility Service tries to determine what to announce based on the class to which the view belongs, the text on the view (“Show Dialog”), and some more information like boundsInScreen, etc. Finally, the Accessibility Service gathers enough data to announce “Show Dialog, Button, Double Tap to activate.”

How does the Declarative type UI interact with Accessibility Services?

Things are a bit different in Declarative views as compared to Imperative views. Jetpack Compose uses a tree of values outlining the Semantics properties of your views. This makes it easy for the Accessibility Service to determine which data to collect; however, it does not define information on how the composable will be drawn. For example:

Button(
        onClick = { Toast.makeText(context, "This is an Accessible Button!", 
Toast.LENGTH_SHORT).show() },
        modifier = Modifier.constrainAs(example1) {
          top.linkTo(divider1.bottom, margin = 10.dp)
          start.linkTo(parent.start)
          end.linkTo(parent.end)
        }
) {
   Text(text = "Accessible")
}

A simple button in Jetpack Compose automatically supplies appropriate semantics to the Accessibility Service which contains all the vital information like Name, Role, Value and State of the composable.

For a more customized composable, Jetpack Compose allows the developers to set the semantics manually, for example:

Row(
modifier = Modifier.border(width = 2.dp, color = Color.Black)
.clickable(onClick = {
Toast
.makeText(context, "You have clicked on the item!", Toast.LENGTH_SHORT)
.show()
})
.padding(16.dp)
.semantics {
// Here I am telling the Accessibility Service
// to treat this Row like a Button
role = Role.Button…

In the example above, the Row is acting like a Button. By setting the Semantics property “role” to button we are telling Accessibility Service to treat this row like a button and add the action “Double Tap to Activate” to it.

As accessibility-related information is collected from each and every element on the screen, it is simultaneously arranged in a tree called the Accessibility Node Info Tree. Jetpack Compose makes it easy for the Accessibility Service to create the Accessibility Node Info Tree because it already maintains a semantics tree.

Source: https://developer.android.com/jetpack/compose/semantics

Compose allows us to look into the composition hierarchy or the semantics tree, which will be covered in part two of this blog series.

Merged and Unmerged Composition Hierarchy in Jetpack Compose

Jetpack Compose creates two types of composition hierarchies for each composable. Consider a simple compose button:

This is how the above button is coded using declarative APIs:

Button(
     onClick = { navController.navigate(destination) },
     modifier = modifier
) {
Text(text = text, fontSize = 18.sp)
}

Jetpack Compose creates two types of semantics composition hierarchy for this button:

Merged Composition Hierarchy

Unmerged Composition Hierarchy

Upon closer look, we can see that Unmerged Composition Hierarchy has Text in a separate Node, whereas in Merged Composition Hierarchy, Text is part of the same node.

The important thing to note is that the Accessibility Service gathers information from the Merged Composition Hierarchy. The Unmerged Composition Hierarchy gives deeper insight into how the elements are arranged within the hierarchy, and the Merged Composition Hierarchy gives insight into how the Accessibility Service is going to treat that particular composable.

Summary

In part one of this blog series on building accessible android apps with Jetpack Compose, we learned how Accessibility Services in Android interacts with Jetpack Compose as compared to Imperative or XML type of views. In part two, we will learn how to make basic Jetpack Compose elements like Button, Switch, TextField, etc. accessible to Accessibility Technology users using simple real-time code examples, and also we will see how to grab composition hierarchy using automated tests in Jetpack Compose.

Exit mobile version