Mach die Flutter!

Flutter: App-Entwicklung mit Googles Cross Platform Framework
1 Kommentar

Google veröffentlichte 2017 die erste Alphaversion eines neuen Frameworks zur Entwicklung von plattformunabhängigen mobilen Applikationen. Das auf den Namen Flutter getaufte Framework basiert auf der Programmiersprache Dart und verfolgt einen anderen Ansatz als bisherige Frameworks, wie beispielsweise Facebooks React Native oder Native Script von Telerik. Seit Dezember 2018 hat das Projekt Version 1.0 erreicht.

Flutter tritt an, um die plattformunabhängige App-Entwicklung zu vereinfachen. Anders als bereits existierende Frameworks verwendet Flutter nicht die nativen UI-Komponenten der Plattformen. Zum Zeichnen der Komponenten wird eine eigene Rendering-Engine namens Skia benutzt. Damit das Look and Feel der jeweiligen Plattformen gewährleistet wird, hat das Flutter-Team alle wichtigen Elemente von iOS und Android nachgebaut (wobei iOS etwas stiefmütterlich behandelt wird, Androids Material Design dagegen erwartungsgemäß gut umgesetzt ist). Was im ersten Moment wie ein Nachteil klingt, ist bei Apps mit eigenem Design und Branding keiner mehr. Hier hat Flutter eindeutig seine Stärken. Was das im Einzelnen bedeutet, sehen wir im weiteren Verlauf des Artikels genauer. Doch so viel vorab: Einen UI-Design-Editor wird bei Flutter nicht mitgeliefert. Weder Xcodes Storyboard noch Androids Layouteditor werden unterstützt. Die Beschreibung des UI ist, ähnlich wie bei React oder VueJS, im Quellcode verankert. Das macht Flutter vor allem für Entwickler sehr intuitiv und einfach zu erlernen und ist weniger tragisch, als es im ersten Moment erscheint.

Dart?

Flutter basiert auf Googles hauseigener Programmiersprache Dart. Version 1.0 ist 2013 erschienen, inzwischen ist Version 2.0 aktuell. Dart hat den Anspruch, für die Web- und App-Entwicklung eine einfache, schnell zu erlernende und robuste Programmiersprache zu sein. Eine klare Syntax, viele bereits enthaltene Core-Module und ein großes Ökosystem von verfügbaren Paketen tragen ebenso dazu bei wie eine aktive Community.

Der Sprachsyntax ähnelt der von C, sodass der Umstieg von Sprachen wie C/C++, Java oder JavaScript schnell zu schaffen ist. Dart ist streng typisiert und objektorientiert. Durch die Kompilierung zu ARM- und x86-Code sind Dart-Programme auf mobilen Betriebssystemen, wie Android oder iOS, ausführbar. Ein Dart Transpiler sorgt für die Konvertierung in JavaScript-Code, was Dart auch im Web zu einer Alternative machen soll. Dart kann den Quelltext in zwei Varianten kompilieren:

  • JIT: Wie bei JavaScript wird der Code just in time während der Ausführung kompiliert bzw. interpretiert. Das ermöglicht ein Hot Reloading der Programme während der Entwicklung. Auf diesen nicht unerheblichen Vorteil während der Entwicklung kommen wir später zurück.
  • AOT: Ahead-of-Time-Kompilierung erstellt nativen ARM- oder x86-Code, der so die Ausführung auf den Endgeräten ermöglicht und damit erst die Distribution als App. Bekanntermaßen erlauben manche Plattformen keinen Interpreter in einer App.

Entwicklungsumgebung einrichten

Bevor wir mit der Entwicklung starten können, müssen wir das Flutter SDK installieren. Die Installation ist einfach und im Vorgehen für die verschiedenen Betriebssysteme sehr ähnlich. Hinweis: Wer iOS-Apps mit Flutter entwickeln möchte, muss naturgemäß einen macOS-Rechner besitzen.

Die Installation erfordert den Umgang mit einer Konsole, ganz egal auf welchem Betriebssystem. Im ersten Schritt laden wir uns das Flutter SDK für unser Betriebssystem herunter und entpacken es in ein beliebiges Verzeichnis. Unter Windows führen wir noch die flutter_console.bat auf der Konsole aus. Anschließend fügen wir das Verzeichnis unserer Path-Variablen zu. Um zu prüfen, welche anderen Tools und SDKs fehlen, führen wir in der Konsole flutter doctor aus. Damit werden die bereits vorhandenen SDKs für Android oder iOS gesucht. Zudem wird gegebenenfalls auf weitere fehlende Libraries oder Tools hingewiesen (Abb. 1).

Abb. 1: Flutter Doctor

Abb. 1: Flutter Doctor

Wer iOS-Simulatoren benutzen möchte, muss noch entsprechende Schritte zur Konfiguration durchführen. Die Dokumentation ist an dieser Stelle vorbildlich; dasselbe gilt für Android. Wurde das SDK installiert, kann die IDE entsprechend konfiguriert werden. Die Flutter-Dokumentation bietet dazu ausführliche Anleitungen für Android Studio und Visual Studio Code.

Die Anatomie einer Flutter-App

Wie bereits eingangs erwähnt, wird das User Interface (UI) einer Flutter-Anwendung im Code beschrieben. Dieser deklarative Ansatz ist vor allem für Entwickler leicht zu verstehen. Das Framework verwendet sogenannte Widgets, die die Grundlage aller Applikationen bilden. Die zentrale Idee ist, das UI durch die Kombination von Widgets zu erstellen. Jedes Widget ist eine Beschreibung eines Teils des UI. Eine Aufteilung in Views, Models, Layouts und Controller ist in Flutter nicht vorgesehen. All das ist vereint in einem Objekt, dem Widget. Das Framework ist in verschiedene Schichten aufgeteilt, in Abbildung 2 ist der grundlegende Aufbau dargestellt.

Abb. 2: Aufbau des Frameworks

Abb. 2: Aufbau des Frameworks

Im Allgemeinen haben wir bei der App-Entwicklung überwiegend Kontakt mit der Framework-Schicht, die in Dart implementiert ist. In der Dokumentation sind die verfügbaren APIs für alle Schichten beschrieben. Die Schichten unterhalb des Frameworks sind in C/C++ (Engine-Schicht) bzw. in den plattformspezifischen Sprachen (Embedder-Schicht) implementiert. Der Zugriff auf diese Schichten ist über ein Dart API möglich. In der fortgeschrittenen Entwicklung gibt es Fälle, in denen der Zugriff auf plattformspezifische APIs notwendig ist.

Um einen besseren Eindruck von der App-Entwicklung mit Flutter zu bekommen, wollen wir anhand einer einfachen Beispielapplikation die grundlegenden Konzepte des Frameworks beleuchten. Das Beispiel verwendet das API des Comicverlags Marvel, um die letzten zwanzig Spider-Man-Comics in umgekehrter Reihenfolge aufzulisten. Durch einen Tap auf eine Ausgabe in der Liste werden Details angezeigt. Der vollständige Quelltext ist auf GitHub zu finden. Wie die App am Ende aussehen soll, ist in Abbildung 3 und 4 zu erkennen.

Abb. 3: Startscreen der Comicapplikation

Abb. 3: Startscreen der Comicapplikation

Abb. 4: Detailansicht mit „Close“-Button

Abb. 4: Detailansicht mit „Close“-Button

Beginnen wir mit der Implementierung. Um den Start zu vereinfachen, legen wir das neue Projekt mit Hilfe des Plug-ins in der IDE an. Wir benutzen Visual Studio Code mit den installierten Dart- und Flutter-Plug-ins. Um das neue Projekt zu erstellen, öffnen wir die Command-Palette und wählen Flutter: New Project aus. Nach Eingabe eines Projektnamens (z. B. Spider-Man-Comics) und Auswahl eines Speicherorts legt das Plug-in alle notwendigen Dateien an. Dazu gehört ein Beispielprojekt, das ohne Probleme kompilieren sollte. Kommt es zu Fehlern beim Ausführen, ist flutter doctor ein nützlicher Hinweisgeber.

Der Einstiegspunkt jeder Flutter-App ist die Datei lib/main.dart. Der autogenerierte Inhalt wird mit Listing 1 ersetzt.

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.white,
        primaryColorDark: Colors.black87,
        secondaryHeaderColor: Colors.black,
        accentColor: Colors.black45,
        backgroundColor: Colors.white24,
        buttonColor: Colors.grey,
        fontFamily: "Roboto",
      ),
      title: 'Spider-Man Comics',
      home: new ComicsList(),
    );
  }
}

In unserem Beispiel nutzen wir für das Layout flutter/material. Das entsprechende Paket muss importiert werden. Darin sind sehr viele nützliche Widgets für die einfache Erstellung von Applikationen mit dem Look and Feel des Material Designs von Google enthalten. Die Comicliste wird als eigene Klasse implementiert und in eine Datei ausgelagert, die ebenfalls importiert wird.

Bei Flutter basiert alles auf Widgets. Deshalb wird in der main.dart das initiale Widget definiert. Es ist vom Typ MaterialApp und hat in diesem Fall drei Properties:

  • die Definition der Theme-Eigenschaften
  • ein Titel und
  • die eigentliche Implementierung des UIs.

Die Implementierung der Liste ist in eine eigene Klasse ausgelagert, die entsprechend instanziiert wird. Auf die ThemeData-Konfiguration gehen wir später ein.

Eine Flutter-Applikation startet immer mit der Funktion runApp(), der als Parameter ein Widget übergeben wird. Das Widget ist vom Typ MaterialWidget und beinhaltet die Startseite der App. Jede Seite einer Applikation ist ein Widget des Typs Scaffold.

Widgets

Schauen wir uns nun die weitere Implementierung im Detail an, insbesondere, wie mit Hilfe der Widgets das UI erstellt wird. Der Einstiegspunkt in die Applikation ist die Liste der Spider-Man-Comics (Abb. 3).

Auf Widgets heruntergebrochen hat diese Ansicht folgenden Aufbau:

  • Kopfzeile mit Überschrift (orange markiert)
  • Liste aller Comics (blau markiert)

Für die Applikation ergibt sich die Widgethierarchie in Abbildung 5.

Abb. 5: Baumstruktur der Widgets

Abb. 5: Baumstruktur der Widgets

Stateful vs. stateless Widgets

Flutter unterscheidet Widgets in stateless und stateful. Erstere sind statische Elemente, die ihr Aussehen nach der Erzeugung nicht mehr verändern. Darunter zählen beispielsweise Buttons, Texte oder Labels.
Stateful Widgets werden eingesetzt, wenn die Applikation Bereiche dynamisch neu zeichnen soll, da sich Inhalte oder Aussehen ändert (kurz: wenn sich deren Zustand verändert). In diesem Beispiel ist die Liste der Comics ein Stateful Widget. Der Inhalt ist im Vorfeld nicht eindeutig definiert, da dieser von einem API bezogen wird und vorher nicht bekannt ist, was genau zurückgeliefert wird.

IoT Conference 2019

Workshop: Deploying MQTT Container and Orchestration Software

mit Anja Helmbrecht-Schaar und Florian Raschbichler (dc-square GmbH)

Die Detailansicht ist mit einem stateless Widget implementiert, da der Inhalt nicht dynamisch geändert wird. Im Allgemeinen können stateless Widgets auch stateful Widgets beinhalten und umgekehrt. Bei der Entwicklung sollte immer mit einem stateless Widget begonnen werden. Falls sich im weiteren Verlauf der Fall ergibt, dass das Widget seinen Zustand verändert, kann der Typ angepasst werden.

In der Datei comics_list.dart bedeutet dies, dass ein stateful Widget initialisiert und mit einem Zustand (State) erzeugt werden muss:

class ComicsList extends StatefulWidget {
  @override
  ComicsListState createState() {
    return new ComicsListState();
  }
}

Der Zustand des Widgets wird in der Klasse ComicListState implementiert. Das UI wird dort beschrieben und eine Liste der Comics mit den Daten, die uns das API liefert, erzeugt. Die Methode getComicData ruft die Liste asynchron ab und füllt comicList mit den zu verwendenden Informationen. Eine globale Funktion getJson ist für das Abrufen der Daten zuständig. Das Marvel API erwartet einen Hash, der mit Hilfe der jeweiligen Libraries unkompliziert erzeugt wird (Listing 2).

Future<Map> getJson() async {
  var apiKey = 'API_KEY_HERE';
  var privAPIKey = 'PRIVATE_KEY_HERE';
  var baseUrl = 'https://gateway.marvel.com:443/v1/public/series/20432/comics?orderBy=-issueNumber';

  var timeStamp = new DateTime.now().millisecondsSinceEpoch.toString();
  var content = new Utf8Encoder().convert('$timeStamp$privAPIKey$apiKey');
  var myHash = crypto.md5.convert(content).toString();

  var url = '$baseUrl&apikey=$apiKey&hash=$myHash&ts=$timeStamp';

  http.Response response = await http.get(url);
  return json.decode(response.body);
}

Der Aufruf des Backends erfolgt mit der Zeile:

http.Response response = await http.get(url);

Das Ergebnis wird mit json.decode in eine Map konvertiert und zurückgegeben. Nach der Antwort des Backends wird durch die Methode setState() der Zustand des Widgets gesetzt, und die Map ist innerhalb der Widgets verfügbar. Flutter rendert das Widget neu, und die Daten werden angezeigt.

Hinweis: In diesem Beispiel verzichten wir bewusst auf Error Handling. Wir gehen davon aus, dass das Backend immer funktioniert und die gewünschten Daten zurückliefert.

Die vollständige Implementierung der Liste ist in Listing 3 zu sehen.

class ComicsListState extends State<ComicsList> {
  
  void getComicData() async {

    var data = await getJson();

    setState(() {
      comicList = data['data']['results'];
    });
  }

  @override
  Widget build(BuildContext context) {

    getComicData();

    return new Scaffold(
      backgroundColor: Theme.of(context).backgroundColor,
      appBar: new AppBar(

        centerTitle: true,
        backgroundColor: Theme.of(context).secondaryHeaderColor,
        title: new Text(
          'Spider-Man Comics',
          style: new TextStyle(
            color: Theme.of(context).primaryColor,
            fontFamily: 'homoarak',
            fontWeight: FontWeight.bold),
          ),
        ),
        body: new Padding (
          padding: const EdgeInsets.all(0.0),
          child: new Column (
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              new Expanded (

                child: new ListView.builder (
                  itemCount: comicList == null ? 0 : comicList.length,
                  itemBuilder: (context, i) {
                    return new FlatButton(

                      child: new ComicListCell(comicList, i),
                      padding: const EdgeInsets.all(0.0),
                      onPressed: (){
                        Navigator.push(context, new MaterialPageRoute(builder: (context){
                          return new ComicDetails(comicList[i]);
                        }));
                      },
                      color: Theme.of(context).primaryColor,
                    );
                  }
                ),
              )
            ],
          )
        )
    );
  }
}

Wir komponieren ein UI

Auf den ersten Blick wirkt der Code erschlagend. Doch bei näherem Hinsehen ist der Aufbau klar und einfach nachvollziehbar. Durch Komposition (Composition) wird das UI beschrieben. Im ersten Schritt wird neben der Hintergrundfarbe die App Bar definiert. Das ist der Bereich oberhalb der Liste, der nicht scrollen soll und unseren Titel Spider-Man Comics beinhaltet. Die Eigenschaften sind alle selbsterklärend. Der Inhalt unserer Liste wird im body dargestellt. Hierzu verwenden wir ein padding-Widget, das die Kindelemente mit einem Abstand (Padding) innerhalb des body platziert. Da die gesamte Höhe und Breite genutzt werden soll, muss das Padding auf 0.0 gesetzt werden.

Zur Darstellung von Inhalten in einem vertikalen Array kann das Column-Widget verwendet werden. Das Widget besitzt in diesem Fall ein Kindelement, das als Column-Widget mit einer Spalte definiert wird. Innerhalb von body wird die Liste erzeugt. Das Material Design Framework stellt dafür ein ListView-Widget zur Verfügung. Anhand einer Widget-Definition für jede Zelle erstellt der ListView Builder die Übersicht über die Comics. Durch Tap auf ein Listenelement wird die Detailansicht aufgerufen. Daher sind die Elemente als FlatButton-Widget implementiert. Durch die Implementierung des onPressed Handlers für den Button kann die Detailansicht aufgerufen werden.

Schauen wir uns nun die Implementierung eines Listenelements im Detail an.

Das Listenelement

Auf der linken Seite eines Listenelements soll das Vorschaubild des Comics stehen. Rechts davon gliedern sich Titel und Inhalt an (gekürzt auf drei Zeilen). Abbildung 6 zeigt den Aufbau.

Abb. 6: Listenzelle

Abb. 6: Listenzelle

Ein Listenelement besteht aus zwei Spalten. Abschließend folgt am unteren Ende eine dünne graue Linie. In der linken Spalte findet sich das Vorschaubild, die rechte ist aufgeteilt in zwei Reihen: den Titel des Comics und denie ersten Wörter aus der Beschreibung, nicht länger als drei Zeilen. Die Implementierung der Widgets entspricht dem Code aus Listing 4.

Widget build(BuildContext context) {
    return new Column(
      children: <Widget>[
        new Row(
          children: <Widget>[
            new Padding(
              padding: const EdgeInsets.all(0.0),
              child: new Container(

                margin: const EdgeInsets.all(16.0),
                child: new Container(
                  width: 80.0,
                  height: 80.0,
                ),

                decoration: new BoxDecoration(
                  //borderRadius: new BorderRadius.circular(4.0),
                  color: Theme.of(context).backgroundColor,
                  image: new DecorationImage(
                      image: new NetworkImage(
                        comics[i]['images'][0]['path'] + "." + comics[i]['images'][0]['extension']
                      ),
                      fit: BoxFit.cover
                    ),

                  boxShadow: [
                    new BoxShadow(
                      color: Theme.of(context).primaryColorDark,
                      blurRadius: 5.0,
                      offset: new Offset(1.0, 1.0)
                    )
                  ],
                ),
              ),
            ),

            new Expanded(
                child: new Container(
                  margin: const EdgeInsets.fromLTRB(16.0,0.0,16.0,0.0),
                  child: new Column(children: [

                    new Text(
                      comics[i]['title'],
                      style: Theme.of(context).textTheme.title,
                    ),

                    new Padding(padding: const EdgeInsets.all(2.0)),

                    new Text(comics[i]['description'],
                      maxLines: 3,
                      style: new TextStyle(
                        color: Theme.of(context).accentColor,
                        fontFamily: 'roboto'
                      ),
                    )
                  ],
                  crossAxisAlignment: CrossAxisAlignment.start,
                ),
              )
            ),
          ],
        ),

        new Container(
          width: 400.0,
          height: 0.5,
          color: Theme.of(context).accentColor,
          margin: const EdgeInsets.all(0.0),
        )

      ],
    );
  }
}

Die Zelle ist eine Komposition aus einem Column– mit einem Row-Widget. Ein Row-Widget dient der horizontalen Darstellung von Widgets. An erster Stelle der Reihe steht das Bild, das wir in einem Padding-Widget mit einem Container-Widget darstellen. Die Breite ist fix mit 80×80 logischen Pixeln definiert. Durch Setzen der BoxDecoration erhält das Bild abgerundete Ecken und einen Schlagschatten.

Das zweite Widget in der Reihe ist ein Expand-Widget, das den restlichen verfügbaren Raum einnimmt. Je nach Auflösung des mobilen Endgeräts kann das mehr oder weniger sein. Darin wird ein Column-Widget mit drei Elementen definiert: ein Text-Widget gefolgt von einem kleinen Abstand (Padding) und wieder ein Text-Widget. Dem zweiten Text-Widget geben wir als Parameter maxLines: 3 mit, um den Raum nicht unendlich lang werden zu lassen. Durch ein abschließendes Container-Widget wird die Trennlinie erzeugt.

Hot Reload

Auch in diesem Fall hört sich die Beschreibung des UI komplizierter an als es letztendlich ist. Wer im Vorfeld klar plant und strukturiert, hat den ersten Entwurf schnell implementiert. Die Detailanpassungen können anschließend einfach umgesetzt werden. Wie bereits am Anfang erwähnt, erlaubt Flutter Hot Reloading des Codes innerhalb eines Emulators/Simulators oder auf dem Endgerät. Wird die Applikation im Debugmodus gestartet, werden die Änderungen beim Speichern einer Datei umgehend eingespielt und ausgeführt.

Das Plug-in für VSCode bietet einige Shortcuts und einen Debugger, der die Entwicklung und das Testen sehr komfortabel machen. Bei einem Hot Reload wird nicht die vollständige Applikation neu geladen und gestartet, sondern nur die geänderten Dateien und ggf. neu gerendert, sofern die View gerade angezeigt wird. Die Darstellung wird sofort aktualisiert, was das Spielen mit Abständen, Farben oder Layouts ermöglicht. Werden grundlegende Konfigurationen der Applikation angepasst, wird ein Hot Restart durchgeführt. Das alles übernimmt die IDE bzw. die Plug-ins für unsere IDE beim Speichern der bearbeiteten Datei. Der Entwickler kann sich vollständig auf seinen Code und die Darstellung in Simulator oder Endgerät konzentrieren.

Von der Liste zum Detail

Nachdem das Layout der Listenelemente in einer eigenen Klasse definiert ist, kann sie im Itembuilder der Listenansicht als Child-Items instanziiert und mit Inhalten gefüllt werden. Der Itembuilder erzeugt in diesem Schritt für jedes Comic eine Zelle in der Liste. Um die Details bei Tap auf ein Listenelement anzuzeigen, wird das Navigator-Objekt verwendet. Vereinfacht gesagt ist das ein Stack, auf den Widgets gelegt und dargestellt werden. Die Detailansicht ist, wie die Listenansicht, eine Komposition von Widgets, die eine Seite repräsentiert (Scaffold-Widget). Dazu wird im Navigator eine Instanz der ComicDetails-Klasse erzeugt und auf den Stack gelegt. Die Detailansicht ist ähnlich aufgebaut wie die Listenansicht, mit der Ausnahme, dass sie als Stateless-Widget umgesetzt ist, da Daten sich nach dem Erstellen nicht wieder ändern.

Auf die Komposition der Seite werden wir nicht näher eingehen. Auch bei dieser Ansicht wird eine Kombination von Layoutwidgets verwendet. Am unteren Ende der Detailansicht gibt es einen FlatButton, der es ermöglicht, die Detailansicht wieder zu verlassen (Abb. 4). Die Funktionalität ist denkbar einfach umzusetzen (Listing 6).

...
new Row(
  children: <Widget>[

  new Expanded(
    child: new FlatButton(
      child: new Container(
        width: 400.0,
        height: 60.0,
        alignment: Alignment.center,
        child: new Text(
        'Close',
        style: new TextStyle(
          color: Theme.of(context).primaryColor,
          fontFamily: 'Arvo',
          fontSize: 20.0),
        ),
        decoration: new BoxDecoration(
        borderRadius: new BorderRadius.circular(5.0),
        color: Theme.of(context).buttonColor
        ),
      ),
      onPressed: () {
        Navigator.pop(context);
      },
      )
  ),

  ],
)
...

Die Implementierung des onPressed Handlers ruft lediglich Navigator.pop(context) auf. Damit wird die Detailansicht von Stack genommen, und die Listenansicht wird angezeigt. Mit diesem Schritt ist die Applikation vollständig funktional umgesetzt.

Das Beispiel erklärt nur einen kleinen Umfang von Flutter. Es zeigt auch, wie einfach eine datenbasierte Applikation umgesetzt werden kann, abseits von plattformspezifischen Eigenschaften und Workflows.

Themes

Wer den Quelltext aufmerksam liest, wird feststellen, dass die Farben in der Applikation über ein Theme definiert sind. Das Material Framework bietet Konfigurationsmöglichkeiten, um bspw. Farben zentral zu definieren. Verzichtet man auf diese Möglichkeit, muss für jedes Element die Farbe unter Verwendung der Color-Klasse gesetzt werden:

var myColor1 = const Color(0xFFFFC400)
var myColor2 = const Color.fromARGB(0xFF, 0xFF, 0xC4, 0x00);
var myColor3 = const Color.amberAccent[400]

Themes helfen, die Farbwelt der App einheitlich zu gestalten. In der main.dart werden die Farben für die Beispielapplikation definiert und in den jeweiligen Views wiederverwendet. Themes bieten noch mehr Möglichkeiten, wie die Definition der Texteigenschaften. Es ist sinnvoll, bei der Entwicklung von Anfang an Themes einzusetzen, da nachträgliche Korrekturen an der Farb- und Schriftwelt somit leichter fallen. Der Zugriff auf einen Theme-Wert ist einfach:

{ 
...
  color: Theme.of(context).primaryColor
...
}

Der Farbwert primaryColor des Themes aus dem aktuellen Context wird der Eigenschaft color: zugewiesen. Ändert sich der definierte Farbcode für primaryColor, ist an jeder Stelle der Applikation der Wert neu gesetzt.

Wie „Cross Platform“ ist Flutter?

Flutter geht mit dem vollmundigen Versprechen an den Start, echte Plattformunabhängigkeit zu bieten. Das gelingt bei der UI-Gestaltung schon sehr gut. Tatsächlich wird eine einzige Codebasis für die verschiedenen Plattformen genutzt. Das wird durch das eigene Rendering möglich. Ein Nachteil ist jedoch, dass die Applikationen im Aussehen und Verhalten nicht zu 100 Prozent dem jeweiligen Betriebssystem entsprechen. Allerdings erheben die Apps von heute auch nicht mehr unbedingt Anspruch darauf. Gerade bei der Entwicklung von eigenen UIs ist man mit Flutter sehr flexibel.

Der deklarative Ansatz ermöglicht es Entwicklern, schnell zu einem Ergebnis zu kommen. Layouts werden durch Komposition erstellt und beschrieben; vor allem beim Prototyping von Apps ist das ein großer Vorteil. Die Widgets für das Material Design sind durch die Bank durchdacht und gut umgesetzt. Applikationen sind in kürzester Zeit auf verschiedenen Simulatoren (iOS) oder Emulatoren (Android) testbar. Auch das Deployment auf reale Hardware ist schnell eingerichtet, und das Hot Reloading während der Entwicklung ist eine tolle Sache. Iterationszyklen sind sehr viel schneller, als immer wieder eine App neu zu bauen und als Ganze zu deployen.

Ob und wie sich Flutter in Zukunft durchsetzen wird, ist schwer zu beurteilen. Facebooks React Native ist mit denselben Ansprüchen angetreten, und einige Unternehmen wechseln wieder zurück zur nativen, plattformspezifischen Entwicklung (z. B. Airbnb). Denn eins darf man nicht verschweigen: Die Performance von Flutter-Apps ist naturgemäß schlechter als bei nativen Apps. Für einfache Applikationen ist das zu vernachlässigen. Für komplexe Spielereien mit vielen Animationen kann das aber einen Unterschied machen. Hier muss der Entwickler im Vorfeld wissen, ob das ausschlaggebend ist oder nicht.

Für mich persönlich ist Flutter ideal, um App-Ideen schnell umzusetzen und zu testen. Dart ist eine einfache und leicht zu erlernende Sprache. Die mitgelieferten Widgets sowie die verfügbaren Packages sind umfangreich. Dabei handelt es sich sowohl um Plug-ins für plattformspezifische Funktionalitäten (GPS, Kamerazugriff etc.), als auch um allgemeine Widgets etwa zur Verwendung von Google Maps oder zur Nutzung diverser UI-Funktionen (z. B. Carousels etc.).

Fazit

Flutter ist durchdacht und macht wirklich Spaß. Schnell eine App-Idee umsetzen oder etwas mit einem Proof of Concept testen? Mit Flutter kein Problem. Wer sich in das Widgetsystem eingearbeitet hat, wird es zu schätzen wissen. Vor allem das Hot Reloading trägt zum Entwicklungsspaß bei. Das mitgelieferte Tooling ist durchdacht und funktioniert out of the box. Entwickler müssen nicht verschiedene IDEs der jeweiligen Plattform nutzen. Mein Fazit: Flutter ist ein ernstzunehmender Mitbewerber auf dem Feld der Cross-Plattform-Entwicklung und sollte bei einer Evaluation eines geeigneten Frameworks in Betracht gezogen werden.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

1 Kommentar auf "Flutter: App-Entwicklung mit Googles Cross Platform Framework"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Tobias Schiek
Gast

Zitat: „Denn eins darf man nicht verschweigen: Die Performance von Flutter-Apps ist naturgemäß schlechter als bei nativen Apps.“

Warum? Flutter-Apps werden zu nativem ARM-Code kompiliert? Da ist nichts dazwischen. Was sollte hier langsamer sein?! Die Flutter UI rendert mit 60 FPS nativ.

X
- Gib Deinen Standort ein -
- or -