Barrierefreie Android-Apps mit Jetpack Compose erstellen: Beispiele in Echtzeit

Devanshu Chandra

Von Devanshu Chandra

30. Juni 2022

Jetpack Compose Teil 2 01

Im ersten Teil dieser Blogreihe zum Erstellen barrierefreier Android-Apps mit Jetpack Compose haben wir gelernt, wie sich Barrierefreiheitsdienste in Verbindung mit Jetpack Compose verhalten und wie Jetpack Compose einen Semantikbaum erstellt, der wiederum den Barrierefreiheitsdiensten beim Erfassen von Daten hilft.

Im zweiten Teil werden wir verschiedene einfache Echtzeit-Beispiele durchgehen, um zu verstehen, wie man grundlegende Composables wie Textfelder, Schaltflächen und Schalter für Nutzer des Barrierefreiheitsdienstes zugänglich macht.

Schaltflächen

Beispiel 1

Screenshot mit verschiedenen Beispielen für Schaltflächen sowie einer Beschreibung der einzelnen Schaltflächen, die mit Jetpack Compose programmiert wurden.

Die erste Schaltfläche im obigen Screenshot sieht in deklarativen APIs wie folgt aus:

Button(
            onClick = { Toast.makeText(context, "Dies ist eine barrierefreie Schaltfläche!", 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 = "Barrierefrei")
        }

Diese Schaltfläche ist für Barrierefreiheitsdienste wie TalkBack vollständig barrierefrei, da der Name, die Rolle und die Aktion, die mit dem Composable verknüpft sind, alle als „Barrierefrei, Schaltfläche. Zum Aktivieren doppelt antippen“ angesagt werden. Die native Button-Klasse von Jetpack Compose stellt standardmäßig alle erforderlichen semantischen Informationen bereit, um die Schaltfläche vollständig barrierefrei zu gestalten.

Beispiel 2

Schauen wir uns nun eine individuell angepasste Schaltfläche an. Das folgende Beispiel zeigt, wie man UI-Elemente ganz nach den eigenen Bedürfnissen anpassen und gleichzeitig sicherstellen kann, dass die Schaltfläche barrierefrei ist.

Die zweite Schaltfläche im obigen Screenshot ist eigentlich ein Composable vom Typ „Row“, das so programmiert wurde, dass es wie eine Schaltfläche aussieht und auch so funktioniert. So sieht das in deklarativen APIs aus:

Row(
     modifier = Modifier
      .size(height = 40.dp, width = 120.dp)
      .background(color = Color(98, 0, 238))
      .constrainAs(example4) {
         top.linkTo(divider4.bottom, margin = 10.dp)
         start.linkTo(parent.start)
         end.linkTo(parent.end)
     }
     .clickable(
         enabled = true,
         onClick = {
            Toast.makeText(context, "Barrierefrei", Toast.LENGTH_SHORT).show()
       },
       role = Role.Button
     )
) {
    Text(
      text = "Zugänglich",
      fontWeight = FontWeight.Bold,
      modifier = Modifier.padding(10.dp),
      color = Color.White
   )
}

Die Rolle wird als „Button“ zugewiesen, und „onClick“ wird ebenfalls definiert, indem die Schaltfläche anklickbar gemacht wird.

Dies hilft dabei, Barrierefreiheitsdienste wie TalkBack oder Switch Access zu aktivieren, um Ihre Benutzeroberfläche zu testen und besser zu verstehen, wo Verstöße gegen die Barrierefreiheit auftreten.

Beispiel 3

Die dritte Schaltfläche im obigen Screenshot ist nicht zugänglich, da der Text der Schaltfläche auf „invisibleToUser“ gesetzt ist. TalkBack ignoriert die Schaltfläche und springt zum nächsten UI-Element weiter, wenn man versucht, den Fokus darauf zu setzen. So wird die Schaltfläche mithilfe deklarativer APIs programmiert:

Button(
       onClick = { Toast.makeText(context, "Dies ist eine nicht barrierefreie Schaltfläche!", 
Toast.LENGTH_SHORT).show() },
       modifier = Modifier.constrainAs(example2) {
         top.linkTo(divider2.bottom, margin = 10.dp)
         start.linkTo(parent.start)
         end.linkTo(parent.end)
       }.semantics {  }
    ) {
       Text(
          text = "Nicht barrierefrei",
          modifier = Modifier.semantics { this.invisibleToUser() }
       )
}

Dieses Beispiel verdeutlicht, dass es bei der Entwicklung von Benutzeroberflächen sehr wichtig ist, sicherzustellen, dass ein Element, das für den Benutzer unsichtbar gemacht werden soll („invisibleToUser“), über die Barrierefreiheitsdienste zugänglich bleibt. Andernfalls ignorieren die Barrierefreiheitsdienste die als für den Benutzer unsichtbar markierten Composables vollständig.

Beispiele für TextField/EditText

Screenshot mit verschiedenen Beispielen für Textfelder sowie einer Beschreibung, programmiert in Jetpack Compose.

Beispiel 1

Das erste Textfeld im obigen Screenshot ist barrierefrei, wie TalkBack ansagt: „Eingabefeld, geben Sie Ihren Namen ein. Doppeltippen zum Bearbeiten, doppeltippen und gedrückt halten für weitere Bearbeitungsoptionen.“ Alle mit dem UI-Element verbundenen Namen, Rollen und Aktionen werden von TalkBack angesagt.

Es ist wichtig zu beachten, dass die Barrierefreiheitsrichtlinie WCAG 1.3.1, Stufe A, vorschreibt, dass Beschriftungen auch dann erhalten bleiben müssen, wenn Text in das „EditText“-Feld eingegeben wird. „Compose“ leistet hier gute Arbeit und sorgt dafür, dass die Beschriftung stets erhalten bleibt.

Screenshot, der zeigt, wie Beschriftungen für Textfelder in Jetpack Compose automatisch beibehalten werden.

Auf dem obigen Screenshot sehen Sie: Wenn Text in das Textfeld eingegeben wird oder wenn es den Fokus erhält, bleibt die Beschriftung unverändert, sodass es vollständig barrierefrei bleibt.

Um ein `EditText`-Element mit XML-Ansichten barrierefrei zu gestalten, müssen Sie ein `TextView`-Element als Beschriftung für das `EditText`-Element zuweisen, um dem Barrierefreiheitsdienst mitzuteilen, dass eine Beschriftung damit verknüpft ist. So wird dies in Jetpack Compose umgesetzt:

val textStateTextField = remember { mutableStateOf(TextFieldValue()) }
  TextField(
     value = textStateTextField.value,
     onValueChange = { textStateTextField.value = it },
     label = { Text(text = "Geben Sie Ihren Namen ein") },
     modifier = Modifier.constrainAs(example1) {
       top.linkTo(divider1.bottom, margin = 10.dp)
       start.linkTo(parent.start)
       end.linkTo(parent.end)
    }
)

Beispiel 2

Jetpack Compose verfügt über praktische APIs, um Icons am Anfang und am Ende von Textfeldern einzufügen. Außerdem bietet es APIs, um Beschreibungen für diese Icons festzulegen.

Das zweite Textfeld im obigen Screenshot veranschaulicht, wie man einem Textfeld ein Symbol am Anfang hinzufügt. Sie können auf eine Inhaltsbeschreibung für das Symbol verzichten, wenn die Beschriftung aussagekräftig genug ist; die Angabe einer Beschriftung für das Textfeld ist jedoch unbedingt erforderlich. So wird das Textfeld programmiert:

TextField(
         value = text,
         leadingIcon = { Icon(imageVector = Icons.Default.Email, contentDescription = "") },
         //trailingIcon = { Icon(imageVector = Icons.Default.Add, contentDescription = null) },
         onValueChange = {
           text = it
          },
          label = { Text(text = "E-Mail-Adresse") },
          modifier = Modifier.constrainAs(example4) {
            top.linkTo(divider4.bottom, margin = 10.dp)
            start.linkTo(parent.start)
            end.linkTo(parent.end)
          }
   )

Beispiel 3

Das dritte Textfeld im obigen Screenshot ist nicht zugänglich, da ihm keine Beschriftung zugeordnet ist. Wenn ein TalkBack-Nutzer den Fokus auf dieses Textfeld setzt, wird daher Folgendes angesagt: „EditBox. Zum Bearbeiten doppelt tippen, für weitere Bearbeitungsoptionen doppelt tippen und gedrückt halten.“ Wie Sie feststellen können, ist die Beschriftung „Name“ nicht Teil des von TalkBack angesagten Textes. Es sieht zwar so aus, als befände sich links neben dem Textfeld eine Beschriftung, doch diese fungiert nicht als Beschriftung für das Textfeld. So sieht das Textfeld im Code aus:

Row(
      modifier = Modifier.constrainAs(example2) {
        top.linkTo(divider2.bottom, margin = 10.dp)
        start.linkTo(parent.start)
        end.linkTo(parent.end)
      }
   ) {

     Text(text = "Name: ", modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp))

     val textStateTextField2 = remember { mutableStateOf(TextFieldValue()) }
     TextField(
       value = textStateTextField2.value,
       onValueChange = { textStateTextField2.value = it },
    )
  }

Man könnte argumentieren, dass man durch die Verwendung der Semantik-Eigenschaft „mergeDescendants“ für die Zeile zwar das Text- und das TextField-Element gruppieren kann, das TextField-Element dann aber ebenfalls nicht zugänglich bleibt. Der einzige Unterschied besteht darin, dass bei Verwendung von „mergeDescendants“ das Text- und das TextField-Element gleichzeitig im Fokus stehen, anstatt nacheinander.

Screenshot, der zeigt, wie zwei Composables gruppiert und gleichzeitig in den Fokus gerückt werden können.

Es ist sehr wichtig, dem TextField in Jetpack Compose ein Label zuzuweisen. Die Gruppierung des TextFields mit einem „Text“-Composable macht dabei keinen Unterschied.

Beispiele für Schalter

Screenshot mit verschiedenen Switch-Beispielen und entsprechenden Beschreibungen, programmiert mit Jetpack Compose.

Beispiel 1

Der erste Schalter ist barrierefrei, da TalkBack Folgendes ansagt: „Ein, E-Mails abrufen, Schalter, Doppeltippen, Doppeltippen zum Umschalten.“ Das Erstellen eines barrierefreien Schalters im Modus „Verfassen“ ist nicht einfach.

Auf dem Screenshot sehen Sie, dass TalkBack nur den Text und nicht das Steuerelement fokussiert, was beabsichtigt ist. Sie können diesen Schalter barrierefrei gestalten, indem Sie das „Switch“-Composable mit dem „Text“-Composable kombinieren und anschließend die Semantik des „Switch“-Composables zurücksetzen. So sieht der erste Schalter im Code aus:

MaterialTheme() {
       val (isSwitchChecked, setSwitchState) = remember { mutableStateOf(true) }
       Row(
         modifier = Modifier
           .constrainAs(example1) {
              top.linkTo(divider1.bottom, margin = 10.dp)
              start.linkTo(parent.start)
              end.linkTo(parent.end)
            }
            .semantics(mergeDescendants = true) { }
            .padding(16.dp)
       ) {
        Text(
          text = "E-Mails abrufen",
          modifier = Modifier
            .toggleable(
                value = isSwitchChecked,
                onValueChange = { setSwitchState(!isSwitchChecked) },
                role = Role.Switch
              )
              .weight(1f),
          fontSize = 25.sp
        )

        Switch(
          checked = isSwitchChecked,
          onCheckedChange = { setSwitchState(!isSwitchChecked) },
          modifier = Modifier
            .clearAndSetSemantics { }
            .padding(top = 5.dp)
        )
    }
}

Hier sind folgende Punkte besonders zu beachten:

  • Das „Text“-Composable (das als Beschriftung für den Schalter dient) und der Schalter selbst sind innerhalb eines „Row“-Composables gruppiert.
  • Für die Zeile ist „mergeDescendants“ auf „true“ gesetzt.
  • Indem die Semantik des Schalters zurückgesetzt wird, richtet TalkBack den Fokus auf den Text. Wird die Semantik des Schalters nicht zurückgesetzt, richtet TalkBack den Fokus erneut auf den Schalter, nachdem es zuvor den Fokus auf die Zeile gerichtet hatte – wie es beim zweiten Schalter im obigen Screenshot der Fall ist.

Beispiel 2

Für den dritten Schalter im obigen Screenshot ist in der übergeordneten Zeile der Modifikator vom Typ „toggleable“ festgelegt, wodurch dem Schalter standardmäßig der Zustand „off“ zugewiesen wird. Talkback verkündet: „On, Get Emails, off, Switch…“ Beachten Sie, dass der Zustand „off“ auch dann zugewiesen wird, wenn sich der Schalter im Zustand „On“ befindet. So sieht der dritte Schalter im Code aus:

Row(
      modifier = Modifier.constrainAs(example3) {
        top.linkTo(divider3.bottom)
        start.linkTo(parent.start)
        end.linkTo(parent.end)
      }
       .semantics(mergeDescendants = true) { }
       .padding(16.dp)
       .toggleable(
          value = isSwitchChecked2,
          role = Role.Switch,
          onValueChange = { setSwitchState2(!isSwitchChecked2) }
        )
   ) {
      Text(
        text = "E-Mails abrufen",
        modifier = Modifier.weight(1f),
        fontSize = 25.sp
      )
      Switch(
        checked = isSwitchChecked2,
        onCheckedChange = { setSwitchState2(!isSwitchChecked2) },
        modifier = Modifier.clearAndSetSemantics {  }
      )
    }

Beispiele für Kontrollkästchen

Screenshot mit verschiedenen Checkbox-Beispielen und entsprechenden Beschreibungen, programmiert mit Jetpack Compose.

Beispiel 1

Das Erstellen eines barrierefreien Kontrollkästchens im Verfassen-Modus ist ziemlich einfach. Das erste Kontrollkästchen im obigen Screenshot wird wie folgt angesagt: „aktiviert, Marketing-E-Mails, Kontrollkästchen, Zum Umschalten doppelt tippen.“ Somit werden alle drei Elemente – Name, Rolle, Status und Aktion – von TalkBack angesagt. So sieht das erste Kontrollkästchen im Code aus:

Row(modifier = Modifier
      .toggleable(
      value = checkedState,
      enabled = true,
      role = Role.Checkbox,
      onValueChange = { setCheckBoxState(!checkedState) }
   )
   .semantics(mergeDescendants = true) { }
   .constrainAs(example1) {
     top.linkTo(divider1.bottom)
     start.linkTo(parent.start)
     end.linkTo(parent.end)
   }
 ) {
    Checkbox(
      checked = checkedState,
      modifier = Modifier.padding(16.dp),
      onCheckedChange = null
)

Text(text = "Marketing-E-Mails erhalten", modifier = Modifier.padding(16.dp))

}

Wichtige Punkte, die im obigen Code zu beachten sind, sind:

  • „The Row“ verhält sich wie ein Kontrollkästchen.
  • Ich habe einen Modifikator vom Typ „umschaltbar“ verwendet, um TalkBack mitzuteilen, dass das Kontrollkästchen durch Klicken an einer beliebigen Stelle in der Zeile umgeschaltet werden kann.
  • Beachten Sie außerdem, dass die Nachkommen der Zeile zusammengeführt werden.

Beispiel 2

Das zweite Kontrollkästchen im obigen Screenshot ist etwas verwirrend, wenn TalkBack den Fokus darauf legt. Nachdem der Fokus beim Wischen nach rechts auf die gesamte Zeile gelegt wurde, wird der Schalter erneut fokussiert, was für den TalkBack-Nutzer zu einer verwirrenden Erfahrung führt. So sieht der zweite Schalter im Code aus:

Row(modifier = Modifier
            .semantics(mergeDescendants = true) { }
            .constrainAs(example2) {
                top.linkTo(divider2.bottom)
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
        ) {
            Checkbox(
                checked = checkedState1,
                modifier = Modifier.padding(16.dp),
                onCheckedChange = { setCheckBoxState1(!checkedState1) }
            )
            Text(text = "Marketing-E-Mails erhalten", modifier = Modifier.padding(16.dp))
        }

Benutzerdefinierte Barrierefreiheitsaktionen in Jetpack Compose

Mit „Compose“ können wir benutzerdefinierte Barrierefreiheitsaktionen für „Composables“ festlegen. Stellen Sie sich ein Szenario vor, in dem sich über einem Element eine Favoriten-Schaltfläche befindet, die den Nutzer bei Anklicken zu einem anderen Bildschirm weiterleitet. Wir müssen dem Barrierefreiheitsdienst mitteilen, dass mit der Ansicht weitere Aktionen verbunden sind, und ihn höflich bitten, diese verschiedenen Aktionen zu verarbeiten.

Im folgenden Screenshot sind der roten Favoriten-Schaltfläche benutzerdefinierte Barrierefreiheitsaktionen zugewiesen. Wenn TalkBack den Fokus auf das Listenelement legt, wird am Ende Folgendes angesagt: „…Zum Aktivieren doppelt tippen, Aktionen verfügbar, zum Anzeigen nach oben und dann nach rechts wischen.“

Screenshot, der zeigt, wie benutzerdefinierte Aktionen in Jetpack Compose funktionieren.  Screenshot, der zeigt, wie benutzerdefinierte Aktionen in Jetpack Compose funktionieren. Screenshot, der zeigt, wie benutzerdefinierte Aktionen in Jetpack Compose funktionieren.

So rufen Sie die Kompositionshierarchie in Jetpack Compose ab

Mit Jetpack Compose können Entwickler die Kompositionshierarchie im Rahmen automatisierter Tests einsehen. Mithilfe von `ComposeTestRule` lässt sich sowohl die zusammengeführte als auch die nicht zusammengeführte Kompositionshierarchie ganz einfach ausgeben. Hier ist ein Beispiel dafür, wie das funktioniert:

composeTestRule.setContent {
            AxeConApp()
        }
        composeTestRule.onNodeWithText("Beispiele für Textfelder").performClick()

        composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG-UN-MERGED")
        composeTestRule.onRoot(useUnmergedTree = false).printToLog("TAG-MERGED")
}

Wie sollte ich meine Anwendung auf Fehler hinsichtlich der Barrierefreiheit testen?

  • Verwenden Sie Barrierefreiheitsdienste wie TalkBack oder Switch Access, um Ihre Anwendung zu testen.
  • Denken Sie daran: Die Benutzererfahrung einer App sollte für Nutzer ohne assistive Technologien und für Nutzer mit assistiven Technologien identisch sein.
  • Nutzen Sie axe Devtools Mobile für Android mit automatisierten Tests – wir unterstützen Jetpack Compose!

Mit all den Grundlagen, die wir in diesem Beitrag besprochen haben, sind wir nun bereit, mit Jetpack Compose nicht nur Android-Anwendungen, sondern auch barrierefreie Android-Anwendungen zu entwickeln. Mit komplexeren APIs gehen auch komplexere Probleme hinsichtlich der Barrierefreiheit einher. Es wird dringend empfohlen, bereits vor Beginn der App-Entwicklung eine barrierefreie App-Architektur zu planen, um Fehler im Zusammenhang mit der Barrierefreiheit zu reduzieren.

Devanshu Chandra

Devanshu Chandra

Devanshu Chandra ist seit 2016 als Android-Entwickler tätig und arbeitet seit 2020 bei Deque . Er engagiert sich intensiv dafür, einer neuen Generation von Entwicklern weltweit Kenntnisse über die Barrierefreiheit bei Android-Mobilgeräten zu vermitteln, und führt bei Bedarf Online-Schulungen zum Thema Barrierefreiheit durch. Er hielt einen Vortrag auf der axe-con 2021 und hat Gastvorlesungen an zahlreichen Hochschulen gehalten. Devanshu setzt sich für Automatisierung ein und hat zahlreichen Organisationen dabei geholfen, ihre Automatisierungsziele zu erreichen. Er ist ein eingefleischter Red-Wings-Fan und träumt davon, dass die Lions eines Tages den Super Bowl gewinnen.

Erhalten Sie Blog-Beiträge direkt in Ihren Posteingang

Kein Geschwafel, sondern echte Erkenntnisse zum Thema Barrierefreiheit von qualifizierten Experten.

Sie erklären sich damit einverstanden, dass Deque Informationen gemäß den Bestimmungen in DequeDatenschutzerklärungbeschrieben, Informationen von Deque entgegennimmt, nutzt und weitergibt. Sie können Ihre Einwilligung jederzeit widerrufen, indem Sie uns kontaktieren.

Mehr zu diesem Thema

Wie Entwicklerteams Bilder in Android barrierefrei gestalten können

Devanshu Chandra
23. August 2022 Von Devanshu Chandra

Bilder in iOS-Apps barrierefrei zu gestalten, ist relativ einfach: Man muss lediglich eine Inhaltsbeschreibung für das Bild bereitstellen, und das war’s schon. Bei Android hingegen …

Artikel lesen
So machen Sie Bilder in Android barrierefrei 01

Entwicklung barrierefreier Android-Apps mit Jetpack Compose: Wie der Barrierefreiheitsdienst mit Jetpack Compose zusammenwirkt

Google hat kürzlich Jetpack Compose vorgestellt, ein modernes Android-UI-Toolkit, mit dem sich native Android-Benutzeroberflächen mithilfe von intuitivem, deklarativem Kotlin schneller und einfacher erstellen lassen…

Artikel lesen
Jetpack Compose – Barrierefreiheit 01