Fragt man eine Entwicklerin oder einen Entwickler, ob sie in verschiedenen Projekten oder innerhalb eines Projektes regelmäßig ähnliche Aufgaben erledigen müssen – wie beispielsweise das Erstellen bestimmter Klassen oder Methoden – erfolgt ganz gewiss ein klares „Ja“.
Diese Tatsache trifft selbstverständlich auch auf Mobile-Projekte zu, die mit Flutter/Dart entwickelt werden. Nähere Infos zum Flutter-Framework gibt es hier:
Cross-Compiled-Entwicklung mit Flutter.
Zwar bietet Flutter durch seine Widgets, gerade im UI-Bereich, die Möglichkeit, Boilerplate-Code zu vermeiden und weite Teile wiederverwendbar zu machen. Doch gerade in den Bereichen solcher Projekte, in denen viel Business-Logic mit ständigen State-Änderungen zu finden ist, ist es oft unvermeidbar Boilerplate-Code zu produzieren. Mit Mason ist es möglich, Templates für ähnlich-bleibende Segmente zu erstellen und daraus Code-Blöcke bis hin zu ganzen Projekt-Strukturen zu generieren. Doch lohnt sich der zusätzliche Konfigurationsaufwand?
Für unsere Flutter-Projekte benutzen wir standardmäßig das Business Logic Component-Pattern (BLoC-Pattern), um das State-Management zu regeln. Kurz umrissen gibt es beim BLoC-Pattern BLoCs, Events und States. Die UI ruft ein Event auf (z.B. wenn ein Button gedrückt wurde) und sendet dieses an den BLoC. Dieser verarbeitet das Event und aktualisiert dementsprechend seinen State. Viele UI-Elemente aktualisieren sich, verändern sich oder erscheinen überhaupt erst, abhängig vom State. Wer sich tiefer mit dem BLoC-Pattern auseinandersetzen will, dem legen wir den TechTalk von Tamara, Quoc und Tobias ans Herz.
Grundsätzlich empfehlen wir für jeden Use-Case einen eigenen BLoC zu erstellen, denn:
Im Folgenden zeigen wir einen minimalen Aufbau, der die Funktionalität eines Authentifizierungsvorgangs demonstrieren soll. Stellen wir uns vor wir haben einen Login-Screen und der Nutzer hat bereits seine Zugangsdaten eingegeben. Nun drückt er auf den Login-Button. Dadurch wird von der UI ein Login-Event an den BLoC gesendet.
Im Bild sehen wir Klassen für LoginEvent und LogoutEvent, welche ein AuthEvent darstellen.
sealed class AuthEvent {
const AuthEvent();
}
final class AuthLoginEvent extends AuthEvent {
const AuthLoginEvent(this.username, this.password);
final String username;
final String password;
}
final class AuthLogoutEvent extends AuthEvent {
const AuthLogoutEvent();
}
In der UI gibt es zwei Zustände. Entweder der Nutzer ist eingeloggt oder ausgeloggt. Abhängig davon wird ihm im ausgeloggten Zustand der Login-Screen angezeigt. Ist er eingeloggt, wird ihm der App-Inhalt gezeigt.
sealed class AuthState {
const AuthState();
}
final class AuthLoggedIn extends AuthEvent {
const AuthLoggedIn();
}
final class AuthLoggedOut extends AuthEvent {
const AuthLoggedOut();
}
Die BLoC-Klasse empfängt das jeweilige Event und ruft dementsprechend die Funktion im Repository o. Ä. auf. Abhängig vom Ergebnis dieses Aufrufes wird der jeweilige State zur UI ausgegeben.
final class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(const AuthLoggedOut()) {
on<AuthLoginEvent>(_onAuthLoginEvent);
on<AuthLogoutEvent>(_onAuthLogoutEvent);
}
void _onAuthLoginEvent(
AuthLoginEvent event,
Emitter<AuthState> emit,
)
{
// Call repository.
}
void _onAuthLogoutEvent(
AuthLogoutEvent event,
Emitter<AuthState> emit,
)
{
// Call repository.
}
}
Auch in sehr unterschiedlichen Projekten, in denen verschiedene Arten der Authentifizierung implementiert werden sollen, zeigen die BLoCs, die den Vorgang auf der App-Ebene verwalten, strukturelle Ähnlichkeiten. Die spezifischen Details für jedes Projekt sind im Repository enthalten.
Um sich die Arbeit zu erleichtern und sich nicht mit dem wiederholten Schreiben dieser drei Klassen zwischen Projekten aufzuhalten, hält Mason ein Template parat: Im Folgenden zeigen wir euch so ein Mason Template, der einen minimalen BLoC generiert – sogenannte Bricks. Den vollständigen Code findet ihr auf GitHub. Schauen wir uns dafür den Inhalt der brick.yaml-Datei an:
name: bloc
description: Generates a new Bloc in Dart. Built for the bloc state management library.
version: 0.1.0
environment:
mason: ">=0.1.0-dev.49 <0.1.0"
vars:
name:
type: string
description: The name of the BLoC class.
default: Test
prompt: What is the BLoC name?
In vars (siehe Abbildung) kann man, wie der Name schon verrät, Variablen definieren, die dem Nutzer abgefragt werden. Diese werden verwendet um die Platzhalter in Mustache-Syntax zu ersetzen. Zusätzlich zu der Ersetzung können wir Modifier hinzufügen, die den Inhalt der Variablen entsprechend verändert. Beispielsweise kann pascalCase() dazu verwendet werden den Inhalt der Variable in PascalCase darzustellen.
Folgend ist ein minimales Mason Template für einen BLoC zu sehen:
final class {{ name.pascalCase() }}Bloc extends Bloc<{{ name.pascalCase() }}Event, {{ name.pascalCase() }}State> {
{{ name.pascalCase() }}Bloc() : super(const {{ name.pascalCase() }}LoadInProgress()) {
on<{{name.pascalCase()}}InitializeEvent>(_on{{ name.pascalCase() }}InitializeEvent);
}
void _on{{ name.pascalCase() }}InitializeEvent(
{{ name.pascalCase() }}InitializeEvent event,
Emitter<{{ name.pascalCase() }}State> emit,
) {}
}
Bloc
sealed class {{ name.pascalCase() }}Event {
const {{ name.pascalCase() }}Event();
}
final class {{ name.pascalCase() }}InitializeEvent extends {{ name.pascalCase() }}Event {
const {{ name.pascalCase() }}InitializeEvent();
}
Event
sealed class {{ name.pascalCase() }}State {
const {{ name.pascalCase() }}State();
}
final class {{ name.pascalCase() }}LoadInProgress extends {{ name.pascalCase() }}State {
const {{ name.pascalCase() }}LoadInProgress();
}
final class {{ name.pascalCase() }}LoadOnSuccess extends {{ name.pascalCase() }}State {
const {{ name.pascalCase() }}LoadOnSuccess();
}
State
Nachdem wir nun den Brick erstellt haben, müssen wir diesen noch in Mason registrieren. Hierzu müssen wir eine mason.yaml erstellen mit folgendem Inhalt:
bricks:
bloc:
path: bricks/bloc
Anschließend müsst ihr diesen Mason-Befehl ausführen:
mason add bloc –path bricks/bloc
Nun haben wir unser erstes Mason Template erstellt. Ihr könnt den Brick mit folgendem Befehl ausführen:
mason make bloc
Dabei wird Mason euch alle Variablen bzgl. des Bricks abfragen, die wir in der brick.yaml definiert haben.
Wenn ihr alle Fragen beantwortet habt, wird Mason euch diese drei Dateien basierend auf unserem Template (Brick) generieren. Diese können wir als Grundlage für den zu Beginn gezeigten BLoC verwenden.
final class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(const AuthLoadInProgress()) {
on<AuthInitializeEvent>(_onAuthInitializeEvent);
}
void _onAuthInitializeEvent(
AuthInitializeEvent event,
Emitter<AuthState> emit,
)
{}
}
Bloc
sealed class AuthEvent {
const AuthEvent();
}
final class AuthInitializeEvent extends AuthEvent {
const AuthInitializeEvent();
}
Event
sealed class AuthState {
const AuthState();
}
final class AuthLoadInProgress extends AuthState {
const AuthLoadInProgress();
}
final class AuthLoadOnSuccess extends AuthState {
const AuthLoadOnSuccess();
}
State
Falls ihr euch wundert, warum die States jetzt plötzlich „Initial“, „LoadInProgress“ und „LoadOnSuccess“ heißen: Sobald irgendeine Form der Kommunikation mit einem Backend stattfindet, ist das Ergebnis in der Regel nicht direkt da. Dieser Zustand muss auch abgefangen und in der UI berücksichtigt werden (bspw. mit einem Loading bzw. Progress Indicator). In unserem vereinfachten Authentifizierungs-Beispiel wäre der Initial State bspw. AuthLoggedOut. LoadInProgress wäre die Zeit, in dem die Anmelde-Daten im Backend geprüft werden (im vereinfachten Beispiel nicht berücksichtigt) und LoadOnSuccess wäre AuthLoggedIn.
Wäre der Authentifizierungs-Vorgang fehlgeschlagen (z.B. wegen falscher Anmelde-Daten), könnte man wieder im AuthLoggedOut-State landen, oder man erstellt einen ganz eigenen State dafür, um passende Fehlermeldungen in der UI anzeigen zu können.
Wie bereits erwähnt, ist die Anwendung von Mason nicht auf BLoCs und Authentifizierungs-Vorgänge beschränkt. In jedem Teil eines Projekts, in dem sich ähnliche Muster wiederfinden, ist die Anwendung denkbar. Die Erstellung eines Bricks ist zeitlich nicht besonders aufwendig. Somit ist Mason ein hilfreiches Mittel, um sich auf die wesentlichen Aufgaben eines Entwicklers zu konzentrieren, nämlich das Lösen von Problemen. Zusätzlich müssen bei einem existierenden Template keine zusätzlichen Personen-Tage für das Setup eines Projektes aufgewendet werden. Dadurch ist Mason sowohl für den Entwickler als auch für den Kunden ein Gewinn. Wir sind in diesem Artikel lediglich auf die Grundlagen von Mason eingegangen. Darüber hinaus ist es bspw. mit sogenannten „Hooks“ möglich, Skripts vor oder auch nach der Erstellung der jeweiligen Klassen ausführen zu lassen. Wer sich weiter in die Welt von Mason einlesen möchte, dem legen wir die Dokumentation des Entwicklers Felix Angelov ans Herz.