Mason – Wenn schon Boilerplate, dann richtig!

Lesezeit: 7 Min, veröffentlicht am 22.02.2024
Mason – Wenn schon Boilerplate, dann richtig!

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?

State-Management und Boilerplate

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:

  • Er trägt zur verbesserten Lesbarkeit von Projekten bei.
  • Er hilft dabei, Fehler leichter zu identifizieren, den Code zu entkoppeln und wartungsfreundlicher zu gestalten.
  • Das erleichtert wiederum das Testen.

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.

Wie löst Mason das Boilerplate-Problem?

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.

mason question

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.

Fazit

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.

Tags

Verfasst von:

Foto von Tom

Tom

Tom ist Mobile Engineer bei cosee und begeistert sich vor allem für Cross-Plattform-Technologien wie Flutter.

Foto von Quoc

Quoc

Quoc ist Mobile-Entwickler bei cosee, verirrt sich aber immer mal wieder ins Backend.