App State-Management mit Flutter BLoC
Flutter ist ein deklaratives Framework, bei dem die UI auf den aktuellen Zustand der App reagiert. Doch wie verwaltet man diesen Zustand? Dafür gibt es mehrere Ansätze. Einer davon, der in all unsere bisherigen Flutter-Projekten zum Einsatz kommt, ist das sogenannte BLoC Pattern, das ich in diesem Blogpost kurz vorstellen will.
Was ist BLoC?
BLoC steht für Business Logic Component und beschreibt ein Desing-Pattern, das Entwicklern ermöglicht, den Zustand über die gesamte App zu steuern und von der Präsentationsschicht zu entkoppeln. Durch diese Trennung werden eine höhere Wiederverwendbarkeit und Testbarkeit geschaffen.
Um dies zu erreichen, horcht der BLoC, in dem die Businesslogik gekapselt wird, auf einen Stream von Events aus dem Frontend. Der Bloc verarbeitet die empfangenen Events, macht ggf. Abfragen gegen eine Datenschicht und gibt den aktualisierten Zustand zurück, auf den die UI reagiert.
Quelle: Bloc Architecture
Solch ein Event könnte zum Beispiel eine Nutzereingabe sein, um einen Artikel in den Warenkorb zu legen. Der Nutzer löst dieses über den Klick/Tab auf einen Button aus, welcher vom korrespondierenden Bloc entgegengenommen wird. Auch alle weiteren UI-Objekte, die auf den Zustand des Warenkorb-Bloc horchen, bekommen diese Aktualisierung mit und können entsprechend darauf reagieren und ihre Darstellung anpassen, z.B. ein Badge mit der Anzahl im Warenkorb befindlichen Artikel.
Warum BLoC?
Flutter BLoC wurde erstmals auf der Google I/O 2018 als State-Management Plugin empfohlen und hat sich seitdem zu einem der Standards für Flutter entwickelt. Doch wozu ein extra Plugin? Die Zustandsverwaltung ist kritisch für einen fehlerfreien Ablauf einer App und die Abbildung mit Bordmitteln wird vor allem bei großen und komplexen Projekten umständlich.
BLoC verspricht hier Abhilfe und wurde mit drei Grundprinzipien aufgebaut:
- Unkompliziert: Einfach zu verstehen und kann von Entwicklern mit unterschiedlichen Vorkenntnissen genutzt werden
- Leistungsstark: Hilft bei der Erstellung von komplexen Anwendungen durch die Zerlegung in kleinere Komponenten
- Testbar: Testen Sie einfach jeden Aspekt einer Anwendung, um mit Zuversicht iterieren können.
BLoC erlaubt es, den Applikationsstatus zu jedem Zeitpunkt nachzuverfolgen und zu testen. Mehrere Widgets können auf denselben BLoC zugreifen und mit diesem interagieren, um Code-Duplikation zu vermeiden. Genauso können aber auch verschiedene Teile einer App inhaltlich getrennt und separat verwaltet werden. Dabei bleibt der Programmcode dank Konventionen und Best-Practices verständlich und wartbar.
Natürlich ist BLoC nicht die einzige Lösung für das Management des Applikationsstatus. Im Gegenteil gibt es inzwischen eine Fülle an alternativen Lösungen, die im Kern dasselbe Problem lösen. Die Wahl hängt immer vom konkreten Team und den Herausforderungen des Projekts ab. Wenn viele der Entwickler z.B. aus dem JavaScript Bereich kommen und bereits viel Erfahrung mit Redux gesammelt hat, kann dieses Package nutzen. BLoC verfolgt hier einen ähnlichen Ansatz, ist aber von Grund auf für Flutter geschrieben.
Eine Liste von weiteren verfügbaren Zustands-Verwaltungskonzepten finden Sie hier.
BLoC Beispiel
In diesem Beispiel wird das Counter-Hello-World Projekt von Flutter mit BLoC ergänzt. Um den Umfang des Posts nicht zu sprengen, konzentrieren wir uns auf die wichtigsten Punkte.
Quelle: Flutter Counter Tutorial
Wie beschrieben werden drei Teile benötigt: Events, der eigentliche BLoC und der State. Als Erstes definieren wir die Events, die durch das Frontend ausgelöst werden können. In diesem Fall das Inkrementieren und Dekrementieren des Zählers. Weitere denkbare Events wären z.B. das Zurücksetzen des Counters.
part of 'counter_bloc.dart';
abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
class CounterDecrementPressed extends CounterEvent {}
Als Zweites wird der State angelegt. In diesem Beispiel ist er denkbar simpel und könnte auch direkt als int definiert werden. Bei entsprechend komplexeren Zuständen zeigt sich die volle Stärke. Der Vollständigkeit halber definieren wir aber den State mit einem count Attribut, welches den aktuellen Zählerstand hält.
part of 'counter_bloc.dart';
class CounterState extends Equatable {
final int count;
const CounterState (this.count);
@override
List<Object> get props => [count];
}
Damit kommen wir jetzt zum Herzstück, dem BLoC selbst:
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<CounterIncrementPressed>((event, emit) => emit(CounterState(state.count + 1)));
on<CounterDecrementPressed>((event, emit) => emit(CounterState(state.count - 1)));
}
}
Auf diese Weise wird ein CounterBloc angelegt, der einen Stream von CounterEvents verarbeitet und den CounterState entsprechend ausgibt (emit). Initiiert wird der Zustand mit 0 und bei jedem CounterIncrementPressed-Event wird der Zustand um 1 erhöht und ausgegeben. Bei CounterDecrementPressed-Events entsprechend gegensätzlich. Nun können in der UI die CounterEvents ausgelöst werden, der State wird im BLoC aktualisiert und das Frontend kann entsprechend auf die Änderung reagieren.
Das sieht auf den ersten Blick nach sehr viel Aufwand für ein so simples Problem aus. BLoC spielt seine Stärken vor allem bei komplexen Zuständen aus. Das Framework bietet aber auch eine simplere Variante genau für solche Fälle namens Cubit.
Cubit Beispiel
Ein Cubit ist ein vereinfachter Bloc der, statt auf Events zu horchen, durch vordefinierte Funktionsaufrufe ausgelöst wird.
Quelle: Flutter BLoC - Cubit
Hier die gleiche Funktionalität des oben beschriebenen Beispiels, nur mit Cubit gelöst:
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
In diesem konkreten Fall ist das CounterIncrementPressed-Event durch die increment-Funktion abstrahiert, CounterDecrementPressed durch decrement und der State ist als einfacher integer abgebildet (der letzte Punkt wäre auch oben möglich gewesen). Diese Einfachheit im Gegensatz zum „vollen“ BLoC erkauft man sich aber durch den Verzicht auf eine Hand voll Features, auf die ich im nächsten Abschnitt kurz eingehen will.
Damit die Oberfläche entsprechend reagieren kann, muss natürlich der BLoC oder Cubit noch in der UI registriert werden. Wer mehr ins Detail gehen will, kann sich die ausführlichen Beispiele in der Dokumentation ansehen.
Wann Cubit, wann BLoC?
In den letzten Abschnitten haben wir gesehen, dass mit dem BLoC-Package effektiv zwei Arten geliefert werden, den Applikationszustand zu manipulieren. Welche Variante wann sinnvoll ist möchte ich hier kurz beleuchten.
Vorteile von Cubit
Der große Vorteil von Cubit ist seine Einfachheit. Es muss lediglich der Zustand und die Funktionen, um diesen zu beeinflussen, definiert werden. Das macht den Programmcode kürzer, verständlicher und damit wartbarer.
Vorteile von BLoC
Sobald es komplexer wird, spielt BLoC seine Stärken aus. Vor allem in zwei Punkten ist es Cubit überlegen: Nachvollziehbarkeit und bei der Transformation von Events.
Nachvollziehbarkeit
BLoC gibt Zugriff auf den kompletten Übergang, inklusive des Events, durch den der Zustandswechsel erfolgt ist. Diese Information kann kritisch sein, um Fehler zu analysieren oder den korrekten Ablauf innerhalb der App zu prüfen. Im Gegensatz dazu gibt Cubit nur den aktuellen und nächsten Zustand aus, ohne die auslösende Funktion zu nennen.
Cubit
Change {
currentState: AuthenticationState.authenticated,
nextState: AuthenticationState.unauthenticated
}
BLoC
Transition {
currentState: AuthenticationState.authenticated,
event: LogoutRequested,
nextState: AuthenticationState.unauthenticated
}
Transformation von Events
Der zweite große Vorteil ist die Möglichkeit, Events zu transformieren. Ein prominentes Beispiel hierfür ist ein Debounce-Timer z.B. für eine Suchanfrage. Dieser bewirkt, dass nicht bei jedem Tastenanschlag im Suchfeld eine Anfrage abgesendet wird, sondern erst nach einer definierten Zeit. Dafür kann am Event-Handling ein eigens definierter EventTransformer registriert werden, der den Stream wie gewünscht manipuliert.
EventTransformer<SearchEvent> debounce<SearchEvent>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
class SearchBloc
extends Bloc<SearchEvent, SearchState> {
SearchBloc() : super(SearchState.initial()) {
on<UpdateSearch>(_mapSearchTermToState,
transformer: debounce(const Duration(milliseconds: 500)));
…
}
}
Wenn die Komplexität initial gering ist und mit der Zeit wächst, kann auch mit Cubit gestartet und nachträglich ohne großen Aufwand zu BLoC ausgebaut werden.
Fazit
Mit Flutter Bloc erhält man ein umfangreiches Package, um den Zustand über die gesamte App hinweg zu steuern, das auch eine Option gestattet, für weniger komplexe Situationen eine einfachere Notation zu nutzen. Darüber hinaus ist die Dokumentation vollständig und verständlich geschrieben und bietet anschauliche Beispielprojekte. Bisher kommt BLoC in all unseren Flutter Projekten zum Einsatz. Man sollte aber nicht vergessen, dass es sich hierbei bei Weitem nicht um die einzige Lösung handelt. Es gibt weitere Optionen für das State Management, welche je nach Team, Vorwissen und Projekt ggf. passender sind.
Links
- Flutter BloC Package: https://pub.dev/packages/flutter_bloc
- Dokumentation: https://bloclibrary.dev/#/gettingstarted
- Liste von State-Management Optionen: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options
Flutter Reihe:
- Teil 1: Mobile App-Entwicklung mit Google Flutter
- Teil 2: Flutter und Dart im Einsatz für Apps – ein Erfahrungsbericht
- Teil 3: Animationen mit Flutter - das exensio Logo als Ladeanimation
- Teil 4: Flutter for Web mittels Google Cloud Bucket veröffentlichen
- Teil 5: Wie erstellt man Packages mit Google Flutter
- Teil 6: Barcodes Scannen mit Google Flutter
- Teil 7: Wie eine Google Flutter App sprechen lernt
- Teil 8: Flutter meets Video
- Teil 9: Flutter und schimmernde Skelette
- Teil 10: Flutter Design Series: Glas Morphismus
- Teil 11: App State-Management mit Flutter BLoC
- Teil 12: Mit Quick Actions Schnellzugriffe in Flutter Apps umsetzen
- Teil 13: Listen in Listen: Flutter Performance Optimierung