Einleitung
Früher oder später, wenn eine Flutter App eine bestimmte Größe erreicht, denkt man darüber nach, wie und wo man die ganze Logik programmiert. Flutter bietet von Haus aus schon einige Lösungen dafür. Da wäre beispielsweise der Standart StatefulWidget mit seiner setState Methode oder ein InheritedWidget um Daten in verschiedenen Teilen der App zu verwenden.
Da ich jedoch Fan davon bin die Logik von der UI zu trennen, nutze ich in meinen meisten Apps Flutter Bloc. Ich hatte etwas zu kämpfen als ich mit Flutter Bloc angefangen habe und weiß wie kompliziert Flutter Bloc sein kann. Daher schauen wir uns erst einmal an wie Flutter Bloc funktioniert.
Falls du dich alternativ für Flutter Riverpod interessierst, dann schau dir gerne meinen ausführlichen Blog Eintrag dazu an. Außerdem gibt es auch ein Vergleich von Bloc und Riverpod. Schau auch da gerne vorbei.
Der Artikel ist zusammen mit meinem Youtube Video endstannden. Falls dir Videos besser beim lernen helfen, dann schau doch gerne mal vorbei.
Wie funtkioniert Bloc?
Flutter Bloc trennt Logik und UI durch einen Controller (Bloc) und Zustände (State). Controller verwalten Zustandsänderungen basierend auf Ereignissen (Events), die von der UI ausgelöst werden.
Wer schonmal auf der offiziellen Webseite war, der hat bestimmt schonmal diese Abbildung gesehen:
Die Abbildung zeigt die Architektur einer App, die mit dem Flutter Bloc-Achitktur erstellt wurde. Es gibt drei Bereiche: die Benutzeroberfläche (UI), Bloc und die Datenquellen.
Heute konzentrieren wir uns jedoch nur auf die ersten beiden Bereiche, die UI und Bloc. Die UI ist der sichtbare Teil der App, die Screens und Interaktionen enthält. Was jetzt aber dazu kommt sind die drei Parts die zu Bloc gehören. Die Events, die States und der Bloc-Controller.
Event
Die Events sind Auslöser in der App. Das kann sowohl ein Button sein, der gedrückt wird als auch ein Timer, der alle 10 Sekunden ein Event auslöst, oder man hat bis zum Ende einer Liste gescrollt. Alles was mit der Interaktion in der App zu tun hat ist ein Event.
State
Dann gibt es noch States, die den aktuellen Status der App bestimmen. Zum Beispiel, wenn die App im Hintergrund etwas verarbeitet oder herunterlädt, hat sie den Status “Loading”. Wenn die App den Prozess abgeschlossen hat, ändert sich dieser Status zu “Success”.
Wenn jedoch ein Fehler auftritt, ändert sich der Status zu “Error”. Auf diese Weise kann der aktuelle Status der App jederzeit ermittelt werden, um dem Nutzer zu signalisieren, was gerade in der App passiert.
Controller
Der Controller bestimmt, was bei einem Event passieren soll und welchen Status die App haben soll. Er ist auch dafür verantwortlich, HTTP-Anfragen zu stellen und Daten weiterzuverarbeiten. In diesem Bereich wird die eigentliche Logik der App programmiert.
Das Bloc Prinzip
Betrachten wir nun die verschiedenen Parts, funktioniert Flutter Bloc immer nach dem gleichen Prinzip. Wenn in der App ein Button gedrückt wird, wird ein Event ausgelöst. Dieses Event wird vom Controller erkannt und weiterverarbeitet.
Der Controller setzt den Status der App auf “Loading”, wodurch in der App eine Ladebildschrim angezeigt werden kann. Anschließend wird vom Controller eine HTTP-Anfrage gestellt. Nachdem die Anfrage erfolgreich war, setzt der Controller den Status auf “Success” und übergibt die Daten an die UI und zeigt eine Bestätigungsnachricht an.
Schauen wir uns das einmal in einem Beispiel an.
Beispiel für Flutter Bloc
Hier gibt es den Quellcode zum ausprobieren.
In diesem Beispiel soll nach drücken des Buttons eine Liste von Cocktails geladen werden.
Da Flutter Bloc nicht offiziell zu Flutter gehört müssen wir erstmal das Package mit einbinden. das machen wir mit folgenden Befehl.
flutter pub add flutter_bloc
Zunächst erstellen wir einen Ordner für unsere gesamten Bloc-Dateien und einen weiteren Ordner für unseren Home-Bloc. Es wird übrigens empfohlen mindestens für jeden Screen in der App einen Bloc zu erstellen. Das Flutter Bloc-Plugin für Android Studio erweist sich hierbei als sehr hilfreich. Es generiert automatisch alle drei Dateien, die wir benötigen: eine für Events, eine für die States und eine für den Controller.
Beginnen wir mit den Events und States. Für unseren Button benötigen wir nur ein Event. Bei den States definieren wir jeweils einen für “Loading”, “Success” und “Failure”. Im Success-Status implementieren wir zusätzlich noch eine Variable für die Cocktail Daten, die in der UI angezeigt werden.
// home_event.dart
part of 'home_bloc.dart';
@immutable
abstract class HomeEvent {}
class HomeFetchedCocktails extends HomeEvent {}
// home_state.dart
part of 'home_bloc.dart';
@immutable
abstract class HomeState {}
class HomeInitial extends HomeState {}
class HomeInProgressState extends HomeState {}
class HomeSuccessState extends HomeState {
final List cocktails;
HomeSuccessState(this.cocktails);
}
class HomeFailureState extends HomeState {}
Der Controller der alles verwaltet
Fehlt nur noch der Controller. Hier bestimmen wir als erstes welches event erkannt werden soll. Dann setzen wir unsere states.
Einen am Anfang, der später unseren Ladescreen anzeigt, einen falls ein Fehler auftritt und einen nach beenden des http requests. Beim Success übergeben wir auch gleich die Cocktails.
// home_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
part 'home_event.dart';
part 'home_state.dart';
class HomeBloc extends Bloc {
HomeBloc() : super(HomeInitial()) {
on((event, emit) async {
if (event is HomeFetchedCocktails) {
// Status auf 'loading' setzen
emit(HomeInProgressState());
try {
await Future.delayed(const Duration(seconds: 3));
List cocktails = exampleCocktails;
// status auf 'success' setzen und Daten übergeben
emit(HomeSuccessState(cocktails));
} catch (error) {
// status auf 'error' setzen
emit(HomeFailureState());
}
}
});
}
}
Zur Vereinfachung ist der HTTP-Request in diesem Beispiel nur eine konstante mit 3 Sekunden Verzögerung. Aber man kann sich an dieser Stelle eine Kommunikation mit einer API vorstellen.
Die Logik steht. Jetzt schauen wir uns an was in der UI noch gemacht werden muss.
Dazu gehen wir erstmal zurück zu unseren Home Screen und erstellen uns eine HomeBloc Variable. Diese wird in der initState-Funktion initialisieren. Gleichzeitig müssen wir den Bloc-Stream auch schließen, wenn wir ihn nicht mehr benötigen.
Dazu fügen wir in die dispose-Funktion einen Aufruf von homeBloc.close() hinzu.
late HomeBloc _homeBloc;
@override
void initState() {
_homeBloc = HomeBloc();
super.initState();
}
@override
void dispose() {
_homeBloc.close();
super.dispose();
}
Für unsere Liste benötigen wir jetzt einen BlocBuilder. Dieses speziell für das Bloc-Muster ausgelegte Widget erkennt Änderungen im State und kann darauf reagieren. Im Beispiel sehen wir, dass wenn der Status auf “inProgress” gestellt ist, dann wird ein CircularProgressIndicator angezeigt.
Bei einem “success”-Status wird dann die Liste der Cocktails dargestellt. Beachte hierbei, dass die Cocktails als Variable im State-Objekt gespeichert sind.
BlocBuilder(
bloc: _homeBloc,
builder: (context, state) {
if (state is HomeInProgressState) {
return const Expanded(
child: Center(child: CircularProgressIndicator())
);
}
if (state is HomeSuccessState) {
return Column(
children: List.generate(
state.cocktails.length,
(index) {
CocktailModel cocktail = state.cocktails[index];
return ListTile(
title: Text(cocktail.strDrink.toString()),
subtitle: Text(cocktail.strCategory.toString()),
);
},
),
);
}
return Container();
},
),
Somit haben will alles programmiert und unsere Liste funktioniert jetzt nach dem Flutter Bloc Prinzip.
Bloc Widgets
Neben BlocBuilder gibt es noch einige weiter hilfreiche Bloc Widgets. Diese Widgets helfen dir in deiner Flutter App mit dem Blocs zu interagieren.
Im Folgenden findest du eine detaillierte Beschreibung und Codebeispiele für jedes dieser Widgets.
Wie funktioniert ein BlocBuilder Widget?
Der BlocBuilder ist ein Widget, das Änderungen im Bloc State erkennt und darauf reagiert. Es baut die UI basierend auf dem aktuellen Zustand (State) des Bloc. Der buildWhen Parameter kann verwendet werden, um zu steuern, wann das UI neu aufgebaut wird. Dabei können die Zustände für einen Vergleich verwendet werden.
BlocBuilder(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
if (state is HomeInProgressState) {
return CircularProgressIndicator();
}
if (state is HomeSuccessState) {
return Text('Laden erfolgreich: ${state.cocktails.length} Cocktails');
}
return Text('Willkommen!');
},
)
Wie funktioniert ein BlocProvider Widget?
BlocProvider stellt eine Bloc-Instanz für den gesamten Widget-Baum zur Verfügung. Es erleichtert den Zugriff auf den Bloc in untergeordneten Widgets.
BlocProvider(
create: (context) => HomeBloc(),
child: HomeScreen(),
)
Wie funktioniert ein BlocListener Widget?
Der BlocListener reagiert auf Zustandsänderungen im Bloc, ohne die UI neu zu rendern. Er eignet sich für Aktionen wie das Anzeigen von Snackbars oder das Navigieren. Der listenWhen Parameter kann verwendet werden, um zu steuern, wann die Listener Funktion aufgerufen wird.
BlocListener(
listenWhen: (previous, current) => current is HomeFailureState,
listener: (context, state) {
if (state is HomeFailureState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Laden fehlgeschlagen')),
);
}
},
child: HomeScreen(),
)
Wie funktioniert ein BlocConsumer Widget?
BlocConsumer kombiniert die Funktionalität von BlocBuilder und BlocListener. Es bietet sowohl die Möglichkeit, auf Zustandsänderungen zu reagieren, als auch die UI basierend auf dem Zustand zu aktualisieren. Die buildWhen und listenWhen Parameter können verwendet werden, um zu steuern, wann der Builder oder Listener aktiv wird.
BlocConsumer(
listenWhen: (previous, current) => current is HomeSuccessState,
listener: (context, state) {
if (state is HomeSuccessState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Daten erfolgreich geladen')),
);
}
},
buildWhen: (previous, current) => previous != current
builder: (context, state) {
if (state is HomeInProgressState) {
return CircularProgressIndicator();
} else if (state is HomeSuccessState) {
return ListView.builder(
itemCount: state.cocktails.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(state.cocktails[index].strDrink),
);
},
);
} else {
return Text('Bitte drücke den Button zum Laden.');
}
},
)
Vorteile
- Trennung von Logik und UI
- gerade bei größeren Projekten geordneter Code
- leichtere Implementierung von zusätzlichen Features
Nachteile
- Viel Code auch bei wenig Logik
- Kompliziertere Tests
- Einarbeitung nicht einfach und für einige etwas komplex am Anfang.
Abschluss
Das war mein kleiner Einblick in die Welt von Flutter Bloc. Ich hoffe ihr habt was gelernt und konntet was mitnehmen. Schreibt mir gern in die Kommentare falls es noch Fragen dazu gibt oder falls ihr andere Themen habt, die ich mal durchleuchten soll. Ich bin noch dabei ein Video zu den Thema zu schneiden also bleibt gespannt.
Wer das alles lieber als Video Format anschauen will, der kann gerne auf meinen Youtube Kanal schauen. Zu Bloc hab ich da ein kurzes 5 min Video hochgeladen.