Flutter – Cross-Plattform à la Google

Googles Flutter ist eine Dart-basierte Alternative zu Cross-Plattform-Klassikern wie Qt, Embarcadero und Co., die eine unkonventionelle Herangehensweise ans Benutzer-Interface mitbringt. Dieser einführende Artikel stellt die Grundlagen vor.

Werkzeuge  –  2 Kommentare

(Bild: Shutterstock)

Der wichtigste und für Quereinsteiger in die Cross-Plattform-Entwicklung schwierigste Punkt ist das deklarative Designkonzept des GUI-Stacks. Desktop- und Mobilbetriebssysteme betrachten Widgets normalerweise als langlebige Konstrukte, denen während der Programmausführung neue Attribute zugeschrieben werden. Dieser als imperativ bezeichnete Programmierstil führt zu langlebigen Steuerelementen.

Deklarative GUI-Stacks betrachten Steuerelemente als Wegwerfartikel. Entwickler melden beim Framework ein Zustandsobjekt an, das die Inhalte des Widgets beschreibt. Bei Änderungen verwirft die Runtime die Widget-Instanz, um aus den Zustandsinformationen ein neues Steuerelement zu bauen. Aufgrund der Zustandslosigkeit fallen die Widgets "schlank" aus – das native Aussehen entsteht nicht durch Kapselung nativer Views, sondern durch Mimikry im GUI-Stack, wie das nachfolgende Architekturdiagramm veranschaulicht.

Flutter verwendet keine nativen Widgets (Abb.1) (Bild: Flutter.io)

Als Motivation für diese zugegebenermaßen wenig intuitive Vorgehensweise führt Google in der Flutter-Dokumentation die Möglichkeit an, die Handhabung von Übergängen zwischen Zuständen in die Verantwortung des Frameworks zu legen.

Eine Frage des Werkzeugs

Flutter unterstützt eine Vielzahl IDEs und lässt sich auch für Nicht-Mobilplattformen kompilieren. Flutter unterstützt Android ab Version 4.1. Die nachfolgenden Schritte beziehen sich auf den Einsatz von Android Studio 3.4. und dem SDK in Version 9.0.

Zur Vorbereitung des Systems für die Installation von Flutter müssen alle aktuellen Updates eingespielt und anschließend geöffnete Projekte geschlossen werden. Über die Option Configure Plugins hinter dem Zahnradsymbol auf der unteren Seite des Startbildschirms von Android Studio lässt sich die Rubrik Marketplace des Plug-in-Assistenten für die Suche nach Flutter öffnen.

Das Flutter-Plug-in steht in den offiziellen Paketquellen zum Download bereit (Abb.2)

Nach Installation von Flutter und dem erforderlichen Dart-Plug-in aktiviert ein Reboot von Android Studio das Plug-in. Anschließend lässt sich über die Option Start a new Flutter project mit der Vorlage Flutter Application ein neues Projekt mit beliebigem Namen einrichten (vgl. Abb. 3).

Flutter setzt das Herunterladen eines zusätzlichen SDK voraus (Abb.3)

Sollte die automatische Installation des SDK in einem leeren Ordner des Systems über den CommonDialog von Android Studio fehlschlagen, lassen sich über die Starthilfe-Website von Flutter manuell aktuelle Archive in einem möglichst bequem zugänglichen Unterordner /flutter herunterladen. Sobald der Installationsassistent den Flutter-Ordner erkannt hat, bietet er die Möglichkeit zum Aktivieren der Platform Channels an – die beiden dabei angezeigten Checkboxen dürfen deaktiviert bleiben. Nach erfolgter Bestätigung lädt Android Studio einige Komponenten aus dem Internet herunter und beginnt die Indizierung des Projekts.

Erste Schritte mit Flutter

Die automatisch generierte Projektstruktur ist in Abbildung 4 zu erkennen. Von besonderer Wichtigkeit ist das /lib-Verzeichnis, in dem die Datei main.dart liegt. Abbildung 5 zeigt beispielhaft, wie das Programm auf einem BlackBerry PRIV aussieht.

Das Projekt besteht aus drei Unterordnern (Abb.4)
Das Projektbeispiel realisiert einen Knopf, der ein Label anpasst (Abb.5)

Flutter-Applikationen bestehen aus einem in Dart geschriebenen Programmcode. Der Einsprungpunkt der Datei besteht aus der Inklusion der Bibliothek mit dem Material Design sowie einer main-Funktion. Sie hat die Aufgabe, die Betriebssystemfunktion runApp mit einem beliebigen Widget auszustatten. Dieses dient fortan als Kopf der Widget-Hierarchie und wird vom Betriebssystem prinzipiell bildschirmfüllend angezeigt:

import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {

Im Fall der von Google vorgegebenen Demo handelt es sich um ein StatelessWidget – eine Basisklasse, die ein zustandsloses Widget realisiert. Die wichtigste Aufgabe einer Widget-Klasse – egal, ob zustandslos oder zustandsbehaftet – ist das direkte oder indirekte Bereitstellen einer Build-Methode. Der Flutter-GUI-Stack beginnt bei der Darstellung eines Bildschirms mit der "obersten" Widget-Instanz und führt die Build-Funktionen so lange aus, bis er am Ende bei einem primitiven Render Object ankommt. Render Objects sind dabei Widgets, die die Geometrie des anzuzeigenden Steuerelements beschreiben und mehr oder weniger direkt zur Anzeige freigegeben sind.

Um nachfolgend MaterialWidget-basierte Widgets verwenden zu können, ist als "Kern-Element" eine Instanz der Klasse MaterialApp erforderlich. Dabei handelt es sich um einen klassischen Helfer, der diverse Objektinstanzen erzeugt, die für die unterliegenden Widgets notwendig sind – von besonderer Wichtigkeit ist neben dem für die Darstellung der Farben verantwortlichen Theme-Attribut auch die Home-Eigenschaft. Sie legt fest, mit welchem Widget die Applikation zu starten hat.

    return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

Flutter bietet mit der Cupertino-Bibliothek übrigens auch eine Gruppe von Steuerelementen an, die das Aussehen von iOS-Steuerelementen emulieren. An die Stelle einer Instanz der Klasse MaterialApp tritt dann eine Instanz von CupertinoApp – weitere Informationen hierzu finden sich in der Cupertino-Dokumentation. Zu beachten ist, dass Cupertino-Widgets auch unter Android funktionieren. Da Google den Zeichenprozess zur Gänze nachbildet, sind die nativen Klassen des Betriebssystems nicht erforderlich.

Und jetzt, mit Zustand!

Im Rahmen der Build-Funktion entsteht eine neue Instanz der MyHomePage-Klasse. Ihre Deklaration sieht folgendermaßen aus:

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}

Im Unterschied zu einem zustandsfreien Widget kommt nun die Basisklasse StatefulWidget zum Einsatz. Sie ist dadurch gekennzeichnet, dass sie ihren Zustand nicht schon im Rahmen der Erzeugung erhält. Stattdessen enthalten StatefulWidgets eine Funktion namens createState(), die für die Bereitstellung des Zustandsobjekts erforderlich ist. Interessant ist dabei, dass die Build-Funktion in einem zustandsbehafteten Widget nicht in der Widget-Klasse, sondern im Zustand unterkommt. Diese auf den ersten Blick widersinnige Vorgehensweise ist notwendig, weil das Widget im Rahmen jedes Rendering-Durchlaufs neu erzeugt wird. Das persistente Element ist – anders als in klassischen GUI-Stacks – nicht das Darstellungs-Widget, sondern das eigentliche Zustandsobjekt. Seine Definition gestaltet sich vorliegenden Beispielprogramm folgendermaßen:

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}

State-Klassen entstehen durch Ableitung von State<>, einer Template-Klasse, die über ihren Parameter eine direkte Verbindung zum als Elternelement dienenden Widget aufnimmt. Daraufhin lassen sich Funktionen und Variablen implementieren, die für das eigentliche Vorhalten des Programmzustands sorgen. Im Fall des Beispielprogramms ist dies einerseits eine Zählvariable und andererseits eine Funktion, die für die Erhöhung der Werte sorgt.

Angesichts des zeitkritischen Rendering-Prozesses erfolgt die eigentliche Zustandsaktualisierung über einen kleinen Trick. Methoden, die den Zustand der Klasse anpassen, müssen ihrerseits setState aufrufen. Diese nimmt dann einen Parameter entgegen, der ein Callback enthält, das für die eigentliche Anpassung verantwortlich ist – beispielsweise der Befehl, der den Wert der Variable implementiert.

Der an die Funktion übergebene Callback wird vom Betriebssystem normalerweise sofort, auf jeden Fall aber innerhalb des zeitkritischen Rendering-Prozesses aufgerufen. Daraus folgt, dass es nicht erlaubt ist, in ihm lang anhaltende Aufgaben durchzuführen oder gar Futures zurückzugeben. Andererseits spricht nichts dagegen, noch ein Future in der "außen liegenden" Methode unterzubringen. In der Dokumentation findet sich dazu folgendes Snippet, das die Vorgehensweise illustriert:

Future<void> _incrementCounter() async {
setState(() {
_counter++;
});
Directory directory = await getApplicationDocumentsDirectory();
final String dirName = directory.path;
await File('$dir/counter.txt').writeAsString('$_counter');
}

Die neue Inkrementierungsmethode ist aus Sicht der Runtime legitim, unterscheidet sich aber von üblichen Vorgehensweise insofern, als nach der Addition noch diverse Dateisystem-Operationen erfolgen.

Im Beispielprogramm enthält die Klasse eine Build-Methode, die ihrerseits mit dem Zurückliefern eines Scaffold-Objekts beginnt:

  @override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),

Scaffolds sind insofern besonders, als sie eine Gruppe von Einsprungspunkten für Steuerelemente aufweisen. appBar sorgt dabei für das Erscheinen der Statusleiste, während sie im Body-Attribut die eigentlich darzustellenden Widgets anliefern:

      body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),

QML-erfahrene Entwickler sehen an dieser Stelle Vertrautes: Layout-Elemente dienen dazu, die Anzeigesteuerelemente übereinander anzuordnen. Zu guter Letzt ist noch das floatingActionButton-Attribut mit einem Steuerelement zu befüllen. Das Betriebssystem zeigt das übergebene Widget unten rechts an. Um die Anzeige des Increment-Knopfes zu steuern, muss ihm über das onPressed-Attribut ein Event Handler zugewiesen sein.

      floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

An dieser Stelle sei noch auf eine Besonderheit von Hot Reload hingewiesen: Anders als das im normalen Android-SDK implementierte Instant Run-Verfahren geht bei Hot Reload der Programmzustand bei Veränderungen in den Dateien normalerweise nicht verloren. Ein Vorteil, der weiter oben besprochenen strengen Trennung zwischen Zustand und Mark-up.

Nach dem Starten des Programms und mehrmaligem Drücken des Buttons am Telefon lässt sich die Farbe des Hintergrund-Themes ändern:

class MyApp extends StatelessWidget {
. . .
primarySwatch: Colors.pink,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

Sofern die Verbindung zwischen Flutter-Runtime und Android Studio nicht abgebrochen ist, erscheint die neue Version der Page automatisch auf dem Endgerät, der im Label angezeigte Zahlenwert bleibt dabei erhalten.

Entwickler, die Hot Reload intensiv nutzen, müssen eine kleine Besonderheit der update-Klassen beachten: Das Betriebssystem ruft die reassemble-Methode immer dann auf, wenn ein Hot-Reload-Ereignis stattfindet. Die Methode erlaubt Entwicklern das erneute Laden von Ressourcen, die aus irgendeinem Grund "verloren" gehen könnten. Nähere Informationen dazu finden sich in der Dokumentation des Features.

Seitenwechsel mit Flutter

Nach diesen Überlegungen zur Architektur soll im nächsten Schritt eine mehrseitige Applikation entstehen. Ein Rechts-Klick auf den /lib-Ordner öffnet das Kontextmenü mit der Option New Dart file. Die neue Datei erhält den Namen scene2.dart und folgenden Inhalt:

import 'package:flutter/material.dart';
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),

Da iOS und Android für Formulare verschiedene Terminologien verwenden, normalisiert Google die Begriffe in Flutter. Activities, ViewController und so weiter werden dabei als Routen bezeichnet. Aus Sicht des GUI-Stacks sind Routen allerdings mehr oder weniger gewöhnliche Widgets.

Die Interaktion zwischen den einzelnen angezeigten Routen erfolgt über eine vom Framework bereitgestellte Klasse namens Navigator. Bei der Auswahl der Begriffe hat sich Google offensichtlich von Frameworks für die Web-Entwicklung inspirieren lassen.

Für das gewählte Beispiel reicht es an dieser Stelle, wenn sich das zweite Formular bei Deaktivierung selbst vom Bildschirm entfernt – da keine Informationen zwischen den Elementen ausgetauscht werden sollen.

Die Navigator-Klasse legt einen Stack von Routen an. Durch Aufruf von Pop lässt sich die oberste davon eliminieren. Um die Korrelation zu vereinfachen, ist zudem ein Kontext zu übergeben. Das Kontextobjekt verhält sich insofern wie unter Android bekannt: Es beschreibt die Ausführungsumgebung der Applikation näher.

Die Abstammung von Web-Frameworks wird besonders deutlich, wenn man das Konzept der namensbasierten Routen implementiert. In diesem Fall ist die Applikation um eine Routendeklaration zu erweitern und zudem über das initialRoute-Attribut festzulegen, welche Route als "Start" aufzurufen ist:

MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => FirstScreen(),
'/second': (context) => SecondScreen(),
},
);

Die weiteren Schritte beschränken sich auf das klassische Instanziieren der Formulare, das App-Entwicklern geläufiger sein dürfte. In main.dart ist dazu folgende Inklusion erforderlich, um die in ihr enthaltenen Komponenten sichtbar zu machen:

import 'package:flutter/material.dart';
import 'scene2.dart';

In main.dart ist ein Steuerelement für die Auslösung der Routen-Veränderung erforderlich, das darüber hinaus die Kommunikation verantwortet. Hierzu lässt sich auf den bereitgestellten Katalog aller verfügbaren Widgets zurückgreifen. Die Unterrubrik Material enthält all jene Steuerelemente, die das Look and Feel des Android-Betriebssystems nachbilden.

Als Steuerelement soll ein einfacher Button dienen, die in Android normalerweise entweder einfarbig oder mit transparentem Hintergrund ausgestattet sind. Da Flutter die Steuerelemente zur Laufzeit mit Zeichenbefehlen erzeugt, lässt sich problemlos noch eine kleine Änderung vornehmen, die über die Möglichkeiten des Android-GUI-Stacks hinausgeht:

children: <Widget>[
RaisedButton(
onPressed: () {},
textColor: Colors.white,
padding: const EdgeInsets.all(0.0),

Normalerweise nimmt das RaisedButton-Steuerelement ein Child entgegen, das für die Anzeige des Labels sorgt. Im Beispiel kommt aber im ersten Schritt ein LinearGradient zum Einsatz, der erst anschließend einen Text übernimmt, wie die Emulator-Darstellung in Abbildung 6 zeigt:

    child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
Color(0xFF0D47A1),
Color(0xFF1976D2),
Color(0xFF42A5F5),
],
),
),
padding: const EdgeInsets.all(10.0),
child: const Text(
'Gradient Button',
style: TextStyle(fontSize: 20)
),
),
),
Wer nicht auf native Steuerelemente setzt, hat auch Vorteile (Abb.6)

Der früher häufig kritisierte Verzicht auf native Steuerelemente bei der Programmierung mobiler Anwendungen spielt heute kaum noch eine Rolle, da 5-Wege-Navigation und Tastaturen bei Mobilgeräten nahezu vollständig durch Touchscreens ersetzt sind. Benutzer erwarten daher lediglich ausreichend große Steuerelemente, deren Funktion unmittelbar ersichtlich ist.

Um im nächsten Schritt die Ansicht zu aktivieren, muss der Event Handler mit folgendem Code adaptiert werden:

RaisedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},

Bevor sich die neue Route an den Navigator übergeben lässt, ist noch eine zusätzliche Instanz der Hilfsklasse MaterialPageRoute erforderlich, da neuere Versionen von Android häufig komplexe Animationen durchführen. Die Hilfsklasse hat die Aufgabe, den GUI-Stack zur Durchführung der plattformüblichen Animationen anzuleiten.

Damit ist das erstellte Programm zum Einsatz bereit und lässt sich auf dem Emulator oder dem Smartphone durch Druck auf den Knopf starten und wieder schließen.

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),

Fazit und Ausblick

Flutter bietet dank des modernen technischen Unterbaus mit Dart und Hot Reload einen schnellen Lohn: Das Kompilieren von Android-Debug-Builds erweist sich in der Praxis der Java-Entwicklung als Flaschenhals, der bei der Verwendung von Flutter weitgehend wegfällt.

Als Nachteil könnten sich die gegenüber klassischen Programmiersprachen deutlich abweichenden Entwicklungsparadigma erweisen. Wer intensiv mit Java, C++, Delphi oder .NET vertraut ist, muss komplett umdenken und sich in neue Ansätze einarbeiten.

Der nächste Teil dieser Artikelserie beleuchtet die Cross-Plattform-Möglichkeiten von Flutter.

Was ist mit Fuchsia?

Im Umfeld von Flutter ist immer wieder die Rede davon, dass Googles neues Betriebssystem die Programmierumgebung intensiv nutzen werde – technisch betrachtet, stimmt das sicherlich.

Nach Einschätzung des Autors gibt es für Entwickler zum gegenwärtigen Zeitpunkt dennoch keinen Handlungsbedarf, denn für das Fuchsia OS steht im Moment kaum geeignete Hardware zur Verfügung. Wer nicht gewillt ist, eines der drei von Google vorgesehenen Zielsysteme zu verwenden, bleibt außen vor. Der Fuchsia-GUI-Stack setzt zudem Vulkan voraus, das nur in wenigen PC-Emulatoren funktioniert. Wer den (ressourcenaufwendigen) Kompilierungsprozess trotzdem auf sich nimmt, bekommt am Ende nur ein "besseres" Terminalfenster präsentiert.

Inwiefern sich Entwickler im Allgemeinen überhaupt auf absehbare Zeit mit Fuchsia auseinandersetzen müssen, bleibt daher vorläufig offen. Die Flutter-Runtime zeichnet die diversen Steuerelemente ja zur Laufzeit aus GDI-Primitiva und nutzt die (so vorhandenen) nativen Steuerelemente der Plattform nicht. Daher dürfte das Umstellen von Flutter-Anwendungen aus technischer Sicht kein großes Problem darstellen. Für PC- oder Notebook-User andererseits eignet sich die GUI einer typischen Smartphone-App sowieso eher weniger.

Tam Hanna
befasst sich seit dem Jahr 2004 mit Handcomputern und Elektronik. Derzeit liegt sein Fokus auf interdisziplinären Anwendungen von Informationstechnologie.