Table of Contents
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 |
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 |
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 |
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