You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
thesis/chapters/basics/state-management.tex

115 lines
11 KiB

\pagebreak
\section{Zustandsverwaltung}
Nachdem nun die Grundlagen des Flutter-Frameworks und die Details der Zustandsverwaltung der Widgets im letzten Kapitel erläutert wurden, kann jetzt betrachtet werden, was überhaupt die Zustandsverwaltung umfasst.
\citeauthor{flutterinaction} fasst den Komplex der Zustandsverwaltung in Flutter in seinem Standardwerk \textit{\citetitle{flutterinaction}} wie folgt zusammen:
\begin{displayquote}[{\cite[Kap.8.1.2]{flutterinaction}}]
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 Flutter is dependent on the State object and its lifecycle.
\end{displayquote}
Daraus ergibt sich, dass die State-Klasse in der Zustandsverwaltung von Flutter eine wichtige Rolle spielt, und alle Ansätze dieses Konzept benutzen müssen, um in das Flutter-Framework integrierbar zu sein. \citeauthor{flutterinaction} beschreibt dabei die Aufgabe der Zustandsverwaltung eher auf einer Ebene des Datenflusses und des Ablaufs der Neu-Erstellung der Benutzeroberfläche.
\citeauthor{managingstateinflutter} sieht die Zustandsverwaltung dabei eher auf einer eher ablaufszentrierten Sichtweise indem er die Aufgabe der Zustandsverwaltung wie folgt analysiert:
\begin{displayquote}[{\cite[Kap.1]{managingstateinflutter}}]
State management is simply a technique, or multiple techniques, used to take care of the changes that occur in your application.
\end{displayquote}
Als Beispiele nennt er dabei, das Reagieren auf Interaktionen mit der Anwendung oder die Beibehaltung des Datenflusses über mehrere Screens hinweg.
Beide Sichtweisen haben gemein, dass die Grundaufgabe der Zustandsverwaltung die Sicherstellung eines korrekten Zustands der Anwendung, einzelner Screens oder einzelner Widgets sein muss, sowie die mögliche Überführung dieses Zustands in einen neuen Zustand als Reaktion auf Veränderungen.
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.
\subsection{Mitgelieferte Werkzeuge}
\label{chap:included}
Die erste Kategorie der Zustandsverwaltungssysteme umfasst jene, welche ohne eine zusätzliche Bibliothek auskommen und somit de facto im Flutter Framework mitgeliefert werden. Hierbei wird mit den einfacheren Konzepten und Werkzeugen begonnen und anschließend die komplexeren Konzepte und Werkzeuge vorgestellt.
\subsubsection{setState}
\label{chap:setState}
Die wohl grundlegendeste Möglichkeit, den Zustand in einer Flutter Anwendung zu verwalten stellt das ausschließliche Benutzen der \texttt{setState}-Methode dar. Ein Beispiel zur Verwendung wurde bereits in \autoref{lst:stateful} in der \texttt{incrementCounter}-Methode eingeführt. Hier findet die Speicherung des Zustands also durch die direkte Manipulation des States von StatefulWidgets statt.
\begin{wrapfigure}{l}{0.33\textwidth}
\centering
\includegraphics[width=0.98\linewidth]{chapters/basics/setState_tree.png}
\caption{Flutter Widget Tree bei setState}
\label{fig:flutterTreeSetState}
\end{wrapfigure}
Wie vorausgehend beschrieben, muss ein Zustandsverwaltungssystem aber nicht nur den Zustand einzelner Widgets verwalten können, sondern auch von größeren Ordnungen wie beispielsweise von Screens oder der ganzen Anwendung. Um dies bei diesem Ansatz erreichen zu können, wir der Zustand oder Teile des Zustands über die Konstruktor innerhalb des Widget-Trees weiter nach unten gegeben.
Anschaulich lässt sich dies durch das Beispiel \autoref{fig:flutterTreeSetState} 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, macht es Sinn, diese Information weit oben im Baum in Form eines \texttt{StatefulWidget} namens \texttt{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 \texttt{InformationConsumer} - muss \texttt{LoginStateWidget} die Information per Konstruktor an das nachgelagerte Widget weitergeben. Diese nachgelagerten Widgets (\texttt{A, B, C}) müssen dies ebenfalls tun, bis die Information am Ziel \texttt{InformationConsumer} angekommen ist. Diese Anwendungsmuster wird in der Literatur als \blockquote[{\cite[Kap.8.2]{flutterinaction}}]{lifting state up} bezeichnet.
 \subsubsection{InheritedWidget}
\label{chap:inheritedWidget}
Neben der Möglichkeit, den Zustand über den Widget-Baum nach unten weiter zu propagieren, bietet Flutter noch das Konzept \texttt{InheritedWidget} an. Diese Widgets bilden eine eigene Widget-Gruppe und sind weder den \texttt{StatefulWidgets} noch den \texttt{StatelessWidgets} zuzuordnen. \autocite[Kap.8.2.1]{flutterinaction} \texttt{InheritedWidgets} ermöglichen es nachgeordneten Widgets, auf den Zustand des Widgets direkt zuzugreifen. Hier muss allerdings beachtet werden, dass das \texttt{InheritedWidget} immer unveränderlich ist. Dies bedeutet, dass andere Widgets über die Veränderung von Konstruktor-Parametern neue Instanzen des Widgets erstellen müssen, um eine Zustandsänderung zu bewirken. Daher lassen sich diese Widgets oft in Kombination mit \texttt{StatefulWidgets}, welche für die Manipulation des Zustands zuständig sind, vorfinden.
Im Beispiel \autoref{lst:inheritedWidgetExample} 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 \texttt{currentUser}. Diese*r kann nicht vom \texttt{InheritedWidget} selbst geändert werden, sondern hängt von der Eingabe im Konstruktor ab. Das \texttt{InheritedWidget} wird dabei gemäß des Mottos \blockquote{Everything is a widget} in dem Widget-Baum integriert. Der Konstruktor-Parameter \texttt{child} gibt dabei das im Baum untergeordnete Widget an. Über die \texttt{updateShouldNotify}-Methode wird dem Framework kommuniziert, ob sich der Zustand im vergleich zum vorherigen geändert hat, und somit ein Neubauen der Widgets, die dieses \texttt{InheritedWidget} referenzieren, notwendig ist.
\begin{lstlisting}[caption={Aufbau eines InheritedWidget}, label={lst:inheritedWidgetExample}]
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<UserStore>();
}
@override
bool updateShouldNotify(UserStore old) {
return currentUser != old.currentUser;
}
}
\end{lstlisting}
Anders als bei dem \texttt{setState}-Konzept kann hier direkt der Zustand durch andere nachgelagerte Widgets referenziert werden. Dafür werden oft Hilfsmethoden wie in diesem Fall die \texttt{of}-Methode verwendet. Diese greifen auf Werkzeuge des Frameworks zu, die das \texttt{InheritedWidget} des angebenden Typen zurückgibt und sicherstellt, dass bei einer Veränderung des \texttt{InheritedWidget} das referenzierende Widget auch neugebaut wird und so die Änderung beachtet wird.
Dieser Mechanismus wird auch von diversen anderen Zustandsverwaltungssystemen verwendet.
Als Erweiterung des \texttt{InheritedWidget} kann man \texttt{InheritedModel} sehen. Dabei ist die Funktionsweise äquivalent mit einer Besonderheit. Es besteht hier nämlich die Möglichkeit Zugriffe und Änderungen nach sogenannten \texttt{aspects} zu kategorisieren. Somit kann bei komplexeren Zuständen es ermöglicht werden, dass Widgets nur dann neugebaut werden, wenn der betreffende \text{aspect} sich ändert. \autocite{inheritedModel}
\subsection{\acf{bloc}}
Das 2018 auf der Entwicklerkonferenz DartConf vorgestellte \ac{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 \ac{bloc} ist es, die komplette Logik von der Benutzeroberfläche zu trennen. \autocite[S.17]{Faust} Die Logik und der Zustand wird dabei in den namensgebenden \acf{bloc} verwaltet. Die Komponenten haben die Aufgabe, Zustands-Ereignisse von Widgets zu empfange und Widgets zu aktualisieren, wenn sich der Zustand ändert. Diese Komponenten unterliegen grundlegenden, nicht-verhandelbaren Regeln, welche im Vortrag von \citeauthor{blocTalk} definiert worden sind:
\blockquote[{\cite[24:04]{blocTalk}}]{\begin{enumerate}
\item Inputs and outputs are simple Streams/Sink only
\item Dependencies must be injectable and platform agnostic
\item No platform branching allowed [...]
\end{enumerate}}
Die erste Regel bedeutet, dass \ac{bloc} weder Methoden noch Variablen nach außen freigeben dürfen, sondern nur über \texttt{Stream}s und \texttt{Sink}s mit Widgets kommunizieren. Ein \texttt{Stream} ist dabei in Flutter ein asynchroner Fluss von Daten oder Ereignissen. Widgets können diesen Ereignissfluss abonnieren und werden dann aktualisiert, wenn sich dieser ändert. Ein \texttt{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 \texttt{Sink} lassen sich also Daten und Ereignisse an das \ac{bloc} übergeben.
% TODO ggf noch beispiele einsetzen, wie die regeln anzuwenden sind
Die zweite Regel sagt aus, dass \ac{bloc} keine Abhängigkeiten zur Benutzeroberfläche haben dürfen. Selbst das Importieren von Flutter-Bibliotheken in diese Dateien ist verboten. Damit wird erreicht, dass \ac{bloc} komplett plattformunabhä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 \ac{bloc}s keine Unterscheidungen zwischen Betriebssystemen oder Plattformen vorgenommen werden darf.
Der innere Aufbau der \ac{bloc} ist explizit nicht vorgeschrieben, dient aber dazu die über die \texttt{Sink}s eingehenden Daten und Ereignisse zu verarbeiten und anschließend den neuen Zustand über die \texttt{Stream}s zurück an die Widgets zu propagieren. Technisch kommen hier oft Techniken und Werkzeuge aus der reaktiven Programmierung wie \texttt{RxDart} zum Einsatz.
Jede Seite (engl. Screen) sollte dabei exakt einem \ac{bloc} zugeordnet sein. Damit die Widgets auf diesen \ac{bloc} zugreifen können, müssen diese \texttt{injectable} sein - also zwischen mehreren Widgets geteilt. Dies kann unter anderem mit den bereits in \autoref{chap:included} vorgestellten Ansätzen umgesetzt werden.
\subsection{Provider}
\subsection{Riverpod}
\subsection{Redux}
\subsection{GetIt}
\subsection{MobX}
\subsection{Binder}