Hochschule Darmstadt - Fachbereich Informatik- opus4.kobv.de
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Hochschule Darmstadt – Fachbereich Informatik – Evaluation von Zustandsverwaltungssystemen für das mobile Cross-Plattform-Framework Flutter Abschlussarbeit zur Erlangung des akademischen Grades Bachelor of Science (B.Sc.) vorgelegt von Jonas Franz Matrikelnummer: 760233 Referent : Prof. Dr. Kai Renz Korreferent : Prof. Dr. Hans-Peter Wiedling
Jonas Franz: Evaluation von Zustandsverwaltungssystemen für das mobile Cross- Plattform-Framework Flutter, © 03. März 2022
ERKLÄRUNG Ich versichere hiermit, dass ich die vorliegende Arbeit selbstständig verfasst und keine anderen als die im Literaturverzeichnis angegebenen Quellen be- nutzt habe. Alle Stellen, die wörtlich oder sinngemäß aus veröffentlichten oder noch nicht veröffentlichten Quellen entnommen sind, sind als solche kenntlich gemacht. Die Zeichnungen oder Abbildungen in dieser Arbeit sind von mir selbst erstellt worden oder mit einem entsprechenden Quellennachweis versehen. Diese Arbeit ist in gleicher oder ähnlicher Form noch bei keiner anderen Prüfungsbehörde eingereicht worden. Darmstadt, 03. März 2022 Jonas Franz
ABSTRACT Flutter has multiple approaches in managing the state of an application. Flutter is a popular cross-platform framework for the development of ap- plications for iOS, Android, Web, Windows, Linux and macOS. It offers the advantage to develop one codebase for multiple platforms at once resulting in reduced costs and complexity. Flutter uses a declarative UI that renders widgets based on a given state. The state management of a Flutter appli- cation is one of the most important parts of an application influencing the architecture and structure of an application. This thesis describes multiple already existing approaches for state manage- ment in Flutter. The objective is to determine which approach is the most suitable approach for managing state in Flutter. In order to meet this objec- tive, an evaluation will be carried out by evaluating the state management approaches setState, InheritedWidget, BLoC, Provider, Riverpod, Redux and MobX. An example application will be implemented for every approach. Those applications will be assessed by qualitative and quantitative criteria based on the requirements of state management systems for Flutter. The result, in addition of the evaluation assessments, is also a recommenda- tion which approach to use for a specific use case.
Z U S A M M E N FA S S U N G In Flutter gibt es diverse Ansätze und Lösungsmöglichkeiten, den Zustand einer mobilen Anwendung zu verwalten. Flutter ist ein beliebtes Cross-Platt- form-Framework, mit dem sich Anwendungen für iOS, Android, Web, Win- dows, Linux und macOS erstellen lassen, die den gleichen Quelltext verwen- den. Damit sollen in der Entwicklung Aufwände gespart werden können und die Komplexität reduziert werden. Flutter baut auf eine deklarative Be- nutzeroberfläche, welche anhand eines Zustands erstellt wird. Die Verwal- tung dieses Zustands ist entscheidend für die Architektur und die Funktion einer Anwendung. In dieser Ausarbeitung werden verschieden bereits etablierte Ansätze zur Verwaltung des Zustands einer Flutter-Anwendung dargestellt und unter- sucht. Dabei ist das Ziel, herauszufinden, welcher Ansatz am besten zum Verwalten des Zustands einer Flutter-Anwendung geeignet ist. Dafür wird eine Evaluation für die Zustandsverwaltungssysteme setState, InheritedWid- get, BLoC, Provider, Riverpod, Redux und MobX durchgeführt. Grundlage dieser Evaluation ist die Entwicklung einer Beispielanwendung für jedes Zu- standsverwaltungssystem und die Bewertung dieser anhand von qualitative und quantitativen Bewertungskriterien, die anhand der Anforderungen an Zustandsverwaltungssysteme definiert werden. Das Ergebnis der Arbeit stellt neben den Resultaten der Evaluation auch eine Empfehlung dar, welches Zustandsverwaltungssystem für welchen An- wendungsfall am besten genutzt werden sollte.
I N H A LT S V E R Z E I C H N I S i thesis 1 einleitung 2 1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2 Zielsetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.3 Gliederung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.4 Methodik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2 grundlagen 6 2.1 Flutter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.1.1 Einordnung der Rolle für die Entwicklung von Apps . . 6 2.1.2 Technischer Überblick . . . . . . . . . . . . . . . . . . . . 7 2.1.3 Widgets . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.2 Automatisiertes Testen . . . . . . . . . . . . . . . . . . . . . . . . 10 2.3 Zustandsverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.3.1 Auswahl . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.3.2 Mitgelieferte Werkzeuge . . . . . . . . . . . . . . . . . . . 13 2.3.3 Business Logic Components (BLoC) . . . . . . . . . . . . 15 2.3.4 Provider . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.3.5 Riverpod . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.3.6 Redux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 2.3.7 MobX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3 analyse 22 3.1 Anforderungsanalyse . . . . . . . . . . . . . . . . . . . . . . . . . 22 3.1.1 Allgemeine Anforderungen . . . . . . . . . . . . . . . . . 22 3.1.2 Spezifische Anforderungen . . . . . . . . . . . . . . . . . 24 3.1.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . 24 3.2 Bewertungskriterien . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.2.1 Änderbarkeit/Skalierbarkeit . . . . . . . . . . . . . . . . 25 3.2.2 Testbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.2.3 Effizienz . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.2.4 Komplexität / Wartbarkeit . . . . . . . . . . . . . . . . . 26 3.2.5 Verständlichkeit / Lesbarkeit . . . . . . . . . . . . . . . . 27 3.2.6 Dokumentierung . . . . . . . . . . . . . . . . . . . . . . . 28 3.2.7 Strukturbestimmung . . . . . . . . . . . . . . . . . . . . . 28 3.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . 29 4 umsetzung und realisierung 30 4.1 Anforderungen an die Beispielanwendung . . . . . . . . . . . . 30 4.1.1 Implizierte Anforderungen . . . . . . . . . . . . . . . . . 30 4.1.2 Explizite Anforderungen . . . . . . . . . . . . . . . . . . 31 4.2 Konzeption einer Beispielanwendung . . . . . . . . . . . . . . . 32
inhaltsverzeichnis vii 4.2.1 Anwendung der impliziten Anforderungen . . . . . . . 32 4.2.2 Wireframes . . . . . . . . . . . . . . . . . . . . . . . . . . 33 4.3 Vorgehen bei der Implementierung . . . . . . . . . . . . . . . . . 33 4.4 Ergebnis der Beispielanwendung . . . . . . . . . . . . . . . . . . 35 4.4.1 Versuchsaufbau . . . . . . . . . . . . . . . . . . . . . . . . 35 5 evaluation 37 5.1 setState . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 5.2 InheritedWidget . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 5.2.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 38 5.2.2 Bewertung . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 5.3 BLoC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 5.3.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 41 5.3.2 Bewertung . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 5.4 Provider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 5.4.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 45 5.4.2 Bewertung . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 5.5 Riverpod . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 5.5.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 49 5.5.2 Bewertung . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 5.6 Redux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 5.6.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 53 5.6.2 Bewertung . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 5.7 MobX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 5.7.1 Implementierung . . . . . . . . . . . . . . . . . . . . . . . 57 5.7.2 Bewertung . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 5.8 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 6 fazit 61 6.1 Empfehlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 6.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 ii appendix a testergebnisse 64 a.1 Metriken: main . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 a.2 Metriken: inheritedwidget . . . . . . . . . . . . . . . . . . . . . . 65 a.3 Metriken: bloc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 a.4 Metriken: provider . . . . . . . . . . . . . . . . . . . . . . . . . . 67 a.5 Metriken: riverpod . . . . . . . . . . . . . . . . . . . . . . . . . . 68 a.6 Metriken: redux . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 a.7 Metriken: mobx . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 literatur 71
ABBILDUNGSVERZEICHNIS Abbildung 2.1 Architekturdiagramm von Flutter [Goo21a] . . . . . . . 7 Abbildung 2.2 Flutter Widget Tree . . . . . . . . . . . . . . . . . . . . . 10 Abbildung 2.3 Flutter Widget Tree bei setState . . . . . . . . . . . . . . 13 Abbildung 2.4 Datenfluss in Redux [GF18, Kap.1.3.3] . . . . . . . . . . 20 Abbildung 4.1 Wireframes der Beispielanwendung . . . . . . . . . . . 33 Abbildung 4.2 Umsetzung der Wireframes in Flutter unter iOS . . . . 35
TA B E L L E N V E R Z E I C H N I S Tabelle 3.1 Bewertungsmatrix . . . . . . . . . . . . . . . . . . . . . . 29 Tabelle 5.1 Ergebnisse der Evaluation . . . . . . . . . . . . . . . . . 60
LISTINGS Listing 2.1 Aufbau eines einfachen Flutter-Widgets in Dart . . . . 7 Listing 2.2 Vereinfachte Darstellung eines Widgets als Methode [Win20] . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Listing 2.3 StatefulWidget . . . . . . . . . . . . . . . . . . . . . . . . 9 Listing 2.4 Aufbau eines InheritedWidget . . . . . . . . . . . . . . . 14 Listing 2.5 Aufbau einer ChangeNotifier-Klasse für Provider . . . 16 Listing 2.6 Injektion und Abruf von Provider-Klassen . . . . . . . . 17 Listing 2.7 Zustandsklasse in Riverpod . . . . . . . . . . . . . . . . 18 Listing 2.8 ConsumerWidget . . . . . . . . . . . . . . . . . . . . . . 19 Listing 4.1 Anzahl der Rendervorgänge . . . . . . . . . . . . . . . . 36 Listing 5.1 Aufbau eines einfachen Flutter-Widgets in Dart . . . . 39 Listing 5.2 Anzahl der Rendervorgänge bei InheritedWidget . . . 40 Listing 5.3 Nesting bei InheritedWidgets in app.dart [Fra22] . . . 40 Listing 5.4 Verwendung eines StreamBuilder in cart_button.dart [Fra22] . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 Listing 5.5 Test des Cart BLoC in cart_bloc_test.dart [Fra22] . . . . 43 Listing 5.6 Anzahl der Rendervorgänge bei BLoC . . . . . . . . . . 44 Listing 5.7 Widget-Test bei Provider in total_price_test.dart [Fra22] 47 Listing 5.8 Anzahl der Rendervorgänge bei Provider . . . . . . . . 48 Listing 5.9 Vergleich des Abrufs von Zuständen zwischen Flutter und Provider . . . . . . . . . . . . . . . . . . . . . . . . . 48 Listing 5.10 Widget-Test bei Riverpod in total_price_test.dart [Fra22] 51 Listing 5.11 Anzahl der Rendervorgänge bei Riverpod . . . . . . . . 52 Listing 5.12 Anzahl der Rendervorgänge bei Redux . . . . . . . . . 55 Listing 5.13 Anzahl der Rendervorgänge bei MobX . . . . . . . . . . 58
ABKÜRZUNGSVERZEICHNIS BLoC Business Logic Components MI Maintainability Index CI Continous-Integration-System
Teil I THESIS
EINLEITUNG 1 In der mobilen Software-Entwicklung dominieren seit Jahren die beiden mo- bilen Betriebssysteme iOS und Android. |[Osd] Dies macht es für die Ent- wicklung von mobilen Anwendungen erforderlich, ein Programm für beide Plattformen zu entwickeln. Da durch die Entwicklung von zwei ähnlichen Anwendungen aus rein technischen Gründen größere Aufwände bei der Ent- wicklung entsteht, sind seit mehreren Jahren sogenannte Cross-Plattform Technologien in Verwendung. Diese ermöglichen es, im Optimalfall mit der Entwicklung einer Anwendung mehrere Plattformen auf einmal abdecken zu können. In dieser Ausarbeitung wird dabei die Cross-Plattform Technologie Flutter betrachtet. Die erste Version von Flutter wurde am 12. Mai 2017 [Bra17] ver- öffentlicht und unterstützt mit der Version 1.20 die Möglichkeit eine Anwen- dung sowohl als App auf Android und iOS, als Desktop-Anwendung für macOS, Linux und Windows sowie als Website auszuspielen. [Goo22a] Wei- tere Details zur Flutter-Technologie werden im entsprechenden Grundlagen- Kapitel behandelt. Dabei wird mit dieser Ausarbeitung eine konkrete Problemstellung, die sich aus der Architektur von Flutter und ähnlichen Werkzeugen wie React behan- delt. Die Problemstellung umgreift die Frage, welche Software-Architektur und Werkzeuge gewählt werden müssen, um den Zustand der Anwendung oder einzelnen Bestandteilen der Anwendung effizient und architekturell sinnvoll zu verwalten. Anhand von entsprechend der Problemstellung definierten Bewertungskri- terien wird durch eine Evaluation verschiedener bestehender Lösungsan- sätze untersucht, welcher sich am besten zur Lösung der Problemstellung eignet. Im Folgenden wird in diesem Einleitungskapitel näher erläutert, welche Mo- tivation zur Wahl dieses Thema führt, welche konkrete Zielsetzung verfolgt wird, welche Gliederung gewählt wird und anhand welcher Methodik vor- gegangen wird.
1.1 motivation 3 1.1 motivation Bei der Entwicklung von mobilen Anwendungen wie beispielsweise im Agen- turgeschäft stellen sich immer wieder Architektur-Fragen, welche den Erfolg eines Projektes maßgeblich mitentscheiden können. Bei Flutter stellt sich hier beispielsweise die Frage, wie man am besten den Zustand der Anwendung verwalten kann. Auf diese Frage eröffnet sich den Entwickler*innen verschie- dene Lösungsmöglichkeiten. Dabei wird aktuell weder seitens der offiziellen Entwicklungsdokumentation von Flutter noch in den Online-Publikationen eine eindeutige Empfehlung getroffen. Erschwerend bei der Entscheidungs- findung kommt hinzu, dass es im Moment eine große Anzahl an verschie- denen Ansätzen zur Lösung des Problems gibt. Alleine die offizielle Flut- ter Dokumentation umfasst nach aktuellen Stand eine Aufzählung von 13 verschiedenen Ansätzen, das Problem der Zustandsverwaltung (engl. state management) zu lösen. [Goo21d] Die Entscheidung für einen bestimmten Architekturentwurf für die Zustands- verwaltung ist dabei eine schwierig umkehrbare Entscheidung, da sie Aus- wirkungen auf diverse Komponenten einer Anwendung hat. Dies kann un- ter anderem dazu führen, dass bei einer späteren Änderung der Zustands- verwaltung größere Änderungen an dem Quelltext der Anwendung nötig sind. Dies zeigt auf, dass die Entscheidung für die dem Anwendungsfall am bes- ten entsprechende Zustandsverwaltung eine signifikant wichtige Entschei- dung im Entwicklungsablauf einer Flutter-Anwendung ist. Daher soll diese Ausarbeitung dabei unterstützen, auf Grundlage einer wissenschaftlichen Evaluation verschiedener Lösungsansätze die für die Entwicklung beste Op- tion wählen zu können. 1.2 zielsetzung Das übergeordnete Ziel dieser Ausarbeitung ist es, den für die Entwicklung von Flutter-Anwendungen oder bestimmten Anwendungsfällen am besten geeignete Ansatz für die Zustandsverwaltung zu finden. Im Detail soll dabei gezeigt werden, welche Lösungsansätze für welchen Anwendungsfall am besten, begründet geeignet sind. Zur Erreichung dieses übergeordneten Zieles ist es erforderlich, einige Zwi- schenziele zu definieren. Das Erste Ziele ist dabei, zu analysieren, welche Anforderungen aktuell an die Zustandsverwaltung in Flutter gestellt wer- den, um im nächsten Schritt geeignete Kriterien für die Bewertung von Lö- sungsansätzen zu finden.
1.3 gliederung 4 Dabei ist es auch ein Ziel, diese Bewertungsansätze möglichst objektiv de- finieren zu können, da die Auswahl von geeigneten Kriterien entscheidend ist für die Aussagekraft der Evaluation. 1.3 gliederung Für die Strukturierung der Arbeit, wurde die Arbeit in verschiedene Kapitel und Unterkapitel gegliedert. Zur Erreichung der Ziele und Unterziele bietet sich folgende Struktur an. Zu Beginn wird mit der Einleitung ein grober Überblick über die Ausarbei- tung sowie Informationen zur Methodik vermittelt. Im Anschluss wird im Grundlagenkapitel anhand von Literaturrecherchen und Quelltext-Beispielen die für die Arbeit grundlegenden Konzepte darge- stellt. Dabei insbesondere die Cross-Plattform-Technologie Flutter näher dar- gestellt und Architekturentwürfe für diese vertieft. Außerdem wird erläutert, welche Probleme die Entwicklung mit einem deklarativen Komponentensys- tem wie bei Flutter mit sich bringen und mögliche Lösungsansätze aus der Literatur diskutiert. Darauf aufbauend werden im Analyse-Kapitel die Bewertungskriterien und Richtlinien für die spätere Evaluation erarbeitet. Für die Durchführung der Evaluation wird im Entwurf- und Realisierungs- Kapitel ein möglicher Versuchsaufbau konstruiert, welcher geeignet ist, die beschriebenen Lösungsansätze anhand der im Analyse-Kapitel erarbeiteten Kriterien zu evaluieren. Dabei werden auch Aspekte der Realisierung proto- kolliert. Im Evaluationskapitel werden die Ergebnisse aus dem Entwurfs- und Real- isierung-Kapitel anhand der im Analyse-Kapitel gewonnen Kriterien bewer- tet und die Ergebnisse analysiert. Zum Schluss findet eine Gesamtbewertung der Lösungsansätze statt. Abschließend wird die Bewertung in einem Gesamtzusammenhang gesetzt und eine Empfehlung abgegeben, sowie mögliche weitere Forschungsfragen diskutiert.
1.4 methodik 5 1.4 methodik Für die Beschreibung der möglichen Lösungsansätze für die Zustandsver- waltung, wird per Literaturrecherche auf verschiedene Ansätze eingegangen und diese näher erläutert. Im Anschluss wird mittels Literaturrecherche mögliche Anforderungen an Zustandsverwaltungssysteme zusammengefasst. Darauf aufbauend, werden Bewertungskriterien konstruiert, die auf die einzelnen Anforderungen ein- zahlen sollen. Für die Umsetzung und Realisierung werden aus diesen Bewertungskriteri- en und Anforderungen neue implizite und explizite Anforderungen an ei- ne Beispielanwendung konstruiert. Die Beispielanwendung soll später als Untersuchungsobjekt für die Evaluation dienen. Zusätzlich wird ein Ver- suchsaufbau konstruiert und beschrieben. Für die Evaluation wird für jedes untersuchte Zustandsverwaltungssystem eine eigene Anwendung implementiert und anhand der definierten Kriteri- en untersucht. Dabei kommen hauptsächlich qualitative Bewertungsmetho- den zum Einsatz. Zusätzlich dazu werden als quantitative Bewertungsme- thoden der Maintainability Index eingesetzt und eine automatisierte Teststre- cke zur Durchführung von Leistungstests eingerichtet. Alle Messungen und Bewertungen erfolgen in einer nachvollziehbaren, reproduzierbaren Docker- Umgebung.
GRUNDLAGEN 2 Dieses Kapitel befasst sich mit den Grundlagen für die nachfolgende Eva- luation. Dabei wird sowohl Literatur zum Thema ausgewertet, als auch Kon- zepte an Hand von Beispielen erläutert. 2.1 flutter Um die konkrete Problemstellung im Detail verstehen zu können, ist es erfor- derlich, die grundlegende Plattform näher zu betrachten. Dabei wird zu Be- ginn erst ein Überblick über das Cross-Plattform-Werkzeug Flutter gegeben und im Anschluss detaillierter auf einzelne Aspekte, die für die Erfassung der Problemstellung von Relevanz sind, eingegangen. 2.1.1 Einordnung der Rolle für die Entwicklung von Apps In diesem Abschnitt wird ein grober Überblick über die aktuelle Situati- on bei der Entwicklung von Apps gegeben. Flutter ist ein von dem US- amerikanischen Digitalunternehmen Google entwickeltes Cross-Plattform- Werkzeug, welches es ermöglichen soll, mobile Anwendungen (Apps) für die Smartphone-Betriebssysteme iOS und Android mit einer gemeinsamen Code-Basis zu entwickeln. [Ama18] In der etablierten App-Entwicklung ist es weit verbreitet, zwei separate Anwendungen für die beiden dominieren- den Betriebssysteme iOS und Android zu entwickeln. Eine Analyse von Ap- pfigures zeigt dabei, dass im Apple App Store 55 % der analysierten Apps auf Swift basieren, welches für die sogenannte native iOS-Entwicklung ge- nutzt wird, und im Google Play Store 38 % der analysierten Apps auf Kotlin basieren, welches dem Pendant zu Swift für Android entspricht.[App22] Das Entwickeln von zwei getrennten Anwendungen bringt dabei das Erforder- nis mit sich, Quelltext zu duplizieren, da für Android entwickelter Software nicht mit iOS kompatibel ist und umgekehrt. Neben Flutter existieren auch andere Cross-Plattform Werkzeuge, die das gleiche Problem lösen möchten. Diese werden jedoch nicht in dieser Aus- arbeitung näher betrachtet. Erwähnenswert jedoch sollte sein, dass das von
2.1 flutter 7 Meta entwickelte React Native SDK vom Design ähnlich ist und Flutter die- ses “well preserved” ([Wu18]) hat, sodass auch dieses SDK von den hier in der Ausarbeitung beschriebenen Problemen betroffen sein kann und die Ergebnisse somit als Basis für eine ähnliche Evaluation verwendet werden könnten. 2.1.2 Technischer Überblick Dieser Abschnitt vermittelt die technischen Grundlagen für die Flutter Tech- nologie. Flutter und die damit entwickelten Anwendungen werden mit der Dart-Programmiersprache entwickelt. Dart lässt sich dabei mit den bereits etablierten Programmiersprachen wie Java oder JavaScript vergleichen, wie in Listing 2.1 zu sehen ist, und ist damit eine objektorientierte, optional sta- tisch typisierte Programmiersprache[Goo21f]. Listing 2.1: Aufbau eines einfachen Flutter-Widgets in Dart import ’package:flutter/material.dart’; class ExampleWidget extends StatelessWidget { const ExampleWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const Row(children: [Text(’Hello World’), Text(’123’)]; } } Von der Architektur her ist Flutter in einem Schichtentwurf aufgebaut, wie in Abbildung 2.1 zu sehen ist. Abbildung 2.1: Architekturdiagramm von Flutter [Goo21a]
2.1 flutter 8 Die unterste Embedder-Schicht ist dabei für jede der unterstützten Plattfor- men einzeln implementiert und stellt den oberen Schichten Ressourcen zur Verfügung, wie beispielsweise ein Zeichnungsbereich, indem die Benutzero- berfläche gerendert werden kann. Zudem können hier native Erweiterungen eingebunden werden, die es der App später ermöglicht, von Flutter / Dart aus auf native Betriebssystemfunktionen wie die Kamera zuzugreifen. Die mittlere Engine-Schicht beinhaltet die meisten Performance-kritischen Komponenten und stellt eine universelle Plattform für das Framework zur Verfügung so beinhaltet sie beispielsweise die Rendering-Engine. Anders als bei anderen Cross-Plattform-Frameworks wird in Flutter die komplette Be- nutzeroberfläche nicht mit nativen Design-Elementen dargestellt, sondern mit dieser eigenen Rendering-Engine gezeichnet. Dies bietet dabei den Vor- teil, dass Design-Konzeptionen auf allen Plattformen gleich aussehen. Die obere Framework-Schicht setzt auf den beiden unteren Schichten auf und bietet die Schnittstellen, die zur Entwicklung von Apps benötigt werden. Zudem beinhaltet sie einen umfangreichen Katalog an Standard-Widgets, die sich am Design der beiden Betriebssysteme iOS und Android orientieren. Diese Widgets sind in den Cupertino-Katalog für das iOS-ähnliche Design und in den Material-Katalog für das besonders auf Android oft genutzte Material-Design eingeordnet. Das Konzept des Widgets, welches bereits öfters bisher erwähnt worden ist, wird in dem folgenden Kapitel näher beleuchtet. 2.1.3 Widgets Eines der wichtigsten Bestandteile des Flutter-Frameworks sind die soge- nannten Widgets. Der Satz “Everything is a widget” [Win20, Kap.1.9] wird in der Literatur oft verwendet, und bringt zum Ausdruck, dass Flutter das Konzept des Widgets für viele Anwendungsfälle nutzt. So kann ein Wid- get diverse Aufgaben übernehmen wie beispielsweise das Rendern einer UI- Komponente, Animationen oder das Anordnen von anderen Widgets. Zur Darstellung der Benutzeroberfläche benutzen also Widgets Kompositionen von diversen Widgets. So lassen sich beispielsweise mit einer Row, wie im Listing 2.1 zu sehen ist, mehrere Widgets nebeneinander anzeigen. Listing 2.2: Vereinfachte Darstellung eines Widgets als Methode [Win20] UI Widget(state) Ein wichtiger Unterschied zu klassischen deklarativen UI-Frameworks ist, dass Widgets nur die Anleitung zum Bauen einer Benutzeroberfläche bein- halten, jedoch nicht den aktuellen Zustand des Widgets. Im Detail bedeutet
2.1 flutter 9 dies, dass die build()-Methode eines Widgets keinerlei Nebeneffekte haben soll und daher eine schnelle Ausführungszeit zu erwarten ist. Bildlich lässt sich ein Widget als Funktion darstellen, welche den aktuellen Zustand (engl. State) erhält und daraus die entsprechende Benutzeroberfläche generiert wie in Listing 2.2 vereinfacht dargestellt. Ein Widget erhält die darzustellenden Daten und generiert daraus die entsprechende Benutzeroberfläche. In Flutter wird bei Widgets grundsätzlich zwischen StatelessWidget und StatefulWidget unterschieden. Ein StatelessWidget hat grundsätzlich keinen veränderbaren Zustand. Dies heißt, dass alle Klassenvariablen unveränderlich sein sollen. In Dart wird dies mit dem Modifikator final gekennzeichnet. Daraus wird impliziert, dass alle Informationen zum Zustand des Widgets aus den im Konstruk- tor übergebenen Werten stammen müssen. Somit wird das Widget nur neu gebaut, wenn sich Änderungen an den Werten des Konstruktors durch Än- derungen in der Hierarchie darüber liegenden Widgets ergeben. Ein StatefulWidget auf der anderen Hand besteht aus zwei Klassen. Zum einen dem StatefulWidget zum anderen dem State dieses Widgets. Diese Widgets vereinen die Möglichkeiten eines StatelessWidget mit der Mög- lichkeit, den Zustand des Widgets selbstständig zu verändern. Dies wäre beispielsweise bei einem Zähler relevant, wenn der Zähler erhöht werden soll. Zustandsänderungen werden dabei über die, wie in Listing 2.3 gezeig- te, setState Methode vorgenommen, damit auch die Änderung an das Fra- mework kommuniziert wird. Intern wird dabei das Widget als dirty mar- kiert, welches dazu führt, dass die Build-Methode des States erneut aufge- rufen wird durch das Framework. Entscheidend ist dabei, dass der State persistent bleibt, auch wenn sich beispielsweise Konstruktor-Parameter der StatefulWidget-Klasse ändern. Listing 2.3: StatefulWidget class Counter extends StatefulWidget { const Counter({Key? key}) : super(key: key); @override State createState() => _CounterState(); } class _CounterState extends State { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); }
2.2 automatisiertes testen 10 @override Widget build(BuildContext context) { return Column( children: [ Text("Count: $_counter"), IconButton(icon: Icon(Icons.add), onPressed: _incrementCounter), ], ); } } Durch die Kombination von diversen Widgets entsteht so ein Widget-Baum, wie in Abbil- dung 2.2 zu sehen ist. Dieser Baum lässt sich auch mittels Hilfswerkzeugen traversieren, ist aber von der Grundstruktur für einen unidirek- tionalen Datenfluss ausgelegt. Dies bedeutete, dass Widgets nur untergeordnete Widgets durch Änderung derer Zustände verändern können sol- len. Übergeordnete Widgets können somit - je- Abbildung 2.2: Flutter Wid- denfalls nicht direkt - verändert werden. get Tree Anders als bei der nativen Entwicklung wird hier also die Benutzeroberfläche deklarativ program- miert. Bei der Entwicklung wird also festgelegt, wie welches Element bei welchem Zustand auszusehen hat, ohne dabei den Kontrollfluss beschreiben zu müssen. Zusammenfassend wird festgehalten, dass der Zustand und die Verwal- tung des Zustands von Widgets einen wichtigen Teil im Lebenszyklus einer Flutter-Anwendung ist. Dies gibt schon einen Vorgriff darauf, dass hier eine Lösung gefunden werden muss, welche auf diverse Szenarien anwendbar sein muss. 2.2 automatisiertes testen Um Flutter-Anwendungen automatisiert testen zu können, bestehen in Flut- ter die drei Testkategorien “unit testing, widget testing [...][und] integration testing” [AA21, S. 21]. Unit-Tests werden zum automatisierten Testen einzelner Funktionen oder Klassen eingesetzt. Tests werden dabei als Lambda-Funktion konstruiert. Mit mitgelieferten Werkzeugen, lassen sich Werte darauf überprüfen, ob sie den erwarteten Wert entsprechen.
2.3 zustandsverwaltung 11 Widget-Tests testen einzelne oder mehrere Widgets darauf, ob sie dem ge- wünschten Verhalten entsprechen. Die Widget-Tests können dabei ohne die Verwendung eines iOS- oder Android-Simulator innerhalb von Flutter ge- testet werden. [Goo21e] Dafür existieren diverse Werkzeuge in der Flutter- Testing-Bibliothek, womit sich Widgets erstellt werden können oder bestimm- te Eigenschaften von Widgets überprüft werden können. Integration-Tests testen die Anwendung als ganzes und prüfen somit, ob die jeweiligen Komponenten auch korrekt untereinander funktionieren. [AA21, S. 21] Diese Art von Tests sind allerdings für die Evaluation nicht relevant und werden daher nicht näher behandelt. 2.3 zustandsverwaltung Nachdem nun die Grundlagen des Flutter-Frameworks und die Details der Zustandsverwaltung der Widgets im letzten Kapitel erläutert wurden, kann jetzt die Zustandsverwaltung im Detail betrachtet werden. Windmill fasst den Komplex der Zustandsverwaltung in Flutter in seinem Standardwerk Flutter in Action wie folgt zusammen: “State management is a combination of passing data around the app, but also re-rendering pieces of your app at the right time. All the re-rendering in Flut- ter is dependent on the State object and its lifecycle.” [Win20, Kap.8.1.2] Daraus ergibt sich, dass die State-Klasse in der Zustandsverwaltung von Flutter eine wichtige Rolle spielt, und alle Ansätze dieses Konzept benut- zen müssen, um in das Flutter-Framework integrierbar zu sein. Windmill beschreibt dabei die Aufgabe der Zustandsverwaltung eher auf einer Ebene des Datenflusses und des Ablaufs der Neu-Erstellung der Benutzeroberflä- che. Arshad sieht die Zustandsverwaltung dabei eher auf einer eher ablaufszen- trierten Sichtweise indem er die Aufgabe der Zustandsverwaltung wie folgt analysiert: “State management is simply a technique, or multiple techniques, used to take care of the changes that occur in your application.” [Ars21, Kap.1] Als Beispiele nennt er dabei, das Reagieren auf Interaktionen mit der An- wendung oder die Beibehaltung des Datenflusses über mehrere Screens hin- weg.
2.3 zustandsverwaltung 12 Beide Sichtweisen haben gemein, dass die Grundaufgabe der Zustandsver- waltung die Sicherstellung eines korrekten Zustands der Anwendung, ein- zelner Screens oder einzelner Widgets sein muss, sowie die mögliche Über- führung dieses Zustands in einen neuen Zustand als Reaktion auf Verände- rungen. Nachdem nun eingeführt wurde, was unter einer Zustandsverwaltung in Flutter zu verstehen ist, werden nun mögliche bestehende Ansätze für eine Zustandsverwaltung skizziert, um im weiteren Verlauf der Ausarbeitung im Analyse- und Evaluationskapitel diese eingehender zu untersuchen. 2.3.1 Auswahl Zur Auswahl der zu evaluierenden Lösungsansätze wird die Aufzählung von Zustandsverwaltungs-Ansätzen aus der Flutter-Dokumentation [Goo21d] als Grundlage verwendet. In dieser Aufzählung werden die Zustandsverwal- tungssysteme Provider, Riverpod, setState, InheritedWidget & -Model, Re- dux, Fish-Redux, BLoC / RX, GetIt, MobX, Flutter Commands, Binder, GetX, states_rebuilder und Triple Pattern genannt. Da eine Evaluation aller aufgezählten Zustandsverwaltungssysteme, den Um- fang dieser Ausarbeitung überschreiten würde, wurde die Auswahl einge- schränkt. Die Ansätze setState und InheritedWidget werden in die Evaluation mit auf- genommen, da sie zu der Grundausstattung der Flutter-Standardbibliothek gehören, und somit einen Basiswert für Zustandsverwaltungssysteme bilden und somit relevant sind. Das BLoC-Pattern wird aufgenommen, da es auf- grund der großen Verbreitung in der Literatur relevant ist. Die Bibliothek Provider wird ebenfalls aufgenommen, da es sich laut der Dokumentati- on um den empfohlenen Ansatz für Zustandsverwaltung in Flutter handelt, und die Bibliothek mit 6132 Like-Angaben [Goo22b] zu einer der beliebtes- ten Flutter-Pakete [Goo] auf der Plattform handelt. Riverpod wird ebenfalls aufgenommen, da dies den Ansatz von Provider weiterentwickelt und somit geprüft werden kann, ob diese Bibliothek tatsächlich besser abschneidet als die Original-Bibliothek. Zuletzt werden die Bibliotheken MobX und Redux aufgenommen, da sie besonders aufgrund ihrer Herkunft aus dem React- Ökosystem eine besondere Relevanz für die Evaluation haben, um ebenfalls feststellen zu können, ob bereits in React verbreitete Ansätze auch in Flutter sinnvoll einsetzbar sind.
2.3 zustandsverwaltung 13 2.3.2 Mitgelieferte Werkzeuge Die erste Kategorie der Zustandsverwaltungssysteme umfasst jene, welche ohne eine zusätzliche Bibliothek auskommen und somit de facto im Flutter Framework mitgeliefert werden. Zunächst werden einfacheren Konzepten und Werkzeugen vorgestellt, anschließend folgen die komplexeren Konzepte und Werkzeuge vorgestellt. 2.3.2.1 setState Die wohl grundlegendste Möglichkeit, den Zustand in einer Flutter An- wendung zu verwalten stellt das ausschließliche Benutzen der setState- Methode dar. Ein Beispiel zur Verwendung wurde bereits in Listing 2.3 in der incrementCounter-Methode eingeführt. Hier findet die Speicherung des Zustands also durch die direkte Manipulation des States von Stateful- Widgets statt. Wie vorausgehend beschrieben, muss ein Zu- standsverwaltungssystem aber nicht nur den Zu- stand einzelner Widgets verwalten können, son- dern auch von größeren Ordnungen wie bei- spielsweise von Screens oder der ganzen Anwen- dung. Um dies bei diesem Ansatz erreichen zu Abbildung 2.3: Flutter Wid- können, wir der Zustand oder Teile des Zustands get Tree bei über die Konstruktor innerhalb des Widget-Trees setState weiter nach unten gegeben. Anschaulich lässt sich dies durch das Beispiel Ab- bildung 2.3 darstellen, welches eine Anwendung, die global speichern muss, welche Person aktuell angemeldet ist, zeigt. Da diese Information in diesem Beispiel an diversen Stellen innerhalb der Anwendung benötigt wird, ergibt es Sinn, diese Information weit oben im Baum in Form eines StatefulWidget namens LoginStateWidget zu speichern, da der Datenfluss innerhalb des Baums ausschließlich unidirektional von oben nach unten stattfindet. Um diese Information nun an die Widgets zu kommunizieren, die es benötigen - in diesem Fall InformationConsumer - muss LoginStateWidget die Informa- tion per Konstruktor an das nachgelagerte Widget weitergeben. Diese nach- gelagerten Widgets (A, B, C) müssen dies ebenfalls tun, bis die Information am Ziel InformationConsumer angekommen ist. Dieses Anwendungsmuster wird in der Literatur als “lifting state up” [Win20, Kap.8.2] bezeichnet.
2.3 zustandsverwaltung 14 2.3.2.2 InheritedWidget Neben der Möglichkeit, den Zustand über den Widget-Baum nach unten weiter zu propagieren, bietet Flutter noch das Konzept InheritedWidget an. Diese Widgets bilden eine eigene Widget-Gruppe und sind weder den StatefulWidgets noch den StatelessWidgets zuzuordnen. [Win20, Kap.8.2.1] InheritedWidgets ermöglichen es nachgeordneten Widgets, auf den Zustand des Widgets direkt zuzugreifen. Hier muss allerdings beachtet werden, dass das InheritedWidget immer unveränderlich ist. Dies bedeutet, dass ande- re Widgets über die Veränderung von Konstruktor-Parametern neue Instan- zen des Widgets erstellen müssen, um eine Zustandsänderung zu bewirken. Daher lassen sich diese Widgets oft in Kombination mit StatefulWidgets, welche für die Manipulation des Zustands zuständig sind, vorfinden. Im Beispiel Listing 2.4 kann man erkennen, dass das Widget lediglich die Daten lagert, welche zur Verfügung gestellt werden sollen. In diesem Fall ist dies der aktuelle Benutzende currentUser. Diese*r kann nicht vom In- heritedWidget selbst geändert werden, sondern hängt von der Eingabe im Konstruktor ab. Das InheritedWidget wird dabei gemäß dem Motto “Eve- rything is a widget” in den Widget-Baum integriert. Der Konstruktor-Para- meter child gibt dabei das im Baum untergeordnete Widget an. Über die updateShouldNotify-Methode wird dem Framework kommuniziert, ob sich der Zustand im Vergleich zum vorherigen geändert hat, und somit ein Neu- bauen der Widgets, die dieses InheritedWidget referenzieren, notwendig ist. Listing 2.4: Aufbau eines InheritedWidget import ’package:flutter/widgets.dart’; class UserStore extends InheritedWidget { const UserStore({ Key? key, required this.currentUser, required Widget child, }) : super(key: key, child: child); final User currentUser; static UserStore? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); } @override bool updateShouldNotify(UserStore old) { return currentUser != old.currentUser; } }
2.3 zustandsverwaltung 15 Anders als bei dem setState-Konzept kann hier direkt der Zustand durch andere nachgelagerte Widgets referenziert werden. Dafür werden oft Hilfs- methoden wie in diesem Fall die of-Methode verwendet. Diese greifen auf Werkzeuge des Frameworks zu, die das InheritedWidget des angebenden Typen zurückgibt und sicherstellt, dass bei einer Veränderung des das refe- renzierende Widget auch neu gebaut wird und so die Änderung beachtet wird. Dieser Mechanismus wird auch von diversen anderen Zustandsverwaltungs- systemen verwendet. Als Erweiterung des InheritedWidget kann man InheritedModel sehen. Da- bei ist die Funktionsweise äquivalent mit einer Besonderheit. Es besteht hier die Möglichkeit, Zugriffe und Änderungen nach sogenannten aspects zu kategorisieren. Somit kann bei komplexeren Zuständen, es ermöglicht wer- den, dass die Widgets nur dann neu gebaut werden, wenn der betreffende aspect sich ändert. [Goo21c] 2.3.3 Business Logic Components (BLoC) Das 2018 auf der Entwicklerkonferenz DartConf vorgestellte BLoC-Pattern ist im Vergleich zu den bisher vorgestellten Ansätzen ein Design-Pattern zur Verwaltung von Zuständen und nicht nur ein Werkzeug des Frameworks. Das Ziel von BLoC ist es, die komplette Logik von der Benutzeroberfläche zu trennen. [Fau20, S. 17] Die Logik und der Zustand wird dabei in den na- mensgebenden Business Logic Components (BLoC) verwaltet. Die Kompo- nenten haben die Aufgabe, Zustands-Ereignisse von Widgets zu empfange und Widgets zu aktualisieren, wenn sich der Zustand ändert. Diese Kompo- nenten unterliegen grundlegenden, nicht-verhandelbaren Regeln, welche im Vortrag von Soares definiert worden sind: “ 1. Inputs and outputs are simple Streams/Sink only 2. Dependencies must be injectable and platform agnostic 3. No platform branching allowed [...] ” [Soa18, 24:04] Die erste Regel bedeutet, dass BLoC weder Methoden noch Variablen nach außen freigeben dürfen, sondern nur über Streams und Sinks mit Widgets kommunizieren. Ein Stream ist dabei in Flutter ein asynchroner Fluss von
2.3 zustandsverwaltung 16 Daten oder Ereignissen. Widgets können diesen Ereignisfluss abonnieren und werden dann aktualisiert, wenn sich dieser ändert. Ein Sink ist intern auch eine Art von Stream, welcher aber die Besonderheit hat, dass man von außen neue Ereignisse hinzufügen kann. Über diesen Sink lassen sich also Daten und Ereignisse an das BLoC übergeben. Die zweite Regel sagt aus, dass BLoC keine Abhängigkeiten zur Benutzero- berfläche haben dürfen. Selbst das Importieren von Flutter-Bibliotheken in diese Dateien ist verboten. Damit wird erreicht, dass BLoC komplett plattfor- munabhängig sind, und somit die komplette Benutzeroberfläche theoretisch ersetzt werden könnte, ohne die Logik ändern zu müssen. Die dritte Regel legt fest, dass innerhalb von BLoCs keine Unterscheidungen zwischen Betriebssystemen oder Plattformen vorgenommen werden darf. Der innere Aufbau der BLoC ist explizit nicht vorgeschrieben, dient aber da- zu die über die Sinks eingehenden Daten und Ereignisse zu verarbeiten und anschließend den neuen Zustand über die Streams zurück an die Widgets zu propagieren. Technisch kommen hier oft Techniken und Werkzeuge aus der reaktiven Programmierung wie RxDart zum Einsatz. Jede Seite (engl. Screen) sollte dabei exakt einem BLoC zugeordnet sein. Da- mit die Widgets auf diesen BLoC zugreifen können, müssen diese injectable sein - also zwischen mehreren Widgets geteilt. Dies kann unter anderem mit den bereits in Unterabschnitt 2.3.2 vorgestellten Ansätzen umgesetzt wer- den. 2.3.4 Provider Provider ist eine Bibliothek, die auf dem InheritedWidget-Ansatz (siehe Unterunterabschnitt 2.3.2.2) beruht. Dabei vereinfacht die Bibliothek die Be- nutzung und Erstellung von Klassen, die Zustände verwalten, und kombi- niert das InheritedWidget-Konzept unter anderem mit der ChangeNotifer- Klasse. [Sle20] Die abstrakte Klasse ChangeNotifier bietet die Möglichkeit Event-Listener für die erbende Klasse zu registrieren und bei Veränderung über den Aufruf einer entsprechenden Methode diese zu benachrichtigen, wie in Listing 2.5 in der Methode updateUser gezeigt wird. [Goo21b] Diese Kombinationsmög- lichkeit wird auch für andere Konzepte und Klassen angeboten. So lassen sich Provider mit Listnables, also Objekte, die Events emittieren können, ValueListanble, also Listnables, die nur über Änderungen einer Variable benachrichtigen, Streams, welche bereits in Unterabschnitt 2.3.3 vorgestellt worden sind oder Futures kombinieren.
2.3 zustandsverwaltung 17 Listing 2.5: Aufbau einer ChangeNotifier-Klasse für Provider class UserStore extends ChangeNotifier { var user; UserStore(this.user); void updateUser(newUser) { this.user = newUser; notifyListeners(); } } Es kann festgehalten werden, dass Provider im ersten Schritt eine Form von Dependency Injection zur Verfügung stellt und im zweiten Schritt mit einer Klasse verknüpft wird, welche über Änderungen informiert, und somit zu einer Zustandsverwaltungslösung ausgebaut werden kann. Listing 2.6: Injektion und Abruf von Provider-Klassen class ProvidingWidget extends StatelessWidget { final user; const ProvidingWidget({Key? key, this.user}) : super(key: key); @override Widget build(BuildContext context) { return ChangeNotifierProvider(create: (context) => UserStore(user), child: ConsumingWidget()); } } class ConsumingWidget extends StatelessWidget { const ConsumingWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final user = Provider.of(context).user; return Text(user.firstName); } } Um eine ChangeNotifier als Zustandsverwaltungslösung mit Provider zu nutzen, ist es erforderlich, die jeweiligen Zustandsklassen (oft Store genannt) in den Widget-Baum zu integrieren. Dies erfolgt wie in der Klasse Providing- Widget in Listing 2.6 gezeigt, durch das Erstellen eines Widgets, welche kei- ne eigene Benutzeroberfläche darstellen, sondern nur Widgets “nach unten” durchreichen. Nun können die Daten aus der Zustandsklasse von allen Wid- gets abgerufen werden, die sich im Widget-Baum an einen der nachgelager- ten Äste der injizierenden Widgets befindet.
2.3 zustandsverwaltung 18 Die nachgelagerten Widgets können dabei, die Zustandsklasse durch eine einfachen, generischen Methodenaufruf der Provider-Bibliothek abrufen. Durch die Verwendung von InheritedWidget innerhalb der Bibliothek wird damit sichergestellt, dass wenn sich Daten in den Zustandsklassen ändern, referenzierende Widgets über diese Änderung informiert werden und gege- benenfalls neu gebaut werden. 2.3.5 Riverpod Riverpod baut auf dem bereits vorgestellten Provider-Konzept auf, ergänzt es aber mit weiteren Funktionalitäten. Ein großer Unterschied liegt auch dar- in, dass hier Provider nicht in den Widget-Baum integriert werden müssen. Zudem ist es hier anders als bei der Provider-Bibliothek möglich, mehre- re Provider vom gleichen Klassentyp zu unterscheiden. Hier wird nämlich nicht der Klassentyp zum Abruf der Zustandsklasse im Widget verwendet, sondern eine Variable. Wie diese im Widget abgerufen wird, ist dabei von den Entwickler*innen zu entscheiden. Denkbar sind hier beispielsweise das Verwenden einer globalen Variable oder einer Dependency-Injection-Lösung wie get_it. Damit wird auch ermöglicht, dass Riverpod im Vergleich zu Pro- vider keine Abhängigkeit zum Flutter-Framework hat und somit eine reine Dart-Bibliothek ist. [Gre+21, S. 8] Durch diese Unabhängigkeit vom Widget-Tree und dem Flutter-Framework kann hier auf das sogenannte Nesting verzichtet werden. Dieses wird bei Provider benötigt, um Provider zu initialisieren, die von anderen Providern abhängen. Riverpod nutzt hier stattdessen eine Methode, mit der bei der Erstellung eines Providers andere Provider gelesen werden können. Listing 2.7: Zustandsklasse in Riverpod class UserStore extends StateNotifier { UserStore(User initialState) : super(initialState); changeFirstName(String newName) { state = User(newName); } } Für die Zustandsklassen selber gibt Riverpod eine eigene Struktur vor. Die Zustandsklassen erben die generische Klasse StateNotifier. Dabei wird über das Generic festgelegt, welchen Datentyp der Zustand haben soll. Die- ser Zustand wird dann der Klasse als vererbte Variable zur Verfügung ge- stellt. Bei Änderung des Zustands reicht es somit aus, den Inhalt dieser Va- riable neu zu setzen wie in Listing 2.7 gezeigt wird. Die Benutzeroberfläche greift dabei ausschließlich auf die Zustands-Variable direkt zu.
2.3 zustandsverwaltung 19 Die Zustandsklasse ist nur dafür zuständig, Methoden zur Änderung dieses Zustands zur Verfügung zu stellen. Riverpod akzeptiert neben diesen Zustandsklassen auch die im Provider- Kapitel (siehe Unterabschnitt 2.3.4) vorgestellen Event-Emitter wie beispiels- weise ChangeNotifier. Da wie bereits erwähnt, Riverpod keine Flutter-Bibliothek ist und somit nicht wie Provider InheritedWidget zum Aktualisieren von referenzieren- den Widgets benutzt, unterstützt sie von Haus aus noch nicht die Nutzung innerhalb eines Widgets. Um dies jedoch zu ermöglichen, besteht die Wahl zwischen zwei Ansätzen, die sich nur semantisch unterscheiden. Listing 2.8: ConsumerWidget final userStoreProvider = StateNotifierProvider((ref) => UserStore()); class ConsumingWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final userStore = ref.watch(userStoreProvider); return Text(userStore?.state.firstName); } } Bei flutter_riverpod werden die bereits eingeführten Widget-Typen State- fulWidget und StatelessWidget durch neue Typen ergänzt beziehungswei- se ersetzt. Einer dieser neuen Typen ist ConsumerWidget. Durch das Erben von dieser Klasse, wird der build-Methode des Widgets eine neue Variable ergänzt, über die der Zustand von Riverpod-Providern abgefragt werden kann, wie in Listing 2.8 zu sehen ist. Die hier verwendete watch-Methode bewirkt, dass das Widget neu gebaut wird, wenn sich der betreffende Provi- der ändert. 2.3.6 Redux Redux ist ein Zustandsverwaltungssystem, welches ursprünglich für React entwickelt worden ist. React ist eine JavaScript-Bibliothek zum Bauen von Benutzeroberflächen. Da React und Flutter ähnliche Techniken wie Widgets oder das State-Prinzip verwenden, lässt sich dieser Ansatz auf Flutter und Dart übertragen.
2.3 zustandsverwaltung 20 Redux basiert auf drei grundlegenden Prinzipien: Single source of truth [...] State is read-only [...] Changes are made with pure functions [...] [GF18, Kap.1.3.2] Das Prinzip der “Single source of truth” [GF18, Kap.1.3.2] wird dadurch umgesetzt, dass es für die komplette Anwendung nur einen zusammenge- fassten Zustand gibt. Es findet hier also keine Aufteilung in diverse Unter- zustände beispielsweise für einzelne Seiten statt. Abbildung 2.4: Datenfluss in Redux [GF18, Kap.1.3.3] Dieser globale Zustand ist, wie im zweiten Prinzip dargestellt wird, unver- änderlich. Um den Zustand zu ändern beziehungsweise in diesem Fall zu ersetzen, ist es erforderlich, sogenannte actions zu emittieren. Actions sind dabei in Flutter einfache Objekte, die auch Parameter in Form von Variablen beinhalten können. Diese Action wird dann wie in Abbildung 2.4 anschau- lich zu sehen ist, über einen Reducer auf den globalen Zustand (engl. store) angewendet. Damit wird dann auch das dritte Prinzip umgesetzt. Ein Reducer stellt eine einfache Funktion dar, welche als Parameter den aktuellen Zustand und die Action erhält und als Rückgabewert einen neuen Zustand angibt, welcher den globalen Zustand ersetzt. Für Dart wurde dies mit der redux Bibliothek umgesetzt. Ähnlich wie bei Riverpod wird hier noch eine zusätzliche Bibliothek benötigt, die Redux für das Flutter-Widget-System anwendbar macht. Dafür wird flutter_redux verwendet. Diese adaptiert das Prinzip der Provider-Bibliothek für Redux- Stores. So wird der Store mittels eines Provider-Widgets über den Widget- Tree für nachgelagerte Widgets zur Verfügung gestellt. Auf den Store zu- gegriffen wird über Selektoren, welche den benötigten Teil des globalen Zu- stands eingrenzen und per Callback-Funktion zurückgeben. Somit wird über diese Selektoren auch die Veränderungen der Widgets ausgelöst, wenn sich der entsprechende Teil des globalen Zustands ändert.
2.3 zustandsverwaltung 21 2.3.7 MobX MobX ist wie Redux ebenfalls ein Ansatz, der ursprünglich aus dem React- Ökosystem stammt. MobX stellt sich dabei die Aufgabe, es zu vereinfachen automatisch diverse Informationen aus dem Anwendungs-Zustand abzulei- ten. Um dies zu erreichen, gibt es bei MobX fünf grundlegende Konzepte: “ • Observables [...] • Computed Values [...] • Reactions [...] • Actions [...] ” [Mez18, S. 130] Observables bilden die Grundlage eines Zustands. Ein Observable stellt einen Wert dar, welcher beobachtet werden kann und somit auf Änderungen rea- giert werden kann. Von Observables werden alle anderen Informationen ab- geleitet. Mit Computed Values lassen sich ein oder mehrere Observables kombi- nieren oder einer weiteren Verarbeitung unterwerfen. Wenn sich eines der zugrundeliegenden Observables ändert, wird auch dieser Wert neu berech- net. Reactions reagieren auf Änderungen von Observables oder Computed Va- lues und lösen Seiteneffekte aus. Ein Beispiel hierfür ist das Observer-Widget, welches es ermöglicht, Widgets neu zu bauen, wenn sich die referenzierten Observables ändern. Actions werden wie bei Redux hier ebenfalls verwendet, um Änderungen am Zustand - hier also Observables - durchzuführen. Allerdings wird hier als Action eine Funktion verstanden, welche Observables abändert und nicht ein Objekt, welche mithilfe eines Reducers den Zustand mutiert. Observables, Computed Values und Actions werden dabei oft in Klassen zu einem Store zusammengefasst. Die Instanzen dieser Stores müssen dabei über einen Service Locator ähnlich wie bei Riverpod in die Widgets injiziert werden, damit diese den Store mithilfe von Observern nutzen können.
A N A LY S E 3 Nachdem nun die diversen existierenden Ansätze für die Zustandsverwal- tung in Flutter vorgestellt worden sind, muss im nächsten Schritt untersucht werden, welche Anforderungen an solche Systeme gestellt werden. Zudem soll daraus abgeleitet werden, welche Kriterien für die spätere Evaluation entscheidend sind. In “State management approaches in Flutter”[Sle20] wurden bereits Bewer- tungskriterien für eine Evaluation von Zustandsverwaltungssystemen aufge- stellt, allerdings kann die Herleitung der Bewertungskriterien nicht nachvoll- zogen werden, da die verwendeten Quellen nicht die zitierten Bewertungs- kriterien enthalten. Daher werden nun neue Bewertungskriterien definiert. 3.1 anforderungsanalyse Die grundlegende Anforderung für Zustandsverwaltungssysteme ist es, dass sie den Zustand einer Anwendung und bestimmter Teile einer Anwendung wie eines Widgets oder einer ganzen Seite verwalten können. Alle bereits vorgestellten Lösungsansätze können dies mit mehr oder weniger Aufwand auch erfüllen. Allerdings ist es neben der Aussage, ob ein System diese An- forderung umsetzen kann, auch wichtig wie und in welcher Qualität diese umgesetzt werden. 3.1.1 Allgemeine Anforderungen Die Wahl eines Zustandsverwaltungssystems bestimmt die Architektur einer Anwendung signifikant mit. Daher sind die Anforderungen an eine gute Ar- chitektur oder ein gutes Software-Design auch in Teilen auf Zustandsverwal- tungssysteme übertragbar. Um nun daraus Anforderungen zu konstruieren, ist es also erforderlich, sich anzuschauen, was ein gutes Software-Design überhaupt ausmacht. Zur Bewertung von Software-Qualität wurden diverse Anforderungen und Kriterien entwickelt. Diese wurden mit der ISO-Norm 9126 standardisiert.
3.1 anforderungsanalyse 23 Eines der Qualitätsmerkmale ist die Wartbarkeit von Software, die wie folgt, beschrieben wird: Fähigkeit des Softwareprodukts änderungsfähig zu sein. Ände- rungen können Korrekturen, Verbesserungen oder Anpassungen der Software an Änderungen der Umgebung, der Anforderun- gen und der funktionalen Spezifikationen einschließen [Bal09, S. 470] Daraus ergeben sich auch Anforderungen an die Quelltext-Qualität. Darun- ter wird beispielsweise die Anforderungen der “Änderbarkeit[...] [und] Test- barkeit” [Bal09, S. 470] verstanden. Diese Anforderungen können auch für Zustandsverwaltungssysteme übernommen werden. Zustandsverwaltungs- systeme und die Anwendungen, die diese verwenden, müssen effizient ver- änderbar und erweiterbar sein. Zusätzlich ist eine wichtige Anforderung, dass die resultierende Anwendung auch automatisiert testbar sein muss, um zu prüfen, ob sie den gewünschten Anforderungen entspricht. Hinzukommend spieln auch noch weitere Anforderungen zur Sicherstel- lung von Software-Qualität eine Rolle wie in “Software quality metrics for object-oriented environments” beschrieben wird. So seien die Attribute der Effizienz, Komplexität, Verständlichkeit, Wiederverwendbarkeit und Testbar- keit/Wartbarkeit von Bedeutung. [vgl. RH97, S. 1] Diese Anforderungen lassen sich auf den beschriebenen Evaluationsfall übertragen, müssen aller- dings noch auf diesen Anwendungsfall hin angewandt werden. Angewendet auf Zustandsverwaltungssysteme könnte die Effizienz dadurch beschrieben werden, wie effizient das Aktualisieren der Widgets im Widget- Baum erfolgt. Beispielsweise könnte hier untersucht werden, ob nur die Wid- gets neu erstellt werden, die von einer Änderung wirklich betroffen sind. Die Verständlichkeit kann angewendet so gedeutet werden, ob die durch die Verwendung des Systems neu geschaffenen Strukturen auch einfach zu verstehen - also verständlich sind. Die Wiederverwendbarkeit lässt sich nicht gut auf Zustandsverwaltungssys- teme übertragen, da die geschaffenen Strukturen immer auf den spezifischen Anwendungsfall ausgerichtet sind, und somit eine Wiederverwendbarkeit keine hohe Relevanz hat. Die Testbarkeit/Wartbarkeit wurde bereits bei den Erläuterungen zum ISO- Standard berücksichtigt und lässt sich auch auf den Anwendungsfall wie beschrieben anwenden.
Sie können auch lesen