Flutter – Cross-Plattform à la Google

Widgets in Bewegung!

Auch wenn exotischere Formate wie quadratische Bildschirme bei Smartphones größtenteils der Vergangenheit angehören, gilt es bei der App-Programmierung dennoch, eine Vielzahl an Bildschirmauflösungen und Ansichtsvarianten wie den Landscape Mode zu unterstützen. Um die erforderliche Größenanpassung und die Skalierung des Android-Benutzer-Interfaces umzusetzen, bieten Cross-Plattform-Frameworks wie Qt mittlerweile eine explizite Layout-Engine. Welche Möglichkeiten sich dadurch eröffnen, deutet das aus der Dokumentation von Google entnommene Beispiellayout an, das die Abbildungen 7 und 8 zeigen.

Die drei Programmsymbole erscheinen nebeneinander ... (Abb.7)

Dass die Steuerelemente in Abb. 7 in Rahmen eingepasst sind, ist auf das Attribut debugPaintSizeEnabled zurückzuführen: Ist es auf true gesetzt, liefert der GUI-Stack zusätzliche Debug-Zeicheninformationen.

... was Einiges an Unterstützungsklassen voraussetzt (Abb.8) (Bild: https://flutter.dev/docs/development/ui/layout#sizing-widgets)

Bei den in der Abbildung farblich hervorgehoben Diagrammelementen handelt es sich um Instanzen von Container, einem Widget, das ein anderes Widget als Child aufnehmen kann und diesem dann Styling-Attribute wie Margins (Ränder) und sonstige Abstände übergibt. Die eigentlichen Layout-Klassen finden sich in der erwähnten Widget-Galerie und sind exemplarisch in Abbildung 9 gezeigt.

Diese Widgets helfen bei der Anordnung der Elemente am Bildschirm (Abb.9)

Darüber hinaus gibt es noch die Placeholder-Klasse, die Google in der Dokumentation als Stellvertreter für ein noch nicht angelegtes Widget beschreibt. In der Praxis dürfte sie sich aber auch als Ersatz für die aus Qt bekannten Spacer-Steuerelemente eigenen.

Das Beispielprogramm arbeitet mit einer Textbox, die Eingaben entgegennehmen soll. Dazu sind folgende Anpassungen in der Datei scene2.dart notwendig:

body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
maxLines: null,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term'
),
),
RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
]
)
),

Anstatt dem child-Attribut wie bisher eine Widget-Instanz zu übergeben, übernimmt nun eine Instanz von Column die Anordnung der Elemente am Bildschirm. Beim Anlegen einer mehrteiligen Textbox ist der Wert maxLines auf Null zu setzen, damit die Textbox eine beliebige Größe annehmen kann. Fehlt das Attribut komplett, bleibt die Textbox auf eine Zeile beschränkt.

Beim Ausführen des Programms in der bisher vorliegenden Version und dem Öffnen der Textbox und mehrfachem Bestätigen der Dialoge bietet sich die in Abbildung 10 gezeigte Darstellung: Im Debug-Modus überprüft der Renderer, ob die gesamte Szene auf den Bildschirm passt. Ist das nicht der Fall, erhalten Entwickler die gezeigte Warnung.

Hier ist etwas nicht am Bildschirm sichtbar (Abb.10)

Auf dem Startbildschirm präsentieren sich die Widgets des Formulars noch zentral angeordnet, wie Abbildung 11 zeigt. Ausschlaggebend dafür sind die Einstellungen für Row und Column, die beide das Element Main Axis aufweisen. Diese Hauptachse gibt die Richtung vor, über die sich die child-Steuerelemente anordnen lassen.

Die beiden Widgets schweben in der Mitte des Bildschirms (Abb.11)

Row gibt die horizontale, Column die vertikale Anordnung vor. Von Besonderheit ist, dass die Ausrichtung der Widgets anhand der Textflussrichtung erfolgt: Auf einem beispielsweise auf Arabisch eingestellten BlackBerry würden die Formulare daher anders aussehen. Die bei der mehrsprachigen Lokalisierung von Apps offensichtliche Wichtigkeit dieses Konzepts spiegelt sich auch in Abbildung 12 wider, die die Anordnung grafisch beschreibt. MainAxisSize bestimmt dabei die Ausbreitung, der standardmäßig eingestellte Wert max lässt die Achse auf den maximal verfügbaren Raum anwachsen.

Row und Column unterscheiden sich in der Ausbreitungsrichtung der Hauptachse (Abb.12)

Die Anordnung der einzelnen Widgets erfolgt über das MainAxisAlignment-Attribut, das bisher die sechs in der Tabelle gezeigten Attribute kennt. Im Beispielprogramm soll MainAxisAligment.start die Steuerelemente am oberen Rand des Bildschirms versammeln:

body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
Attribut Verhalten
MainAxisAligment.start Platziert alle Steuerelemente am Beginn der Achse
MainAxisAligment.end Platziert alle Steuerelemente am Ende der Achse
MainAxisAligment.center Platziert alle Steuerelemente in der Mitte der Achse
MainAxisAligment.spaceBetween Nutzt den gesamten Platz und verteilt die Widgets äquidistant
MainAxisAligment.spaceEvenly Nutzt den gesamten Platz und verteilt die Widgets äquidistant. Vor dem ersten und nach dem letzten Widget finden sich in diesem Betriebsmodus ebenfalls zwei Spaces
MainAxisAligment.spaceAround Entspricht SpaceEvenly, wobei der oberste und der unterste Space nur halb so hoch sind

Der nächste Schritt stellt sicher, dass das Textfeld den maximal möglichen Platz auf dem Bildschirm einnimmt. Hierzu dient das Expanding-Steuerelement, das das als child übergebene Widget maximal ausbreitet, wie in Abbildung 13 zu sehen ist:

child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: TextField(
maxLines: null,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term'
),
),
),
RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
]
)
Die Textbox hat den maximalen Platz am Bildschirm eingenommen (Abb.13)

Entwickler finden detaillierte Informationen zu den Anordnungswerkzeugen in der Flutter-Dokumention zum Layout.

Daten aus der Textbox einlesen

Die Entscheidung für TextField erfolgte nicht zufällig. Das Steuerelement ist insofern interessant, als die Erfassung und Weiterverarbeitung der Daten Aufwand verursacht. Im ersten Schritt ist dazu das bisher zustandsfreie Zweitformular in ein Widget umzuwandeln, das wie oben besprochen einen Status hält.

Ein StatefulWidget lässt sich mit createState erzeugen. Darauf folgt eine zweite Klassendeklaration, die über die schon vorhandenen Methoden des vorhergehenden StatelessWidgets zu platzieren ist:

class SecondRoute extends StatefulWidget {
@override
_SecondRouteState createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute> {
@override
Widget build(BuildContext context) {

Im nächsten Schritt entsteht eine Instanz von TextEditingController. Als eine Container-Klasse enthält er die für die Interaktion mit Textboxen verantwortliche Logik:

class _SecondRouteState extends State<SecondRoute> {
final myController = TextEditingController();

Anschließend muss die Textbox Kontakt zum Controller aufnehmen. Dazu ist dem controller-Attribut des Steuerelements myController hinzuzufügen:

Expanded(
child: TextField(
controller: myController,
maxLines: null,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term'
),
),
),

Eine einfach zu bedienende Hilfsfunktion erlaubt daraufhin den Zugriff auf die Textinformationen:

Text(myController.text),