Flutter Accessibility: Getting Started
Learn how to improve the accessibility of your Flutter app by providing more semantic details for screen readers and following other items from Flutter’s accessibility checklist. By Alejandro Ulate Fallas.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Flutter Accessibility: Getting Started
25 mins
- Getting Started
- Exploring the Starter Project
- Why Accessibility is Important
- The Built-in Flutter Accessibility
- Making Interactions Visible
- Testing With a Screen Reader
- Introducing The Semantics Widget
- Enabling the Screen Reader
- Adding Support for Screen Readers
- Using Semantics With Custom Widgets
- Using SemanticsService to Fill the Gaps
- Making SnackBars Accessible on iOS
- Considering Contrast Ratio and Color Deficiency
- Responding to Scale Factor Changes
- Testing Mealize’s Responsiveness
- Notifying Users on Context Switching
- Undoing Important Actions
- Where to Go From Here?
Testing With a Screen Reader
The next step is to do screen reader testing. To get an idea of how your app might feel for someone with vision impairments, you need to enable your phone’s accessibility features. If enabled, you’ll get spoken feedback about the screen’s contents and interact with the UI via gestures.
Flutter takes care of the heavy load since it enables the screen reader to understand most of the widgets on the screen. But, certain widgets need context so the screen reader can accurately interpret them.
Introducing The Semantics Widget
The Semantics
widget provides context to widgets and describes its child widget tree. This allows you to provide descriptions of widgets so that Flutter’s accessibility tools can get the meaning of your app.
The framework already implements Semantics
in the material
and cupertino
libraries. It also exposes properties you can use to provide custom semantics for a widget or a widget subtree.
But, there are times when you’ll need to add your own semantics to provide the correct context for screen readers. For example, when you want to merge or exclude semantics in a widget subtree, or when the framework’s implementation isn’t enough.
Enabling the Screen Reader
To enable your device’s screen reader, go to your phone’s settings and navigate to Accessibility. Then, enable TalkBack or VoiceOver if you’re using an iOS device.
Give the screen reader permission to take over the device’s screen.
By enabling TalkBack/VoiceOver, your navigation and interaction with the mobile phone will change. Here’s a quick rundown of how to use the screen reader:
- Tap once to select an item.
- Double-tap to activate an item.
- Drag with one finger to move between items.
- Drag with two fingers to scroll (use three fingers if you’re using VoiceOver).
Hot reload the app. Try using the app by opening a random meal and saving a couple of meals for later. Close your eyes if you want to experience total blindness while using the app. Here’s a preview:
Here’s what you may have experienced:
- When in the Saved Meals For Later screen, it’s not clear what Random Meal does.
- When in the Meal Detail screen, the screen reader refers to Save Meal for Later and Remove From Saved List icons as button. This is confusing.
- When in the Meal Detail screen, after tapping the Save Meal for Later icon button, VoiceOver (iOS) doesn’t read the snackbar.
- When in Meal Detail screen, after tapping the Remove From Saved List icon button, VoiceOver (iOS) doesn’t read the snackbar.
Adding Support for Screen Readers
OK, it’s time to add some Semantics
. Open lib/presentation/widgets/random_meal_button.dart and
in build
wrap FloatingActionButton
in Semantics
like below:
return Semantics(
// 1
button: true,
enabled: true,
// 2
label: 'Random Meal',
// 3
onTapHint: 'View a random meal.',
onTap: () => _openRandomMealDetail(context),
// 4
excludeSemantics: true,
child: FloatingActionButton.extended(
onPressed: () => _openRandomMealDetail(context),
icon: const Icon(Icons.shuffle),
label: const Text(
'Random Meal',
),
),
);
Here’s what’s happening in the code above:
- This tells screen readers that the
child
is a button and is enabled. -
label
is what screen readers read. -
onTapHint
andonTap
allows screen readers to know what happens when you tap Random Meal. -
excludeSemantics
excludes all semantics provided in thechild
widget.
onTap
when implementing onTapHint
. Otherwise, the framework will ignore it.If you’re using VoiceOver (iOS), you’ll notice there’s no change. That is because iOS doesn’t provide a way to override those values and thus it’s ignored for iOS devices. Also, onTap
supersedes onPressed
from FloatingActionButton
. So, you don’t have to worry about _openRandomMealDetail
executing twice.
Restart the app. Then, use the screen reader to focus on Random Meal and notice how the screen reader interprets the app:
You need to do something similar with MealCard
. See if you can implement Semantics
by yourself this time. You can find MealCard
in lib/presentation/widgets/meal_card.dart.
Need help? Open the spoiler below to find out how.
[spoiler title=”Solution”]
return Semantics(
button: true,
label: meal.name,
onTapHint: 'View recipe.',
onTap: onTap,
excludeSemantics: true,
child: Material(...),
);
[/spoiler]
Using Semantics With Custom Widgets
Sometimes, you use Semantics
to provide information about what role a widget plays, allowing screen readers to understand and behave accordingly.
You’ll use that for the meal heading. So, open lib/presentation/widgets/meal_header.dart, wrap Column
with Semantics
and set header
to true
like so:
return Semantics(
header: true,
child: Column(
...
),
);
This tells screen readers that the contents inside Column
is a header.
Hot reload. Navigate to MealDetailPage
using the screen reader. Confirm that the screen reader identifies it as a header. Here’s how the app is coming together:
showSemanticsDebugger
to true
in the app’s top-level MaterialApp
. Semantics Debugger shows the screen reader’s interpretation of your app.
Using SemanticsService to Fill the Gaps
Now, to finish adding screen reader support, open lib/presentation/widgets/meal_appbar.dart. Replace // TODO: Add Tooltip for Remove Meal
with this line of code:
tooltip: 'Remove from Saved Meals',
Do the same with // TODO: Add Tooltip for Save Meal for Later
— replace it with this:
tooltip: 'Save for Later',
As you might’ve known, tooltips in IconButton
s serve as semantic labels for screen readers. They also pop up when tapped or hovered over to give a visual description of the icon button — significantly improving your app’s accessibility and user experience.
Hot reload. Navigate to a meal and select the Save Meal for Later icon. Here’s what you’ll experience:
Now the screen reader knows appropriate labels for those buttons.
Making SnackBars Accessible on iOS
There’s still one issue you need to address, and it’s a platform-specific problem. For VoiceOver users, the snackbars aren’t read when they appear on the screen.
There is an issue about this on Flutter’s Github explaining reasons behind this behavior for iOS devices. For now, it’s safe to say you’ll need to use an alternative: SemanticsService.
SemanticsService
belongs to Flutter’s semantics
package, and you’ll use it to access the platform accessibility services. You shouldn’t use this service all the time because Semantics
is preferable, but for this specific case, it’s OK.
First, replace // TODO add directionality and semanticsLabel fields
in _onRemoveMeal
with the following:
final textDirectionality = Directionality.of(context);
final semanticsLabel = 'Removed $mealName from Saved Meals list.';
Second, replace // TODO add directionality and semanticsLabel fields
in _onSaveMealForLater
with:
final textDirectionality = Directionality.of(context);
final semanticsLabel = 'Saved $mealName for later.';
Don’t forget to add the corresponding imports at the top of the file:
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';
Flutter’s accessibility bridge needs context about the device’s textDirectionality
. That’s why you obtained it from the current context
.
Next, change Text
s in both _onRemoveMeal
and _onSaveMealForLater
snackbars to the following, respectively:
Text(
'Removed $mealName from Saved Meals list.',
semanticsLabel: semanticsLabel,
),
and
Text(semanticsLabel),
Then, replace // TODO: Add Semantics for iOS.
at the bottom of _onRemoveMeal
with the code below:
if (defaultTargetPlatform == TargetPlatform.iOS) {
SemanticsService.announce(
semanticsLabel,
textDirectionality,
);
}
Lastly, do the same with _onSaveMealForLater
:
if (defaultTargetPlatform == TargetPlatform.iOS) {
SemanticsService.announce(
semanticsLabel,
textDirectionality,
);
}
SemanticsService.announce
will use the platform-specific accessibility bridge in Flutter to read out semanticsLabel
. Then, you provide the device’s textDirectionality
to it since the bridge needs that. This ensures the screen reader announces the snackbar message on iOS in both cases.
Hot reload the app. If you’re using VoiceOver, you’ll now hear both snackbars when saving or removing a meal:
If you’re using TalkBack, you won’t notice any differences.
Great job! Android and iOS screen readers can now interpret Mealize. It was a challenge, but you knocked it out of the park. Congratulations!