One goal of the IRMA made easy project is to make IRMA easy for everyone, including users with disabilities. So, when the plan to develop a new IRMA app took shape in 2019, we set accessibility as a primary goal and got to work. By the time the new IRMA app was released in June 2020, we had implemented and tested many accessibility features. However, the journey did not end here. We were lucky that the NLnet project provided us with an accessibility audit by Stichting Accessibility. Once the app was out there, it was evaluated with regards to accessibility by experts in the field. Unsurprisingly, it turned out that we had missed a few issues and that some aspects still could be improved. To make sure other app developers (especially those using Flutter!) can learn from our initial mistakes, we decided to document the improvements suggested by the audit. On this project blog, you can expect a series of shorter posts, each explaining one type of problem that was identified in the audit as well as how we fixed the problem in the IRMA app. In this first post, we provide a bit of background information on Flutter and accessibility and then show you how we improved the headings in the IRMA app based on the feedback we got. We assume you know the basics of Flutter, and that you have some hands-on experience with the IRMA app. If this is not the case, you can head to the Flutter website to learn about Flutter and download IRMA via irma.app.

Flutter and accessibility: you need to test, test and test!

The new IRMA app is built with Flutter. Luckily, Flutter comes with powerful accessibility features baked-in and there are many valuable resources that support developers with using these features. Three great introductions are the Semantics article by Didier Boelens (2018), a guide to Flutter Semantics by Krzysztof Sroka (2019), and the Building in Accessibility with Flutter video from the Flutter Interact ‘19 conference. As also mentioned in the video, one of the crucial steps in building an accessible app is testing. Ideally, you can test with users – including users with disabilities. But before you do this, you should always try to find as many issues as possible yourself. After all, you do not want to bother users with obvious problems that you could have easily found yourself. For us, accessibility testing included regularly walking through the app with TalkBack on Android and VoiceOver on iOS, as well as with a vastly increased font size. We did this to learn how visually impaired users would experience the app.

Testing the app this way allowed us to spot and consequently fix various issues. A common problem was, e.g., overflow issues with the text being cut off or falling out of the screen due to its increased size. Other common issues included missing semantic information on the one hand and undesired information being presented by screen readers on the other hand. In addition, we also encountered more ‘mysterious’ and unexpected issues, such as text not scaling at all or invisible elements being pointed out by the screen reader.

Most issues that we encountered during testing could easily be solved. Missing semantic information, for instance, could be quickly added by wrapping the problematic widget in a Semantics widget. The Semantics widget was, e.g., used to wrap self-made buttons and indicate that these custom buttons are actual buttons. Another common fix involved setting the excludeFromSemantics: true property on images that were solely decorative and did not add any actual information. By setting this property, we prevented VoiceOver and TalkBack from telling users about these images. In contrast, images and icons that actually served a function were annotated by setting the semanticLabel property. For instance, using this label, a chevron-icon pointing downwards was annotated with the term “next” to indicate that this icon allows users to proceed to the next screen.

Testing the app ourselves allowed us to find and solve a lot of issues up front. However, once you know an app inside and out, it can be difficult to put yourself into the shoes of new users – especially if the users you have in mind are very different from yourself and navigate the app with their ears. We thus were extremely happy that Stichting Accessibility provided additional pairs of eyes and ears to evaluate the app. We even received two evaluations, one for Android and one for iOS.

Identified issues

The accessibility reports we received alerted us to a number of remaining issues, which we either had overlooked or simply not had found the time to fix yet. In total, about 40 possible improvements were mentioned, but some issues were mentioned both by the iOS and by the Android report. Various different types of issues were revealed. For instance, the reports identified focus order issues, contrast issues, missing labels, missing information about non-text content, issues with skipped texts, and issues with purely decorative images being mentioned by the screen readers.

Handling headings

One specific issue that was mentioned in the report concerned the way headings were read on iOS. While VoiceOver typically adds the word “heading” after reading aloud headings, titles or headers, it did not always do this for our app. In fact, it only added the word “heading” for the titles that we had placed in our app bar. For app bar titles being read as headings, we had not done anything ourselves. Thanks to Flutter, it just worked out of the box. However, for all other headings, apparently, additional steps were necessary.

Because we were curious if the same issue existed on Android, we immediately tried to reproduce the problem on an Android phone as well. As it turned out, no text was explicitly read aloud as a heading when trying several apps on our approximately five-year-old Samsung Galaxy S6 edge. However, on the emulator, with recent versions of Android, the heading-problem could be reproduced.

After reproducing the issue both on iOS and Android, we sought out a solution that would solve the issue on both platforms. Luckily, it turned out that there is an easy fix…

Semantics to the rescue

To ensure all headings are actually described as headings by VoiceOver and TalkBack, only a single tiny change is necessary: One needs to wrap the Text widgets that contain the heading-texts with a Semantic widget. On the Semantic widget, one can set the header property to true. The general structure then looks like this:

Semantics(
  header: true,
  child: Text(
    title,
    style: Theme.of(context).textTheme.headline3,
  ),
),

In this specific use case, we use the Semantics widget to describe the text as a heading. However, the Semantics widget is generally an ideal solution for adding such semantic information to elements.

Unfortunately, while adding the Semantics widget to our heading-texts was a simple fix, we still had to hunt down all the headings in our app more or less manually. What helped here is that our headings usually used the same large TextStyle. To be specific, the headings typically used Theme.of(context).textTheme.headline3 – so they could easily be found.

A more challenging issue was the decision of what actually counted as a heading. Specifically, we wondered: should titles in a confirmation-dialogue be indicated as a heading? For consistency reasons, we opted to do things the same way Apple does. We checked how their system-dialogues are read aloud and found that titles in confirmation dialogues are not read as headings.

We were happy with the resulting code, but still worried that developers might forget to wrap future headlines with a Semantics widget. The IRMA-team agreed and suggested implementing a solution that would not require them to take the same additional action (wrapping the text with the Semantics widget) every single time a heading is introduced. We followed this advice and implemented our own Heading widget.

The resulting Heading widget works just like a normal Text widget, but automatically marks the text as a heading. Consequently, all the developers need to remember now is to use the Heading widget instead of the Text widget whenever they are introducing a heading. To make things even more convenient, we did set a default TextStyle for headings. So, unless specified differently, all headings automatically will have the same consistent look and feel.

The Heading class has nothing IRMA-specific about it and might be useful to others who want to develop accessible Flutter apps. This is how it looks.

import 'package:flutter/material.dart';

class Heading extends StatelessWidget {
  final String title;
  final TextStyle style;
  const Heading(this.title, {this.style});
  @override
  Widget build(BuildContext context) {
    return Semantics(
      header: true,
      child: Text(
        title,
        style: style ?? Theme.of(context).textTheme.headline3,
      ),
    );
  }
}

You might want to adapt this Heading widget depending on your use cases, e.g., by adding a textAlign property to support both centered and left-aligned headings. Ideally, you can use the Heading widget exactly like you use the Text widget. The code above works well for us in this regard. Have a look at an example of how our code looked before and after implementing the change. This is how headings looked when they were not properly read out by screen readers, before our fix:

Text(
  FlutterI18n.translate(context, 'about.header'),
  style: Theme.of(context).textTheme.headline3,
)

This is how the code looks now, after implementing the improvements.

Heading(
  FlutterI18n.translate(context, 'about.header'),
  style: Theme.of(context).textTheme.headline3,
)

As you can see, we only have to change the word “Text” to the word “Heading” and it all just works. And as a nice bonus, we can even leave out the style when introducing new headings. So, all we are left with is:

Heading(
  FlutterI18n.translate(context, 'about.header'),
)

A nice side effect of using a Heading widget like this is that the code becomes even more readable, making Headings stand out from other forms of text also in the code.

Summary and outlook

The new IRMA app uses the Flutter framework, which comes with many powerful accessibility features built-in. Using Flutter, however, does not mean that everything will be perfectly accessible right out of the box. To ensure the accessibility of an app, testing is key and likely will reveal quite some issues! Unfortunately, finding accessibility issues becomes more and more difficult once you know your own app inside and out. Having someone else have a look – or listen – to your app can provide much valuable additional feedback. We were lucky and got an audit by Stichting Accessibility, which revealed quite some remaining points for improvement. In this post, we showed how to fix one of them and demonstrated how to mark headings as such with the Semantics widget – a simple fix that works both for iOS and Android. However, this is just the beginning. We are dedicated to making IRMA easy and accessible for everyone, and will continue to fix issues – both those identified by the audit, as well as those identified by the community and users! Come back in a week to see how we fixed some contrast issues so our app can more easily be used by visually impaired users, and as a side effect, arguably also looks better for everyone else.