Implementation overview (II) – Application Not Responding

Photo by Gabriella Clare Marino on Unsplash

Introduction

In the previous article, we overviewed the principles of accessibility and introduced its corresponding Flutter API.

Today we’re going to explore different ways of adding a11y (short for accessibility) to our apps.

Brainstorming possible implementations

There are several approaches to tackle accessibility, such as:

  • some ad-hoc solution, adding the semantics widgets directly into our widget tree
  • creating custom accessible widgets
  • by using extensions
  • any mixture of the previous approaches

The following sections deep down into each one of these options.

1. Ad-hoc solution

This first implementation is pretty straightforward. We take advantage of the accessibility widgets and a11y properties in the Flutter API, applying them throughout the code of our app.

Say for example we want an image or icon with some alternative description. We simply use the corresponding semantics property:

const Icon(
   Icons.flutter_dash_rounded,
   size: 100,
   semanticLabel: "This image depicts the Flutter logo, Dash!",
)  

When dealing with widgets that have no semantics properties, we can nest them into a “Semantics” component:

return Semantics(
   value: "Welcome to the accessible app",
   child: MaterialApp(
      ....
   ),
);

So nothing fancy here, this solution is a no-brainer. Either way the following table recaps its advantages and disadvantages:

PROS CONS
Straightforward Verbose
Effective, gets the job done! Ad-hoc repetitive code across the app
Prone to error
Ad-hoc implementation

Can we do any better? Let’s see…

2. Custom widgets

The previous approach forces us to write a lot of (repeated) code. And, most of the times, more code means… more errors. We can improve it by creating reusable custom widgets that encapsulate the semantics for us, avoiding code repetition.

For instance, we can have our own custom “AccessibleText” class that automatically adds some accessibility properties to any text. Underneath, this class uses the Flutter API again, but we do not deal directly with it anymore, since the class offers its own API:

class AccessibleText extends StatelessWidget 
  final String text;
  final String? altDescrip;

  const AccessibleText(required this.text,   this.altDescrip) : super();

  @override
  Widget build(BuildContext context) 
    return Text(
      text,
      ...
      semanticsLabel: altDescrip ?? text,
    );
  
} 

In this solution, we get total control over the implementation and the API we offer to other classes. Moreover, all the details are encapsulated in our custom class.

Say we also want to make mandatory the alternative description property. We simply mark the corresponding parameter as required:

class AccessibleText extends StatelessWidget 
  final String text;
  final String altDescrip;

  const AccessibleText(required this.text, required this.altDescrip) : super();

  @override
  Widget build(BuildContext context) 
    return Text(
      text,
      ...
      semanticsLabel: altDescrip,
    );
  
 

Or maybe we want to modify the look and feel of the displayed text. By applying a default style or using the one we inject through the constructor, we get it done:

class AccessibleText extends StatelessWidget 
  final String text;
  final String altDescrip;
  final TextStyle? style;

  const AccessibleText(required this.text, required this.altDescrip, this.style, super.key) : super();

  @override
  Widget build(BuildContext context) 
    return Text(
      text,
      style: style ?? Theme.of(context).textTheme.bodyMedium,
      semanticsLabel: altDescrip,
    );
  
 

This solution simply applies some core principles of object-oriented programming (O.O.P.), like abstraction, encapsulation or inheritance. Either way, the following table summarizes this approach:

PROS CONS
Control over API and impl. Class explosion
O.O.P. principles “Expensive” implementation
Get the job done (again!) Higher level of abstraction
Custom widgets implementation

Not bad, but is there still room for improvement…?

3. Extensions… to the rescue

The former implementation would require a considerable amount of effort since we have to create a new set of accessible widgets. In some scenarios, it could be even unfeasible. So we need something “lighter”…

As you probably know, Flutter supports extensions by default. With extensions, we can add behaviour to classes, even if we do not “own” the class being extended.

Under the hood, extensions are implemented as static methods with fancy parameters, but the process is completely transparent for us.

Using extensions, for instance, we can customize the behaviour of the default string class. The required steps are:

extension CustomString on String 
   String capitalize()
      return  substring(0,1).toUppercase().
        substring(1).toLowercase();
   
  • invoke the method over a string object anywhere in our code:
"accessibility".capitalize() 

Looks good, doesn’t it? Applying the same idea, we can leverage the power of extensions to automatically wrap any widget inside a “Semantics” widget:

extension SemanticsExtension on Widget 
  Semantics semantics(required String descrip) => Semantics(
   value: descrip,
   child: this,
);

The main advantage of this approach is its convenience: it’s super simple to invoke the custom extension and apply accessibility to any component in our widget tree.

const Text(
   'Some text here...',
).semantics(
   descrip: "Some accessibility description..."),

As we did with the former approaches, let’s analyze this solution too:

PROS CONS
“Cheap” implementation Static resolution
Convenient
Concise syntax
Extensions implementation

Appendix I: testing accessibility

The flutter test package contains built-in features to make sure that our app complies with some of the most common accessibility guidelines.

Basically widget tests for a11y will look like normal UI tests, but using some new matchers in order to check if our widgets comply with the P.O.U.R. principles.

Apart from that, take into account that each platform (Android, iOS) has its own guidelines for accessible content, and those may be slightly different: one platform, for instance, may require a minimum size of 46px for tappable surfaces while the other demands 48px, and so on.

When testing accessibility, we use the “SemanticsHandle” class to access the data stored in the semantics tree. We can get an instance of it through the widget tester object and get rid of it when we no longer need it:

SemanticsHandle? handle;

testWidgets("Some accessibility test", (tester) async 
      handle = tester.ensureSemantics();

      ...

      handle?.dipose();

Inside a11y tests, some of the common matchers are:

  • tapTargetGuideline: ensure that tappable surfaces are big enough
  • textContrastGuideline: ensure that text of a certain size and color used is readable when used over a given background
  • labeledTapTargetGuideline: ensure that all actions have their corresponding descriptive labels

Appendix II: localized semantics

The intl package comes with support for accessibility out-of-the-box. Whenever we add a copy to the collection of strings of our app, we can also add the corresponding alternative description text.

Source code

Check the following repository for the full source code. It contains different branches named “feature/xxx_approach” that correspond to the implementations discussed in the article.

https://github.com/begomez/Flutter-Accessibility

References

https://docs.flutter.dev/development/accessibility-and-localization/accessibility

Next Post

Your Guide to Boosting Engagement

At times, a single tweet falls short of conveying the full breadth of your message. This is where Twitter threads come into play, allowing you to transcend the 280-character limit and reach a wider audience from the pool of 353 million Twitter users. But what is a Twitter thread, you […]
Your Guide to Boosting Engagement