Flutter – Cross-Plattform à la Google

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.