Multiplattform-Frameworks Artikel 2 von 5: Flutter
Flutter
Das UI Toolkit Flutter wird in diesem Artikel vorgestellt. Der Artikel beginnt mit einer Einführung in das Toolkit und die verwendete Sprache Dart. Als Nächstes wird der Set-up-Prozess der Entwicklungsumgebung beschrieben. Darauf folgt die Vorstellung eines Prototypen, welcher die Grundprinzipien von Flutter beinhaltet. Nach der Präsentation des Prototypen wird beschrieben, wie der Prototyp für die Plattformen Windows und Android gebuilded wurde. Zum Schluss werden Vor- und Nachteile von Flutter diskutiert.
Dieser Artikel ist Teil einer Reihe von Artikeln, in welchen verschiedene Frameworks für die Multiplattform-Entwicklung vorgestellt werden:
- Multiplattform-Frameworks Artikel 1 von 5: React Native
- Multiplattform-Frameworks Artikel 3 von 5: Ionic
- Multiplatform-Frameworks Artikel 4 von 5: .NET MAUI
- Multiplattform-Frameworks Artikel 5 von 5: Quasar
- Multiplattform-Frameworks: Fazit
Was ist Flutter
Flutter ist ein UI Toolkit von Google, welches es ermöglicht, mit einer Codebasis mehrere Plattformen zu bedienen (Flutter, 2021a). Dabei wird das Toolkit unter der BSD 3 Clause Lizenz angeboten. Für das Erstellen von App via Flutter wird die Programmiersprache Dart (Dart, 2021) verwendet. Nachdem eine App erstellt wurde, kann diese für verschiedene Zielplattformen gebuilded werden. Dabei wird der geschriebene Dart-Code in den Maschinencode der Zielplattform übersetzt, sodass die Performance der App der einer nativen App gleicht (Flutter, 2021b). Zum Erstellen eines UIs via Flutter werden mehrere Widgets in eine Komposition, welche das UI beschreiben, zusammengefasst (Flutter, 2021c). Dabei verfolgt Flutter das Motto, dass jeder Baustein des Interfaces ein Widget ist. Dies ist vergleichbar mit den komponentenbasierten Ansätzen von modernen Webframeworks wie React (React, 2021) oder Vue (Vue, 2021). Durch die Komposition von Widgets entsteht eine baumartige Hierarchie von Widgets. In Abbildung 1 wird diese Idee präsentiert.
return Flex( direction: Axis.vertical, children: [ UserInput(addUser: addUser), Expanded(child: UserList(users:_users)) ], );
Abbildung 1: Exemplarische Komposition von Widgets.
Die Widgets können entweder selbst erstellt und designed werden oder es können bereits vordefinierte Widgets aus dem Material Design (Material, 2021) verwendet werden. Möglich ist dies, da Flutter Material-Widgets out of the Box mitliefert. Anders als in der zuvor angesprochenen Entwicklung mit Webtechnologien wird für das Styling der Widgets kein CSS verwendet. Stattdessen werden die Styles bei der Initialisierung eines Widgets mitgegeben und gleichzeitig bieten Widgets von sich aus Styles an (zum Beispiel das Center-Widget zentriert den Inhalt). In Abbildung 2 wird das Styling für ein Container-Widget präsentiert. Dabei wird bei der Erstellung eines Containers als decoration eine BoxDecoration, welche Stylings beinhaltet, übergeben. Das komplette Beispiel ist im Abschnitt für die Index-Seite zu finden.
decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: const LinearGradient( colors: [Colors.blue, Colors.green], begin: Alignment.bottomCenter, end: Alignment.topLeft ), ),
Abbildung 2: Exemplarisches Styling eines Container-Widgets via des decoration-Attributs.
Dart
Bei Dart handelt es sich um eine typsichere Sprache, die auf das Erstellen von UIs ausgelegt ist (Dart, 2021). Dabei kann ähnlich wie bei C# oder TypeScript explizit ein Typ angegeben oder durch den Compiler abgeleitet werden. Dart bietet von sich aus mehrere Kernbibliotheken an. Gleichzeitig können Pakete über einen entsprechenden Manager heruntergeladen und installiert werden (Pub.Dev, 2021). Dies ähnelt dem Ansatz von NPM (NPM, 2021).
Anforderungen
Um mit Flutter auf einem Windows-System entwickeln zu können, wird die folgende Software benötigt:
- Git (Git, 2021).
- VS Code als Editor (Visual Studio Code, 2021).
- Flutter SDK (Flutter, 2021d).
- Die Flutter- und Dart-Plugins für VS Code.
Gleichzeitig empfiehlt es sich, die passenden Endgeräte für die Zielplattformen für das Debugging bereitzuhalten. Eine ausführliche Anleitung der Installation ist in den Docs von Flutter zu finden.
Initialisierung des Prototypen
Da nun die Entwicklungsumgebung einsatzbereit ist, kann der Prototyp erstellt werden. Dieser Prototyp soll verschiedene Aspekte von Flutter umfassen, um dadurch einen Einstieg in das Toolkit zu gewähren. Der Prototyp soll drei Seiten umfassen: Eine Index-, eine User- und eine Help-Seite umfassen. Die Index-Seite soll die grundlegende Idee hinter Widgets und dem Styling präsentieren. Die User-Seite soll das Rendern von Listen sowie die Interaktionen eines Nutzers mit einem Text-Input widerspiegeln. Abschließend soll die Help-Seite der Präsentation von Animationen dienen. In der folgenden Abbildung 3 werden die Ergebnisse der drei Ansichten vorgestellt.
Abbildung 3: Die finalen Ansichten des Prototypen: Links: Index-Seite; Mitte: User-Seite; Rechts: Hilfe-Seite.
Für die Erstellung eines Projekts, welches für den Prototypen gedacht ist, müssen die folgenden Schritte ausgeführt werden:
- Es wird in VS Code die Commando-Zeile mit strg + umschalt + p geöffnet und der Befehl zum Erstellen einer neuen Flutter App ausgewählt.
- Es folgt die Auswahl eines Zielordners sowie die Eingabe eines Namens für die App.
- Nun wird VS Code in dem Ordner der App geöffnet.
Nun kann die App im Debug-Modus gestartet werden, in dem die main.dart Datei geöffnet und F5 gedrückt wird. Dadurch wird der Debugger mit dem aktuell ausgewählten Gerät geöffnet und die Startseite wird angezeigt. Die Auswahl eines anderen Geräts kann in VS Code in der unteren Toolbar durchgeführt werden. Wenn die App sich im Debuggmodus befindet, werden zwei Features angeboten: Hot Reload und ein Widget Inspector. Beim Hot Reload wird eine Änderung augenblicklich im geöffneten Gerät wiedergegeben. Der Widget Inspector ähnelt dem Element Inspector eines Browsers und zeigt zum Beispiel den aktuellen State eines ausgewählten Widgets an. Zusätzlich können Animation für das Debuggen verlangsamt ausgeführt werden. In der folgenden Abbildung wird der Widget Inspector sowie die angesprochene Toolbar von VS Code präsentiert:
Abbildung 4: Darstellung des Widget Inspectors während des Debuggings.
Erstellung der Index-Seite
Die Index-Seite soll den Einstieg sowohl in die App als auch in Flutter bieten. In der folgenden Abbildung wird der Code für diese Seite präsentiert:
import 'package:flutter/material.dart'; class IndexPage extends StatelessWidget { const IndexPage({ Key? key }) : super(key: key); @override Widget build(BuildContext context) { return Container( // custom styling decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: const LinearGradient( colors: [Colors.blue, Colors.green], begin: Alignment.bottomCenter, end: Alignment.topLeft ), ), child: Text( 'Hello Index', style: TextStyle( color: Colors.yellow[100], fontSize: 32 ) ) ); } }
Abbildung 5: Code der Index-Seite in index_page.dart.
Als Erstes wird das Material-Packet importiert. Dadurch erhält die Datei Zugriff zu den vordefinierten Material-Widgets. Auf den Import folgt die Definition der Seite als Widget. Dafür wird eine Klasse IndexPage definiert, welche von StatelessWidget erbt. In Flutter gibt es zwei grundlegende Arten von Widgets: Stateless- und StatefulWidgets. StatelessWidgets enthalten keinen State, der den aktuellen Zustand des Widgets definiert. Im Gegensatz dazu besitzt ein StatefulWidget einen State. Dabei wird das Widget neu gerendert, wenn sich der State ändert. Dies wird deutlich in dem Abschnitt, welcher die Seite für die User vorstellt.
Die definierte IndexPage Klasse beinhaltet zwei Elemente: einen Constructor und eine build-Methode. Der Constructor wird hier in einer Zeile definiert. Alternativ kann der Constructor wie in jeder anderen Sprache mit einem Body definiert werden. Es folgt die build-Methode die den Aufbau des Widgets beschreibt. Dafür erhält diese als Parameter einen BuildContext, welcher den Context des Widgets in der baumartigen Struktur der Applikation repräsentiert. Dieser ist für jedes Widget einzigartig (Flutter API, 2021). In der build-Methode wird ein Container zurückgegeben, welcher eine Decoration und ein Child beim Initialisieren erhält. Die Decoration beschreibt das Styling des Containers. Exemplarisch erhält der Container abgerundete Ecken und als Hintergrund einen linearen Gradienten. Bei der Beschreibung der Rundung wird keine Einheit mitgegeben. Es handelt sich um logische Pixel, deren Interpretation vom Endgerät abhängt (Flutter, 2021e). Der Container erhält als sein Child ein Text-Widget, welches einen Text und Stylings für die Schriftfarbe und -größe beinhaltet.
Erstellung der User-Seite
Bei der User-Seite handelt es sich um ein StatefulWidget. Bei der Initialisierung der UserPage wird ebenfalls der State initialisiert. Die Initialisierung des States umfasst private Felder für einen Zufallsgenerator und einer Liste von Nutzern, eine zum initialen Setzen des States, eine Methode für das Hinzufügen eines Nutzers und die build-Methode. Beim Initialisieren des States werden drei User hinzugefügt. Die Methode addUser fügt einen User hinzu und ruft setState auf, um das Widget neu zu rendern. Ohne diesen Aufruf bleibt beim Hinzufügen eines Users der dargestellte Inhalt immer gleich. Die build-Methode gibt ein Flex-Widget zurück, deren Main-Axis die vertikale Achse ist. Als Children werden die Widgets UserInput und UserList übergeben. Die folgende Abbildung zeigt diesen Aufbau der Seite.
import 'dart:math'; import 'package:flutter/material.dart'; import '../models/user.model.dart'; import '../components/users/user_list.dart'; import '../components/users/user_input.dart'; class UsersPage extends StatefulWidget { const UsersPage({ Key? key }) : super(key: key); @override _UsersPageState createState() => _UsersPageState(); } class _UsersPageState extends State<UsersPage> { final _randomGenerator = Random(); final _users = <User>[]; // on init state @override void initState() { _users.add(User('Udo', 1)); _users.add(User('Peter', 2)); _users.add(User('Hans', 3)); super.initState(); } void addUser(String newName){ // modify state, to force rerendering of the list setState(() => _users.add(User(newName, _randomGenerator.nextInt(10000000)))); } @override Widget build(BuildContext context) { return Flex( direction: Axis.vertical, children: [ UserInput(addUser: addUser), Expanded(child: UserList(users:_users)) ], ); } }
Abbildung 6: Code der erstellten User-Seite aus users_page.dart.
Das UserList-Widget
Dieses Widget soll die erstellten User darstellen. Dafür wurde ein Feld für eine User-Liste erstellt. Der Constructor erwartet eine Liste von User und verwendet das this-Keyword um das zuvor definierte Feld mit der übergebenen Liste zu setzen. Die build-Methode erstellt eine ListView. Beim Erstellen der ListView wird die build-Methode des Widgets aufgerufen. Damit das Widget die Anzahl seiner Elemente kennt, wird diesem als itemCount die Anzahl der User übergeben. Für das Rendern der Items wird das Feld itemBuilder verwendet, welches eine Methode für das Rendern der User erhält. Dabei wird jeder User aus der Liste ausgelesen und übergeben. Die folgende Abbildung präsentiert den Code für dieses Widget:
import 'package:flutter/material.dart'; import '../../models/user.model.dart'; class UserList extends StatelessWidget { final List<User> users; const UserList({required this.users, Key? key }) : super(key: key); ListTile _renderUser(User user, int index){ return ListTile( title: Text(user.name), leading: const CircleAvatar(backgroundImage: NetworkImage('https://source.unsplash.com/random')), tileColor: index.isOdd ? Colors.lightBlue : Colors.white, hoverColor: Colors.lightGreen, minVerticalPadding: 9.0, ); } @override build(BuildContext context) => ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), itemCount: users.length, itemBuilder: (context, index) => _renderUser(users[index], index), ); }
Abbildung 7: Code des UserList-Widgets aus user_list.dart.
Das UserInput-Widget
Um User hinzufügen zu können, wird ein Form benötigt. Dafür wurde ein StatefulWidget erstellt, welches in der Abbildung 8 präsentiert wird. Besonders ist bei diesem Widget, dass es eine Funktion namens addUser im Constructor erwartet und diese Funktion gleich dem entsprechenden Feld im Widget zuweist.
import 'package:flutter/material.dart'; typedef AddUserType = void Function(String newName); class UserInput extends StatefulWidget { final AddUserType addUser; const UserInput({ // initialize the field required this.addUser, Key? key }) : super(key: key); @override _UserInputState createState() => _UserInputState(); }
Abbildung 8: Das erstelle Widget für das Hinzufügen von Nutzern in user_input.dart.
Der State wird in der selben Datei erstellt. Dabei wird im State ein Key für das Form sowie ein Controller für das TextFormField definiert. Der Key wird dem Form hinzugefügt und in der folgenden _submitForm-Methode wird das Form referenziert, um für die Validierung des Forms auf dessen State zuzugreifen. Wenn in dieser Methode das Form als valide identifiziert wurde, wird ein entsprechender Hinweistext für den Nutzer angezeigt, es wird die addUser-Methode des Widgets aufgerufen und das TextFormField wird geleert. Es folgen zwei Methoden: Eine, die ein TextFormField und eine, die den Submit-Button rendert. Dem TextFormField wird unter anderem ein Validator mitgegeben, der beim Absenden einer invaliden Eingabe einen Hinweistext anzeigt. Wenn der Submit-Button angeclickt wird, wird die zuvor beschriebene _submitForm-Methode aufgerufen und Form wird abgesendet. Die build-Methode gibt das Form zurück, wobei dieses mit dem zuvor definierten Key versehen wurde. Um sowohl das TextFormField als auch den Button zu darzustellen, werden diese beiden in der build-Methode innerhalb eines Wrap-Widgets Seite an Seite platziert. Dieses Widget wird dem Form als Child übergeben. Der eben beschriebene State wird in der Abbildung 9 präsentiert.
class _UserInputState extends State<UserInput> { final _formKey = GlobalKey<FormState>(); final TextEditingController textFormFieldController = TextEditingController(); void _submitForm(){ if(_formKey.currentState!.validate()){ ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Adding a User')), ); widget.addUser(textFormFieldController.text); textFormFieldController.text = ''; } } TextFormField _renderTextFormField() { return TextFormField( validator: (value) => (value == null || value.isEmpty) ? 'Enter a Name' : null, autocorrect: false, enableSuggestions: false, textAlignVertical: TextAlignVertical.center, controller: textFormFieldController, maxLength: 20, ); } ElevatedButton _renderButton(){ return ElevatedButton( onPressed: _submitForm, child: Row( children: const [Text('Submit'), Icon(Icons.plus_one, size: 24.0)], ) ); } @override void dispose(){ textFormFieldController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Wrap( spacing: 8.0, alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, children: [ SizedBox( height: 80, width: 195, child: _renderTextFormField(), ), SizedBox( height: 40, width: 105, child: _renderButton(), ) ], ) ); } }
Abbildung 9: Der State des UserInput-Widgets in user_input.dart.
Erstellung der Hilfe-Seite
Diese Seite soll beim Aufruf einen animierten Text darstellen. Das erstellte Widget enthält einen State, um dadurch die Animationen zu ermöglichen. Die folgende Abbildung stellt das Grundgerüst dieser Seite dar. Die vereinfachten Teile werden in folgenden Abbildungen präsentiert und besprochen.
import 'package:flutter/material.dart'; class HelpPage extends StatefulWidget { const HelpPage({ Key? key }) : super(key: key); @override _HelpPageState createState() => _HelpPageState(); } class _HelpPageState extends State<HelpPage> with SingleTickerProviderStateMixin { AnimationController? _controller; Animation? _sizeAnimation; Animation? _colorAnimation; bool _isVisible = false; @override void initState(){…} // release ressource on dispose/destroy @override void dispose() { super.dispose(); _controller!.dispose(); } @override Widget build(BuildContext context) {…} }
Abbildung 10: Grundlegender Aufbau der Hilfe-Seite in help_page-dart.
Die Animationen werden in der initState-Methode definiert (siehe Abbildung 11):
@override void initState(){ super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(seconds: 2)); // sequential animation _sizeAnimation = Tween<double>(begin: 0.0, end: 500.0).animate(CurvedAnimation(parent: _controller!, curve: const Interval(0, 0.5))); _colorAnimation = ColorTween(begin: Colors.lightGreen[400], end: Colors.pink[400]).animate(CurvedAnimation(parent: _controller!, curve: const Interval(0.5, 1))); _controller!.addListener(() { setState(() {}); }); // single execution _controller!.forward(); Future.delayed(const Duration(milliseconds: 500), () => setState(() => _isVisible = !_isVisible)); }
Abbildung 11: Die initState-methode der Hilfe-Seite.
In der initState-Methode wird zum Beginn ein Controller initialisiert, der die zugewiesenen Animationen innerhalb von zwei Sekunden durchführt. Darauf folgt die Initialisierung der Animationen für die Größe und die Farbe. Dabei werden Tweens verwendet, welche einen Start- und Endwert erhalten. Tweens interpolieren linear vom Start- bis zum Endwert. Die Animation wird über die animate-Methode definiert. Diese erhält eine CurvedAnimation, welche wiederum selbst den zuvor erstellten Controller und eine curve übergeben bekommt. Durch das Intervall, das einer CurvedAnimation übergeben wird, kann ein sequenzieller Ablauf von Animationen geschaffen werden. In diesem Beispiel wird die ersten 50% der Animation die Größe animiert. Danach folgt die Animation der Farbe bis zum Abschluss der Animation. Nachdem die Animationen definiert wurden, wird mit dem Controller gearbeitet. Als Erstes wird ein Listener auf dem Controller gelegt, welcher bei jeglicher Änderung den State neu setzt, um das Widget neu zu rendern. Dies könnte zwar ineffizient sein, da der gesamte Widget-Tree jedes Mal neu gerendert wird, jedoch ist das in diesem Beispiel kein kritischer Faktor. Nun wird die Animation ausgelöst, indem mit der forward-Methode die Animation ausgeführt wird. Zum Schluss wird im initState ein Delay erstellt, welcher nach 500 Millisekunden den Wert von _isVisible toggelt. Dies wird dazu verwendet, einen alternativen Ansatz der Animation vorzustellen.
Da nun der State initialisiert wurde, wird nun die build-Methode, in der die Animationen verwendet werden, besprochen. In der folgenden Abbildung wird diese präsentiert:
@override Widget build(BuildContext context) { return AnimatedOpacity( opacity: _isVisible ? 1.0 : 0.0, duration: const Duration(milliseconds: 2500), child: Container( width: _sizeAnimation!.value, height: _sizeAnimation!.value, color: _colorAnimation!.value, child: const Text('''Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.'''), ), ); }
Abbildung 12: Die build-Methode der Hilfe-Seite.
Die build-Methode gibt ein AnimatedOpacity-Widget zurück. Dieses Widget animiert eigenständig die Änderungen der Opacity für sein Child über die Dauer von 2.5 Sekunden. Als Child erhält das Widget einen Container. Die Dimensionen und die Farbe des Containers werden mit den Werten der Animationen gesetzt. Da sich die Werte über die Zeit interpoliert werden, wird bei jeder Änderung das Widget neu gebaut und es entsteht visuell eine Animation. Zusätzlich enthält der Container als Child ein Text-Widget, welches den Hilfetext darstellen soll. Das Ergebnis sieht wie folgt aus:
Abbildung 13: Die resultierende Animation der Hilfe-Seite.
Erstellung des Routings
Das Routing wird zusammen mit dem Layout in der main.dart, innerhalb der Main-Klasse, definiert. Dabei wird in das Layout die Navigation sowie die zuvor beschriebenen Seiten eingebettet. Für die Navigation bietet Flutter zwei Versionen mit unterschiedlichen Ansätzen an: Version 1 verwendet einen imperativen und Version 2 einen deklarativen Ansatz (Medium, 2020). In diesem Prototypen wurde die erste Version verwendet, da der Prototyp kein komplexes Navigationsverhalten beinhaltet. Die build-Methode der Main-Klasse gibt eine Material-App mit drei definierten Routen zurück. Um ein einheitliches Layout zu schaffen, wird jede Seite der Methode _renderPage übergeben. Diese Methode bettet jede Seite in ein einheitliches Layout ein. Das Layout beinhaltet eine Appbar, die den Titel der App sowie Buttons für die Navigation beinhaltet. Gleichzeitig wird ein Container als Body übergeben, in welchen die aktive Seite zentriert gerendert wird. Die folgende Abbildung präsentiert den beschriebenen, grundlegenden Inhalt des Layouts.
import 'package:flutter/material.dart'; import 'pages/index_page.dart'; import 'pages/users_page.dart'; import 'pages/help_page.dart'; class Main extends StatelessWidget { const Main({ Key? key }) : super(key: key); List<ElevatedButton> _renderNavigationButtons(BuildContext context){…} @override Widget build(BuildContext context) { Scaffold _renderPage(Widget page, BuildContext context){ return Scaffold( appBar: AppBar( title: const Text('Flutter'), actions: _renderNavigationButtons(context), backgroundColor: Colors.amber, ), body: Container( padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), child: Center(child: page), ), ); } return MaterialApp( routes: { '/': (context) => _renderPage(const IndexPage(), context), '/users': (context) => _renderPage(const Expanded(child: UsersPage()), context), '/help': (context) => _renderPage(const HelpPage(),context), }, ); } } void main() => runApp(const Main());
Abbildung 14: Grundgerüst der Main-Widgets in der main.dart.
Es wurden das Routing und das Layout der App beschrieben. Damit ein Nutzer zwischen den einzelnen Routen/Ansichten wechseln kann, benötigt dieser Interaktionselemente, die diese Funktionalität anbieten. Dafür wird die Methode _renderNavigationButtons verwendet. Eine Präsentation dieser Methode ist in der Abbildung 15 zu finden. Wie der Name besagt, gibt diese Methode die Buttons für die Navigation zurück. Jedoch werden diese abhängig von der aktuellen Route gerendert: Befindet sich ein Nutzer auf der Startseite, werden Buttons für die Hilfe- und User-Seite zurückgegeben. Sonst wird kein Button zurückgegeben. Um zurück auf die vorherige Seite navigieren zu können, bietet die verwendete AppBar von sich aus einen entsprechenden Button an.
List<ElevatedButton> _renderNavigationButtons(BuildContext context){ var currentRoute = ModalRoute.of(context)!.settings.name; var isRootPage = currentRoute == null || currentRoute == '/'; if(isRootPage){ return [ ElevatedBut-ton(onPressed: () => Navigator.pushNamed(context, '/users'), child: const Text('Users')), ElevatedBut-ton(onPressed: () => Navigator.pushNamed(context, '/help'), child: const Text('Help')), ]; } return []; }
Abbildung 15: Die Methode zum Rendern der Navigationsbuttons.
Builden für Android
Der erstellte Prototyp soll nun Android gebuilded werden. Dafür wurde sich an der Anleitung aus den Flutter-Docs orientiert, um eine apk-Datei zu erstellen, welche auf einem Gerät installiert wurde. Es wurden die folgenden Schritte ausgeführt:
- Erstellung eines Keystores: Dafür wurde eine Powershell-Instanz im bin-Ordner der Java-Installation geöffnet und der folgende Befehl ausgeführt:
$ keytool -genkey -v -keystore c:\projects\flutter\hello_flutter\android\keystore\flutter-internal.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias flutter-internal-alias
Abbildung 16: Erstellung eines Keystores für Android.
- Erstellung und Befüllung einer properties Datei im Android-Ordner:
storeFile=../keystore/flutter-internal.jks keyAlias=flutter-internal-alias storePassword=password123 keyPassword= password123
Abbildung 17: Inhalt der Datei key.properties.
- Anpassung der Datei build.gradle in /android/app/build.gradle:
- Hinzufügen des folgenden Codes über dem Abschnitt android:
def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) }
Abbildung 18: Attribute des Keystores, die über dem Android-Abschnitt hinzugefügt werden
- Im Abschnitt android den folgenden Code ergänzen:
signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release } }
Abbildung 19: Inhalte, die innerhalb des Android-Abschnittes eingefügt werden müssen.
- Hinzufügen des folgenden Codes über dem Abschnitt android:
- Builden einer APK mit dem folgenden Befehl:
$ flutter build apk -release
Abbildung 20: Befehl zum Erstellen einer APK
Builden für Windows
Der Prototyp wurde ebenfalls für Windows gebuilded, sodass eine Exe-Datei zurückgegeben wird. Dabei ist zu beachten, dass das Builden für Windows sich noch in der Beta befindet. Beim Builden ist zu beachten, dass der Namen des Ordners, in dem sich der Prototyp befindet, gleich dem Namen des Projekts in der pubspec.yaml ist. Um den Prototyp für Windows zu builden, wurde im Ordner des Prototypen eine Powershell-Instanz geöffnet und die folgenden Befehle ausgeführt:
$ flutter config –enable-windows-desktop # The dot at the end is used, to specify the current dir as output # by specifing the current dir, windows gets added to the existing project $ flutter create –platforms=windows . $ flutter build windows
Abbildung 21: Befehle zum Builden für Windows.
Vor- und Nachteile
Nach der Vorstellung von Flutter kann der Eindruck entstehen, dass dieses Toolkit das Allheilmittel für die Problematik der Multiplattform-Entwicklung ist. Jedoch hat Flutter nicht nur Vorteile, sondern auch Nachteile. Daher werden sowohl identifizierte Vor- als auch Nachteile vorgestellt.
Vorteile
- Flutter kann seiner Idee der Bedienung von mehreren Plattformen gerecht werden. Es werden die gängigsten Plattformen, auch wenn sich deren Support teilweise noch in der Alpha/Beta befindet, unterstützt und das Builden für die Projekte geht problemlos.
- Dadurch, dass Flutter Apps in nativen Code kompiliert werden, wird keine Art Adapter zwischen der App und der Plattform benötigt. Durch diese fehlende Zwischenschicht haben Flutter Apps die Performance einer nativen App (Flutter, 2021b).
- Es wird ein Hot Reload angeboten, der beim Speichern von Änderungen diese umgehend anzeigt.
Nachteile
- Flutter verwendet Dart als Programmiersprache. Diese Sprache ist ähnlich zu bereits bestehenden Sprachen und kann daher relativ leicht erlernt werden. Jedoch wird trotzdem Training zum Erlernen der Sprache benötigt.
- Es kann vorkommen, dass plattformspezifischer Code benötigt wird. Es kann zwar nach Packages mit der gewünschten Funktionalität gesucht werden, jedoch ist nicht garantiert, dass eines existiert. Daher kann es vorkommen, dass ein Entwickler, der die entsprechende Sprache spricht, benötigt wird.
Fazit
Flutter ist ein Tool von Google und ermöglicht das Bedienen mehrerer Plattformen mit einer Codebasis. Flutter verwendet den Ansatz, dass alles ein Widget ist und sich ein User Interface aus einer Hierarchie von Widgets zusammensetzt. Widgets können dabei selbst erstellt oder es kann sich an den bereits mitgelieferten Material-Widgets bedient werden. In der Dokumentation von Flutter werden die Widgets ausführlich mit Beschreibungen und Videos vorgestellt. Gleichzeitig sind interaktive Beispiele vorhanden. Diese Art der Dokumentation führt dazu, dass sich schnell eingearbeitet werden kann. Jedoch besteht beim Einarbeiten die Hürde, dass die Programmiersprache Dart erlernt werden muss. Da mehrere Plattformen bedient werden können, sollte beim Entwickeln auf den Zielplattformen getestet werden. Jedoch war dies in diesem Fall aufgrund der Entwicklung in einer VM nicht möglich. Somit sind plattformspezifische Bugs erst beim späteren Testen der Builds aufgefallen. Es wurden die Prozesse zum Builden für Windows und Android beschrieben. Dabei war das Builden sehr einfach und es sind keine Probleme aufgetreten. Weitere mögliche Zielplattformen können Web, Linux, macOS und iOS sein. Dabei müssen Builds für die letzten drei genannten Plattformen auf einem entsprechenden System durchgeführt werden (Linux für Linux, macOS für macOS und iOS). Bei der Erstellung des Prototypen wurde zusätzlich die Möglichkeit zur Integration von MSAL und Fluent UI untersucht. Zwar gibt es keine offiziellen Pakete, jedoch existieren inoffizielle Pakete, die die entsprechenden Funktionalitäten anbieten und problemlos zum Laufen gebracht wurden.
Referenzen
Dart (2021). https://dart.dev/overview. (Stand: 29.09.2021).
Flutter (2021a). https://flutter.dev/. (Stand: 29.09.2021).
Flutter (2021b). https://flutter.dev/docs/resources/architectural-overview. (Stand: 29.09.2021).
Flutter (2021c). https://flutter.dev/docs/development/ui/widgets-intro. (Stand: 29.09.2021).
Flutter (2021d). https://flutter.dev/docs/development/tools/sdk/releases. (Stand: 29.09.2021).
Flutter (2021e). https://flutter.dev/docs/get-started/flutter-for/android-devs#where-do-i-store-my-resolution-dependent-image-files. (Stand 30.09.2021).
Flutter API (2021). https://api.flutter.dev/flutter/widgets/BuildContext-class.html. (Stand: 30.09.2021).
Git (2021). https://git-scm.com/. (Stand: 30.09.2021).
Material (2021). https://material.io/design. (Stand: 29.09.201).
Medium (2020). https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade. (Stand: 30.09.2020).
NPM (2021). https://www.npmjs.com/. (Stand: 29.09.2021).
Pub.Dev (2021). https://pub.dev/. (Stand: 29.09.2021).
React (2021). https://reactjs.org/. (Stand: 29.09.2021).
Visual Studio Code (2021). https://code.visualstudio.com/. (Stand: 30.09.2021).
Vue (2021). https://vuejs.org/. (Stand: 29.09.2021).