Aus Linux-Magazin 03/2022

Apps programmieren mit Flutter

© nebuto / 123RF.com

Wer in der Vergangenheit Apps für Android, iOS, den Webbrowser oder den Desktop entwickeln wollte, musste meist unterschiedliche Codes schreiben. Mit Googles neuem Framework Flutter gelingt das nun mit einer einzigen Codebasis.

Googles neuer Softwarespross Flutter [1] ist nicht der erste Versuch, die mobile App-Entwicklung zu vereinheitlichen. Mit Konkurrenten wie Microsofts Xamarin oder React Native aus dem Hause Facebook gibt es schon Lösungen in dieser Richtung. Flutter punktet aber mit einer hervorragenden Dokumentation, hoher Performance und der Fähigkeit, neben Apps für Android auch solche für Linux, MacOS, Windows sowie für das Web zu generieren.

Im Gespann mit der freien Entwicklungsumgebung Android Studio lassen sich so komfortabel und einfach Apps für alle diese Plattformen entwickeln. In Form der Website [2] gibt es zudem eine Anlaufstelle für die Suche nach Packages, die sich in Flutter nachinstallieren lassen. Dort finden Sie nach etlichen Kriterien sortierbare Packages für jeden erdenklichen Zweck, die die eigene Flutter-App um gewünschte Funktionen erweitert.

Dass man mit der integralen Programmiersprache Dart für Flutter eine neue Sprache lernen muss, sieht nur auf den ersten Blick nach einem Hindernis aus. Wer schon Kenntnisse in einer anderen objektorientierten Sprache wie Java, Javascript oder C# hat, fühlt sich schnell zu Hause, da sich die Syntax dieser Sprachen ähnelt.

Installation

Es genügen wenige Schritte, um sich das neue Open-Source-Framework auf den Rechner zu holen. Am einfachsten richten Sie Flutter über Snap ein (Listing 1). Die Webseite des Projekts erläutert weitere Details für Linux [3]. Mit »flutter doctor -v« prüfen Sie, welche Komponenten es noch zu installieren gilt. Der Befehl »flutter upgrade« aktualisiert das Framework auf die neueste Version.

Listing 1

Flutter einrichten

$ sudo snap install flutter --classic

Als Entwicklungsumgebung empfiehlt sich die auf der IDE IntelliJ IDEA von Jetbrains basierende Entwicklungsumgebung Android Studio, die ebenfalls von Google stammt. Die neueste Version lässt sich kostenlos herunterladen [4]. Nach der Installation müssen Sie noch die Plugins für Flutter und Dart nachinstallieren. Das erledigen Sie aus der IDE heraus im Willkommensbildschirm unter dem Punkt Plugins. Mit dem Flutter-Plugin wird auch gleich noch das Plugin für die Programmiersprache Dart mitinstalliert.

Einstieg

Legen Sie mit Android Studio über File | New | New Flutter Project… ein neues Flutter-Projekt an, entsteht eine wahre Flut an Verzeichnissen und Dateien. Für den Anfang interessieren jedoch nur zwei davon. Sehen Sie sich zuerst die Datei »main.dart« im Ordner »lib/« genauer an: Sie enthält den eigentlichen Dart-Quellcode. Diese Datei wird auch ausgeführt, sobald Sie das Projekt starten. Den enthaltenen Beispielcode löschen Sie erst einmal komplett.

In guter alter Tradition erstellen Sie nun zuerst eine kleine App, die lediglich den Text “Hallo Welt” ausgibt. Den Code dazu aus der Datei »main.dart« zeigt Listing 2. Die erste Zeile importiert das Package »flutter/material.dart«. Dabei handelt es sich um eine Bibliothek mit Widgets im Material-Design von Google. Sie zählt zu den Standardbibliotheken von Flutter, sodass Sie sie nicht nachinstalliert müssen. Alternativ gibt es auch Widgets im Cupertino-Stil, die das Aussehen einer native iOS-App imitieren. Wir beschränken uns hier aber auf das Material-Design.

Listing 2

Hello World

import 'package:flutter/material.dart';
void main() {
  runApp(HelloWorldApp());
}
class HelloWorldApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hallo Welt',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Hallo Welt'),
        ),
        body: Center(
          child: Text('Hallo Welt'),
        ),
      ),
    );
  }
}

Der Quellcode einer Flutter-App in Flutter ist so ähnlich aufgebaut wie die Baumstruktur eines HTML-Dokuments. Es gibt verschiedene ineinander verschachtelte Knoten, die mehrere Enden haben können. Ein Knoten entspricht dabei einem Widget, man spricht im Kontext von Flutter auch von einem Widget-Tree. In Flutter herrscht das Credo “alles ist ein Widget”, was bedeutet, dass alle grafischen Elemente einer Flutter-App Widgets sind.

In der dritten Zeile befindet sich der Einsprungpunkt der App mit der Main-Funktion. Hier ruft die Funktion »runApp()« die Klasse »HelloWorldApp« auf, die Wurzel des Widget-Trees. Ab Zeile 7 folgt die Deklaration und Definition dieser Klasse. Sie erbt mit »extends« die Funktionalität der Klasse »StatelessWidget«. Wie der Name schon sagt, handelt es sich bei »StatelessWidget« um ein Widget, das seinen Zustand nicht verändert. In diesem Beispiel zeigt es lediglich einen Text an. Das Gegenteil wäre »StatefulWidget«, das seinen Zustand in der UI ändern kann.

Die abstrakte Build-Methode in Zeile 9, die überschrieben wird, erstellt die Oberfläche mit den Widgets. Das Argument »BuildContext« interessiert für den Anfang nicht, Sie ignorieren es fürs Erste. Innerhalb der Build-Methode wird ein »MaterialApp« zurückgegeben, das die grafische Oberfläche beschreibt. Der erste Parameter »title« gibt den Titel des App-Fensters an. Das Scaffold (Gerüst) unter dem Punkt »home« deklariert in Zeile 13 eine »AppBar«, die als Parameter den Titel »’Hallo Welt’« zugewiesen bekommt.

Der Abschnitt namens »body« enthält den eigentlichen Inhalt der App, ähnlich wie bei einem HTML-Dokument. Hier umfasst er lediglich das Widget »Center«, das den gesamten Inhalt zentriert. Es enthält ein Text-Widget mit dem String »’Hallo Welt’«. Das Ergebnis sehen Sie in Abbildung 1.

Abbildung 1: So präsentiert sich die fertige Hello-World-App.

Abbildung 1: So präsentiert sich die fertige Hello-World-App.

Konfigurationsdatei

Neben der Datei »main.dart« zählt die Konfigurationsdatei »pubspec.yaml« im Wurzelverzeichnis des Projekts zu den Dreh- und Angelpunkten einer Flutter-App. In ihr notieren Sie Metadaten wie zum Beispiel die benötigten externen Packages (auch Pubs genannt). Die Integration eines Packages geschieht immer nach demselben Schema. Haben Sie im Repository [2] ein geeignetes Package gefunden, schlagen Sie unter dem Punkt Installing auf derselben Webseite nach, wie das Package zu installieren ist.

Zuerst vermerken Sie das Package in der Sektion »dependencies«, entweder auf der Kommandozeile mit »flutter pub add Package« oder manuell in Android Studio. Danach wechseln Sie in Android Studio wieder zur Datei »main.dart«. Die Entwicklungsumgebung erkennt selbstständig, dass die Datei »pubspec.yaml« modifiziert wurde, und schlägt am oberen Bildschirmrand das Kommando Get dependencies vor. Klicken Sie auf den Link, lädt die IDE das Package herunter und installiert es. Danach binden Sie das Package mit einer »import«-Anweisung in der Datei »main.dart« ins Projekt ein.

Wetter-App

Unsere nächste Beispiel-App in Listing 3 zeigt das aktuelle Wetter an und verwendet dazu zwei externe Packages. Die aktuellen Wetterdaten stellt der Online-Dienst OpenWeatherMap zur Verfügung. Sie könnten die Wetterdaten aus dieser Quelle im Datenformat JSON empfangen und auswerten. Für OpenWeatherMap gibt es aber ein Flutter-Package namens »weather« [5], das diese Prozedur vereinfacht. Sie benötigen nur einen API-Key, den Sie kostenlos auf der Homepage von OpenWeatherMap bekommen können. Solch ein Schlüssel berechtigt dazu, die Wetterdaten abzurufen.

Das zweite Package namens »shared_preferences« [6] speichert diesen API-Key lokal auf der Zielplattform, damit man den Schlüssel nicht nach jedem Neustart erneut eingeben muss. Dieses sehr beliebte Package kommt in der Flutter-Entwicklung sehr häufig zum Einsatz, da sich damit unkompliziert geringe Datenmengen speichern lassen, wie hier eine Zeichenkette. So umgeht man die Nutzung einer ausgewachsenen Datenbank, was im konkreten Fall technischer Overkill wäre.

Listing 3

Wetter-App

import 'package:flutter/material.dart';
import 'package:weather/weather.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Wetter-App',
      home: Scaffold(resizeToAvoidBottomInset: false, appBar: AppBar(title: Text('Wetter-App')),
      body: Center(
          child: Column(
          children: [
            Image.asset('assets/images/weather_icon.png'),
            Text('Aktuelles Wetter', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24),),
            Flexible(child: MyHomePage())
        ],),
      ),),
    );
  }
}
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  final _city = TextEditingController();
  final _apiKey = TextEditingController();
  var _temperature = '';
  var _weather = '';
  var _pressure = '';
  var _windSpeed = '';
  var _humidity = '';
  var _lastUpdate = '';
  void _showApiKeyInputDialog() async {
    final prefs = await SharedPreferences.getInstance();
    if(prefs.containsKey('apikey')){
      var _key = prefs.getString('apikey');
      showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text('API-Key angeben'),
              content: TextField(
                onChanged: (value) {
                },
                controller: _apiKey..text = _key.toString(),
                decoration: InputDecoration(hintText: 'API-Key'),
                onSubmitted: (value) async {
                  await prefs.setString('apikey', _apiKey.text);
                },
              ),
              actions: [TextButton(child: Text('DELETE KEY'), onPressed: () async {
              await prefs.remove('apikey'); _apiKey.clear(); Navigator.pop(context);}), TextButton(
                child: Text("CANCEL"),
                onPressed:  () {Navigator.pop(context);},
              ), TextButton(
                child: Text('OK'),
                onPressed: () async { await prefs.setString('apikey', _apiKey.text); Navigator.pop(context);},
              )],
          );}
      );
    }
    else {
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text('API-Key angeben'),
            content: TextField(
              onChanged: (value) {
              },
              controller: _apiKey,
              decoration: InputDecoration(hintText: 'API-Key'),
              onSubmitted: (value) async {
                await prefs.setString('apikey', _apiKey.text);
              },
            ),
            actions: [TextButton(child: Text('DELETE KEY'), onPressed: () async {
            await prefs.remove('apikey'); _apiKey.clear(); Navigator.pop(context);}),
            TextButton(
              child: Text("CANCEL"),
              onPressed:  () {Navigator.pop(context);},
            ),
            TextButton(
              child: Text('OK'),
              onPressed: () async { await prefs.setString('apikey', _apiKey.text); Navigator.pop(context);},
            )],
      );});
    }
  }
  void _displayWeather() async {
    final prefs = await SharedPreferences.getInstance();
    var _key = prefs.getString('apikey').toString();
    WeatherFactory wf = new WeatherFactory(_key, language: Language.GERMAN);
    Weather w = await wf.currentWeatherByCityName(_city.text);
    double? celsius = w.temperature!.celsius;
    setState(() {
      _temperature = celsius!.round().toString() + ' Grad Celsius';
      _weather = w.weatherDescription!;
      _pressure = w.pressure.toString() + ' hPa';
      _windSpeed = w.windSpeed.toString() + ' km/h';
      _humidity = w.humidity.toString() + ' %';
      _lastUpdate = w.date!.toIso8601String().replaceFirst('T', ' ').replaceRange(w.date!.toIso8601String().lastIndexOf('.')-1, w.date!.toIso8601String().lastIndexOf('0'), '');
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(resizeToAvoidBottomInset: false,
      body: ListView(children: [Container(child: Column(children: [
        SizedBox(height: 10),
        TextFormField(decoration: const InputDecoration(hintText: 'Bitte eine Stadt angeben', labelText: 'Stadt'), controller: _city,),
        SizedBox(height: 10),
        Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Temperatur: ', style: TextStyle(fontSize: 18),),
          Text(_temperature.toString(), style: TextStyle(fontSize: 18)),]),
        SizedBox(height: 10),
        Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Wetter: ', style: TextStyle(fontSize: 18),),
          Text(_weather, style: TextStyle(fontSize: 18)),]),
        SizedBox(height: 10),
        Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Luftdruck: ', style: TextStyle(fontSize: 18),),
          Text(_pressure.toString(), style: TextStyle(fontSize: 18)),]),
        SizedBox(height: 10),
        Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Windgeschwindigkeit: ', style: TextStyle(fontSize: 18),),
          Text(_windSpeed, style: TextStyle(fontSize: 18)),]),
        SizedBox(height: 10),
        Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Luftfeuchtigkeit: ', style: TextStyle(fontSize: 18),),
          Text(_humidity, style: TextStyle(fontSize: 18)),]),
        SizedBox(height: 10),
        Row(mainAxisAlignment: MainAxisAlignment.start, children: [Text('Zuletzt aktualisiert: ', style: TextStyle(fontSize: 18),),
          Text(_lastUpdate, style: TextStyle(fontSize: 18)),]),
        SizedBox(height: 10),
        SizedBox(width: double.infinity, height: 35, child: ElevatedButton(onPressed: _displayWeather, child: const Text('Wetter anzeigen')),),
      ],))],),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _showApiKeyInputDialog, icon: Icon(Icons.add), label: Text('API key'), tooltip: 'API-Key angeben',)
    );
  }
}
Abbildung 2: So bietet die Wetter-App dem Nutzer die neuesten Wetterdaten dar.

Abbildung 2: So bietet die Wetter-App dem Nutzer die neuesten Wetterdaten dar.

Die Grafik mit der Sonne ganz oben in der Oberfläche der Beispiel-App (Abbildung 2) liegt im PNG-Format vor. Um eine Bilddatei wie diese einfügen zu können, müssen Sie sie in »pubspec.yaml« deklarieren, indem Sie den Pfad zur Datei angeben. Gewöhnlich liegen solche externen Ressourcen im Ordner »assets/« des Flutter-Projekts. In der Sektion »flutter« notieren Sie den Dateipfad zur Bilddatei, wie in Listing 4 gezeigt. Dann fügen Sie die Grafik in der »build«-Methode des »StatelessWidget«, wie in Zeile 18 der Datei »main.dart« (Listing 3) zu sehen, in die UI ein. Das umschließende Center-Widget zentriert die Grafik.

Listing 4

Grafik-Asset

flutter:
  assets:
    - assets/images/weather_icon.png

In Zeile 14 wird dem Home-Bereich ein Scaffold übergeben. Er gruppiert eine App-Bar mit dem Text »’Wetter-App’« zusammen mit der auf »false« gesetzten Option »resizeToAvoidBottomInset«. Wenn sich das grafische Keyboard der App öffnet, kann es dadurch die Widgets überlagern. Das Text-Widget in Zeile 19 formatiert die Überschrift »’Aktuelles Wetter’«. Der erste Parameter gibt den gewünschten String an, über den zweiten legen Sie mittels des Widgets »TextStyle« die Art und Größe der Schrift fest. In diesem Fall erscheint die Überschrift fett (»fontWeight«) und in 24 Punkt Größe (»fontSize«). In Zeile 20 fügt das Widget »Flexible« über sein Child-Attribut das »StatefulWidget« namens »MyHomePage« in die UI ein. Mittels des Widgets »Flexible« passt sich das untergeordnete Widget dynamisch der Größe an. »MyHomePage« ist ein »StatefulWidget«, weil es den Bereich der grafischen Oberfläche definiert, der sich ändern kann. Deswegen erbt es in Zeile 27 die Funktionalität der Klasse »StatefulWidget«.

Der API-Key

Die Zeile 42 erstellt eine asynchrone Funktion »_showApiKeyInputDialog«, die dazu dient, den API-Key abzufragen. Der Unterstrich am Anfang des Funktionsnamens bewirkt, dass die Funktion nur in der eigenen Dart-Datei sichtbar ist. Das gilt auch für Variablen- und Klassennamen. Der Aufruf der Funktion erfolgt, sobald man auf den »FloatingActionButton« rechts unten in der App klickt. Zeile 143 definiert den entsprechenden Button. Der Parameter »onPressed« gibt dabei die beim Betätigen aufzurufende Funktion an. In diesem Fall wird ein »AlertDialog« aus der Material-Bibliothek angezeigt, in den man den zuvor erstellten API-Key von OpenWeatherMap eintragen muss.

Wie schon erwähnt, wird der Key mit »shared_preferences« lokal gespeichert, was denkbar einfach klappt. Zeile 43 legt innerhalb der Funktion »_showApiKeyInputDialog« eine Instanz von »shared_preferences« namens »prefs« an. Das geschieht mit dem Schlüsselwort »await« asynchron. Der eigentliche String landet als Schlüssel-Wert-Paar im Cache, ähnlich wie bei einem assoziativen Array. In diesem Beispiel dient der String »apikey« als Schlüssel.

In der Zeile 44 prüft mit »prefs.containsKey(‘apikey’)«, ob bereits ein solcher Schlüssel auf dem Zielgerät existiert. Falls ja, liest die Methode »prefs.getString(‘apikey’)« dessen Wert aus und weist ihn der Variablen »_key« zu. Zeile 46 ruft mit »showDialog()« schließlich den Dialog auf. Dazu gibt Zeile 49 wie erwähnt einen »AlertDialog« zurück, dessen »TextField« den API-Key dann anzeigt.

Falls sich noch kein API-Key lokal im Cache befindet, zeigt der Else-Zweig ab Zeile 74 einen gleichen Dialog an, jedoch mit einem leeren Textfeld. In beiden Fällen hat der »AlertDialog« drei Optionen: DELETE KEY löscht den API-Key aus dem Cache, CANCEL schließt den Dialog, und OK speichert den aktuellen Eintrag im Textfeld als API-Key. Das erledigen drei »TextButtons« unter dem Parameter »actions« in Zeile 60 respektive 86. Am Ende schließt jeweils »Navigator.pop(context)« den Dialog wieder.

Das Gegenstück zu »prefs.getString(‘apikey’)«, um den API-Key auszulesen, ist die Methode »prefs.setString(‘apikey’, _apiKey.text)«, die einen String lokal speichert. Davon machen die Zeilen 66 und 94 Gebrauch, um mit »shared_preferences« den API-Key zu speichern. Der erste Parameter gibt den Schlüssel an, der zweite den String, der unter diesem Schlüssel gespeichert werden soll. In diesem Fall besteht der zweite Parameter aus einem »TextEditingController«, der mit dem Textfeld aus der Funktion »_showApiKeyInputDialog« verbunden ist und den API-Key dort ausliest. Möchte man mit DELETE KEY den API-Key aus dem Cache löschen, geschieht das mit der Methode »prefs.remove(‘apikey’)« in Zeile 61 beziehungsweise 87. Das anschließende »_apiKey.clear()« setzt den Wert des »TextEditingController« auf »empty«.

Das Wetter

Zeile 100 formuliert eine weitere asynchrone Funktion namens »_displayWeather()«. Sie zeigt das Wetter an und wird ausgeführt, wenn man auf den Button Wetter anzeigen klickt. Zeile 141 fügt der grafischen Oberfläche diesen »ElevatedButton«hinzu, wie das betreffende Button-Widget in Flutter heißt. Mit der zweiten Option »child« kann man mittels eines Text-Widgets den Button beschriften. Haben sich die Wetterdaten geändert, weist Zeile 106 mit »setState()« die neuen Werte den Variablen zu und aktualisiert die Text-Widgets in der Oberfläche.

Die Anzeige des aktuellen Wetters erfordert eine weitere Instanz von »SharedPreferences« (Zeile 101). Die folgenden Zeile liest wieder mittels der Methode »prefs.getString(‘apikey’)« den aktuellen Wert im Cache aus und weist ihn der Variablen »_key« zu. In Zeile 103 kommt dann erstmals die Klasse WeatherFactory ins Spiel, mit deren Hilfe man die Wetterdaten vom Online-Dienst OpenWeatherMap abruft. WeatherFactory gehört zum in Zeile 2 importierten Package »weather«. Das Schlüsselwort »new« instanziiert ein Objekt dieser Klasse namens »wf«. Als ersten Parameter erhält der Konstruktor den API-Key in der Variablen »_key«. Mit dem zweiten optionalen Parameter »language« lässt sich die Sprache der Wetteranzeige definieren.

Das Objekt »wf« bringt eine Methode namens »currentWeatherByCityName« mit, die als Parameter die Stadt entgegennimmt, deren Wetter die App anzeigen soll. In diesem Fall dient wieder ein »TextEditingController« als Parameter, um die angegebene Stadt im Textfeld aus Zeile 121 auszulesen. Initialisiert wurde die Variable des »TextEditingController« in Zeile 33 sowie mit der Eigenschaft »controller« in Zeile 121. Über die Eigenschaft »text« dieses Controllers lässt sich der gerade im assoziierten Textfeld stehende String auslesen.

Hat man der Methode »currentWeatherByCityName« als Parameter die Stadt mitgegeben, ermittelt man mit dem dazugehörigen Attribut »temperature« die Temperatur dieses Orts. Wie in Zeile 107 zu sehen, lässt sich die Temperatur in Grad Celsius oder alternativ in Fahrenheit oder Kelvin anzeigen. Das Objekt »w« aus Zeile 104 hat noch weitere Attribute, wie »weatherDescription«, das das Wetter beschreibt, und »pressure«, das den Luftdruck angibt. Analog dazu liefern »windSpeed« die Windgeschwindigkeit, »humidity« die Luftfeuchte und »date« den Zeitpunkt der Wettermessung. Die entsprechenden Variablen für die einzelnen Wettereigenschaften wurden ab Zeile 35 initialisiert.

Die Oberfläche

Die Build-Methode ab Zeile 117 erstellt die eigentliche Oberfläche der App. Zeile 118 gibt dazu mit »return« ein Scaffold zurück, das die Widgets ineinander verschachtelt. Zeile 119 im Body-Bereich fügt eine »ListView« ein, die alle übrigen Widgets umschließt. Sie bündelt alle Child-Widgets mit dem Parameter »children« und ordnet sie an.

Die diversen »SizedBox«-Widgets dienen dazu, Abstände zwischen den Text-Widgets festzulegen. In diesem Beispiel fügen Sie jeweils 10 Pixel vertikal zwischen den Text-Widgets ein, die die Wetterdaten anzeigen. Das Container-Widget »Row« ordnet hier in jeder Zeile zwei Text-Widgets (eines mit der Wettereigenschaft und eines, das den zugehörigen Wert anzeigt) in horizontaler Ausrichtung an. Die Eigenschaft »MainAxisAlignment.start« sorgt für eine linksbündige Positionierung des Child-Widgets.

Zeile 143 deklariert die Schaltfläche mit dem Text Wetter anzeigen. Eine »SizedBox« sorgt dafür, dass sich der Button über die gesamte Breite des Displays erstreckt. Dazu benötigt man die Eigenschaft »width« mit dem Wert »double.infinity«. Der Button nimmt damit die Größe des Parents an, in diesem Fall der »SizedBox«. Die zweite Eigenschaft »height« legt zusätzlich die Höhe fest. Die eigentliche Schaltfläche wird wie gewohnt als Child-Widget der »SizedBox« erstellt. Wie schon bekannt, ruft »onPressed« die bereits beschriebene Funktion »_displayWeather« auf. Die Beschriftung des Buttons gibt ein Text-Widget am Ende an.

Zu guter Letzt erzeugt Zeile 143 in der App rechts unten den schon angesprochenen »FloatingActionButton«. Klickt man darauf, führt die App die bereits besprochene Funktion »_showApiKeyInputDialog« aus. Die Eigenschaft »icon« legt ein Symbol fest, das die Schaltfläche ziert, im konkreten Fall ein Pluszeichen. Wie zu erwarten, gibt »label« die Beschriftung des Buttons an. Zum Schluss definiert »tooltip« noch einen String, der erscheint, wenn man mit dem Mauszeiger über den Button fährt.

Emulator

Die erstellte App testen Sie entweder direkt mit einem Mobiltelefon oder verwenden innerhalb von Android Studio einen Emulator, der ein physikalisches Smartphone per Software nachahmt. Dazu rufen Sie über den Menüpunkt Tools den AVD-Manager auf. Im sich öffnenden Fenster legen Sie links unten über Create Virtual Device ein neues virtuelles Gerät an. Das virtuelle Gerät lässt sich anschließend aus dem Ausklappmenü rechts oben auswählen. Um die App darin zu zu starten, klicken Sie auf den grünen Pfeil daneben. Weitere Infos liefert die Android-Studio-Webseite [7].

Deployment

Läuft die selbst programmierte App wie gewünscht, erzeugen Sie in Android Studio über das Menü Build | Flutter | Build APK eine APK-Datei, die Sie anschließend im Ordner »build/app/outputs/flutter-apk/« finden. Bei einem solchen APK-File handelt es sich um eine Installationsdatei, die Sie auf ein Android-Smartphone übertragen und dort installieren können.

Bevor Sie jedoch das Beispiel der Wetter-App als APK-Datei für Android kompilieren, müssen Sie noch, wie in Listing 5 gezeigt, die Zeile »<uses-permission…« so in die Datei »AndroidManifest.xml« einfügen. Sie finden sie im Verzeichnis »android/app/src/main/«. Damit bekommt die App auf dem Android-Smartphone das für den Abruf der Wetterdaten bei OpenWeatherMap benötigte Zugriffsrecht auf das Internet. Bei Apps, die keinen Internet-Zugang benötigen, können Sie diesen Schritt auslassen. Auch iOS-Apps brauchen keinen solchen Eintrag.

Listing 5

Permissions-Zeile

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.example.weather_app">
  <uses-permission android:name="android.permission.INTERNET"/>
  <application [...]

Cross-Plattform

Möchten Sie die erstellte App für den Desktop kompilieren, genügen dazu drei Befehle auf der Kommandozeile im Wurzelverzeichnis des Projekts (Listing 6). Als Resultat erhalten Sie eine ausführbare Binärdatei im Ordner »build/linux/x64/release/bundle/«. Für Windows oder MacOS als Zielplattform ersetzen Sie lediglich das Wort »linux« durch »windows« beziehungsweise »macos«. Allerdings klappt das Kompilieren für Windows und MacOS nur unter dem jeweils selben Betriebssystem. Für weitere Informationen bezüglich des Desktop-Supports finden Sie auf der Flutter-Webseite [8].

Listing 6

Kompilieren

$ flutter config --enable-linux-desktop
### Wichtig: im folgenden Befehl den
### Punkt am Ende nicht vergessen
$ flutter create --platforms=linux .
$ flutter build linux

Fazit

Dieser Artikel konnte nur einen groben Überblick über Flutter geben. Hat man aber erst einmal das Konzept verstanden, wie sich Apps in Flutter aufbauen, lassen sich inzwischen relativ einfach und komfortabel Apps für die verschiedensten Plattformen programmieren. Das Flutter-Repository [2] erweist sich zudem als wahre Fundgrube an Packages, die den Programmieralltag erheblich erleichtern. Die Wetter-App könnte man zum Beispiel mit dem Package »geolocator« [9] noch dahingehend erweitern, dass sie den aktuellen Aufenthaltsort ermittelt und das lokale Wetter anzeigt, ohne dass man explizit den Ort angeben muss. (jcb/jlu)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 7 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben