Smartphone-Anwendungen mit Flutter erstellen

von | 18.01.2020

Frameworks, die eine gemeinsame Code-Basis für die Entwicklung mobiler Applikationen sowohl für Android als auch für iOS anbieten, findet man heutzutage leicht. Bspw. gibt es React Native von Facebook, Microsoft hat Xamarin im Angebot und auch Google bietet mit Flutter ein entsprechendes Framework. Da Flutter jedoch erst seit Mai 2017 auf dem Markt ist, gibt es noch keine gängigen Architektur-Patterns oder einen Guide to app architecture, wie man ihn etwa aus dem Android-Umfeld kennt.

Gerne möchte ich Ihnen einen Weg vorstellen, der sich meiner Meinung nach gut eignet, um mittlere bis große Smartphone-Anwendungen zu erstellen. Dieser Weg oder besser gesagt dieser Architekturstil nennt sich BLoC.

Was ist eine BloC?

Eine BLoC ist eine Business Logic Component, und davon kann es mitunter mehrere pro Anwendung geben. Die Kernaussage des BloC-Patterns ist, dass alles in der App als Stream von Events abgebildet wird. Beispiel: In Widget 1 wird ein Button gedrückt, in Widget 2 soll sich die UI verändern, also stellt die BLoC in der Mitte einen Stream bereit, den Widget 2 abhören („listen“) und Widget 1 befüllen kann.

Ein großer Vorteil dieser Vorgehensweise ist, dass Dart bereits eine eigene Syntax zum Arbeiten mit Streams bereitstellt, und keine zusätzlichen Plugins benötigt werden, um BLoCs in Flutter zu implementieren.

Die praktische Anwendung mit Flutter-Beispiel

Die meisten Business-Apps müssen in irgendeiner Form immer folgende Aufgaben erfüllen:

  1. Daten von einem Server laden.
  2. Die geladenen Daten verarbeiten.
  3. Die verarbeiteten Daten in der UI anzeigen.

Auch in meinem Beispiel gilt es, diese drei Aufgaben zu erledigen. Meine Anwendung nutzt die API der OpenLigaDb¹, um ein beliebiges Fußballspiel mithilfe eines http-Get-Requests zu laden. Die Daten des Spiels liegen im JSON-Format vor und müssen auf eine passende Datenklasse gemappt werden. Die konvertierten Daten werden anschließend an der Oberfläche angezeigt.

Entsprechend der drei genannten Aufgaben verfügen die Apps meist über drei Architekturschichten. Zu Beginn meiner Arbeit mit Flutter hatte ich jedoch Schwierigkeiten, meine Dateien logisch zu strukturieren. Letztendlich habe ich mich dafür entschieden, das Projekt nach den einzelnen Architekturschichten nämlich der UI, den BloCs und den Services zu schneiden. Zusätzlich verwende ich noch einen „models“-Ordner, in dem alle Datenklassen abgelegt werden.

Ablagestruktur im Flutter-Beispiel

Die Implementierung der Datenschicht

Für den Service erstelle ich eine neue Klasse FootballService in der football_service.dart-Datei. Diese Klasse besteht im Wesentlichen aus der Methode getMatchData(id) mit der Spiel-Id als Parameter. Um einen http-Get-Request abzusetzen, wird das Dart-Package „http“ verwendet, wobei dieses zu Beginn in der pubspec.yaml-Datei bekannt gemacht wird.

//pubspec.yaml

name: fussball_bloc
description: A new Flutter project.
version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  http: ^0.12.0+1

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

Der Request wird auf die URL „https://www.openligadb.de/api/getmatchdata/$id“ abgesetzt und liefert das Fußballspiel mit der angegebenen Id zurück.

// lib/service/football_service.dart

import 'dart:async';

import 'package:http/http.dart';

class FootballService {
  
  Future<String> getMatchData(int id) async {
    final Client client = new Client();
    final response = await client.get("https://www.openligadb.de/api/getmatchdata/$id");
    return response.body;
  }
}

Aufruf des Services

Den FootballService nutzt man nun in der darunterliegenden BLoC-Schicht. Jede BloC muss die abstrakte Klasse bloc erweitern und deren dispose-Methode überschreiben.

// lib/blocs/football_bloc.dart

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:fussball_bloc/models/game.dart';
import 'package:fussball_bloc/services/football_service.dart';

abstract class Bloc{
  void dispose();
}

class FootballBloc implements Bloc {

  final _gameController = StreamController<Game>.broadcast();
  Stream<Game> get gameStream => _gameController.stream;

  final _footballService = new FootballService();

  void getMatch() {
    int id = _getRandomNumber();
    _footballService.getMatchData(id).then((response){
      Game game = _mapResponseToGame(response);
      _gameController.sink.add(game);
    });
  }

  int _getRandomNumber() {
    var random = new Random();
    return random.nextInt(50000) + 1; 
  }

  Game _mapResponseToGame(String response) {
    var decodedResponse = json.decode(response);
    Game match = new Game();
    match.matchDay = DateTime.parse(decodedResponse["MatchDateTime"]);
    match.homeTeam = decodedResponse["Team1"]["TeamName"];
    match.awayTeam = decodedResponse["Team2"]["TeamName"];
    return match;
  } 

  @override
  void dispose() {
    _gameController.close();
  }
}

Die FootballBloc hat eine öffentliche Methode getMatch(), die von der UI aufgerufen werden kann, sowie zwei private Methoden, die für das Erstellen einer zufälligen Zahl und das Mapping der http-Response auf ein Game-Objekt zuständig sind. Das Game-Datenobjekt sieht wie folgt aus:

// lib/models/game.dart

class Game {
  DateTime matchDay;
  String homeTeam;
  String awayTeam;
}

Das vielleicht Wichtigste an FootballBloc ist der gameStream, den sie als Getter öffentlich bereitstellt. Auf diesen kann sich nämlich im nächsten Schritt die UI registrieren, um ein neu geladenes Spiel anzuzeigen.

Die Verwaltung der Abhängigkeiten

Jetzt stellt sich natürlich die Frage, wie man die BloC der UI bekannt macht? Für dieses einfache Beispiel wäre es möglich, eine Instanz der FootballBlocs in der UI zu erstellen, und darüber die Aufrufe abzuwickeln. Sobald das Projekt jedoch minimal komplexer wird, und man dieselbe BloC in mehreren Widgets benutzen möchte, ist dieses Vorgehen nicht mehr adäquat. Deshalb nutzt man vorzugsweise Provider-Widgets, um seine einzelnen BLoCs der ganzen Anwendung zur Verfügung zu stellen.

Im Ordner blocs sollte dazu ein weiterer Ordner essentials angelegt werden. Darin befinden sich die bloc_provider.dart- und die app_bloc.dart-Datei.

// lib/blocs/app_bloc.dart

import 'package:fussball_bloc/blocs/football_bloc.dart';

class AppBloc {
  FootballBloc _footballBloc;

  AppBloc() {
    _footballBloc = new FootballBloc(); 
  }
  FootballBloc get footballBloc => _footballBloc;
}

Die AppBloc ist dabei eine Klasse, die jede BloC einmalig instanziiert und über einen Getter zugänglich macht. In AppBloc könnten bei Wunsch auch die BloCs untereinander kommunizieren. Bei einer App mit Authorisierungsfunktion kann dies beispielsweise hilfreich sein. Hier im Beispiel wird die AppBloc als Konstruktor-Argument in das BlocProvider Widget übergeben. Der Ansatz, dies über eine AppBloc zu realisieren, die alle BLoCs enthält, muss aber nicht unbedingt für jeden Anwendungsfall der Beste sein.

// lib/blocs/bloc_provider.dart

import 'package:flutter/material.dart';

import 'app_bloc.dart';

class BlocProvider extends InheritedWidget {
  final AppBloc bloc;

  BlocProvider({Key key, this.bloc, child}) : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => true;

  static AppBloc of(BuildContext context) =>
      (context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bloc;
}

Der BlocProvider ist übrigens ein Widget. Über die of()-Methode wird anderen Kind-Widgets erlaubt, sich über den BlocProvider alle BloCs der AppBlocs zu holen. Damit dies funktioniert, muss das BlocProvider-Widget in der main.dart-Datei um das MaterialApp-Widget herum gewrappt werden.

// main.dart

import 'package:flutter/material.dart';
import 'package:fussball_bloc/blocs/essentials/bloc_provider.dart';
import 'package:fussball_bloc/ui/football_screen.dart';

import 'blocs/essentials/app_bloc.dart';

void main() {
  final appBloc = AppBloc();
  runApp(FootballApp(appBloc));
}

class FootballApp extends StatelessWidget {
  final AppBloc bloc;
  FootballApp(this.bloc);
  
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      bloc: bloc,
      child: MaterialApp(
        home: FootballScreen(),
      ),
    );
  }

}

Nach diesem kleinen, aber sinnvollen Umweg können wir nun zum Erstellen der UI übergehen. Diese besteht aus einem Column-Widget, das untereinander den Text des aktuell geladenen Spiels und einen Button anzeigt.

// lib/ui/football_screen.dart

import 'package:flutter/material.dart';
import 'package:fussball_bloc/blocs/essentials/bloc_provider.dart';
import 'package:fussball_bloc/blocs/football_bloc.dart';
import 'package:fussball_bloc/models/game.dart';

class _FootballScreenState extends State<FootballScreen> {
  Game game;
  @override
  Widget build(BuildContext context) {
    final FootballBloc footballBloc = BlocProvider.of(context).footballBloc;
    footballBloc.gameStream.listen((Game data) {
      setState(() {
        game = data;
      });
    });
    return Scaffold(
        appBar: AppBar(
          title: Text("Football-App"),
        ),
        body: Card(
          margin: EdgeInsets.symmetric(horizontal: 20, vertical: 20),
          elevation: 10,
            child: Column(
              mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            SizedBox(height: 40,),
            Center(
              child: Container(
                margin: EdgeInsets.symmetric(horizontal: 10),
                child:  game != null ? Text("Am ${game.matchDay.day}.${game.matchDay.month}.${game.matchDay.year} hat ${game.homeTeam} gegen ${game.awayTeam} gespielt") : Text("Button drücken um das erste Spiel zu laden"),
              )
            ),
            SizedBox(height: 10,),
            RaisedButton(
              child: Text("neues Spiel"),
              onPressed: () {
                footballBloc.getMatch();
              },
            ),
            SizedBox(height: 40,),
          ],
        )));
  }
}

class FootballScreen extends StatefulWidget {
  @override
  _FootballScreenState createState() => _FootballScreenState();
}

Besonders bemerkenswert sind hier die Interaktionen mit der FootballBloc, auf deren Stream man sich registriert (Zeilen 11 bis 15), und die onPressed-Methode des Buttons (Zeilen 26 bis 28).

Und so sieht das Ergebnis aus: 

Flutter Beispiel

Meine Eindrücke von Flutter

Mir macht das Arbeiten mit Flutter Spaß. Aus meiner Sicht handelt es sich um ein sehr nützliches Framework. Besonders beeindruckt mich, dass Flutter im Gegensatz zu React Native oder Xamarin nicht die nativen UI-Komponenten von Android und iOS verwendet, sondern den Screen-Inhalt mit einer 2D-Render-Engine komplett selbst zeichnet und die Flutter-Apps dennoch wie native Apps wirken. Natürlich fehlen wie gesagt die Architektur-Patterns und ein Guide zur App-Architektur, und auch die Nutzung der verschiedenen Widgets erfordert zu Beginn etwas Übung, aber sobald man den Dreh raus hat, erleichtert es die Implementierung signifikant. Hätte man die Aufgabenstellung auch anders lösen können? Vermutlich. Wie wären Sie denn vorgegangen? Über einen Austausch würde ich mich freuen.

 

Hinweise:

Interessieren Sie sich für weitere Tipps aus der Praxis? Testen Sie unseren wöchentlichen Newsletter mit interessanten Beiträgen, Downloads, Empfehlungen und aktuellem Wissen.

[1] API der OpenLigaDB: https://www.openligadb.de/
Vor allem der Quellcode und die Schichtarchitektur ist inspiriert von https://www.raywenderlich.com/4074597-getting-started-with-the-bloc-pattern und David Anaya.

Mark Heimer hat zwei weitere Beiträge im t2informatik Blog veröffentlicht:

t2informatik Blog: App-Entwicklung mit NativeScript

App-Entwicklung mit NativeScript

t2informatik Blog: Tutorial: Setup Single-SPA mit Angular und React

Tutorial: Setup Single-SPA mit Angular und React

Mark Heimer
Mark Heimer

Mark Heimer ist bei t2informatik als Junior-Entwickler tätig. Zuvor hat er Informatik an der Hochschule für Wirtschaft und Recht in Berlin studiert. Über einige seiner Erfahrungen schreibt er hier im Blog.