Create smartphone applications with Flutter

by | 18.01.2020

Frameworks that provide a common code base for mobile application development for both Android and iOS are easy to find today. For example, there is React Native from Facebook, Microsoft offers Xamarin and Google offers Flutter, a corresponding framework. But since Flutter has only been on the market since May 2017, there are no common architecture patterns or a guide to app architecture as known from the Android environment.

I would like to introduce you to a way that I think is well suited for creating medium to large smartphone applications. This way or rather this architectural style is called BLoC.

What is a BloC?

A BLoC is a Business Logic Component, and there may be several of them per application. The core message of the BloC pattern is that everything in the app is mapped as a stream of events. Example: In widget 1 a button is pressed, in widget 2 the UI is supposed to change, so BLoC provides a stream in the middle, which widget 2 can listen to and widget 1 can fill.

A big advantage of this approach is that Dart already provides its own syntax for working with streams, and no additional plugins are needed to implement BLoCs in Flutter.

A real-life implementation with Flutter example

Most business apps must always perform the following tasks in some form or another:

  1. Load data from a server.
  2. Process the loaded data.
  3. Display the processed data in the UI.

In my example, these three tasks also have to be performed. My application uses the OpenLigaDb¹ API to load any soccer game using an http get request. The game data is in JSON format and has to be mapped to a suitable data class. The converted data is then displayed on the interface.

In accordance with the three tasks mentioned, the apps usually have three architecture layers. At the beginning of my work with Flutter I had difficulties to structure my files logically. In the end I decided to cut the project according to the individual architecture layers namely the UI, the BloCs and the services. Additionally I use a “models” folder where all data classes are stored.

Filing structure in the Flutter example

The implementation of the data layer

For the service I create a new class FootballService in the football_service.dart file. This class basically consists of the method getMatchData(id) with the match id as parameter. To send a http-get-request, the dart package “http” is used, which is announced in the pubspec.yaml file at the beginning.

//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

The request is sent to the URL “https://www.openligadb.de/api/getmatchdata/$id” and returns the soccer game with the specified id.

// 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;
  }
}

Calling the service

The FootballService is now used in the underlying BLoC layer. Every BloC must extend the abstract class bloc and overwrite its dispose method.

// 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();
  }
}

The FootballBloc has a public method getMatch() that can be called from the UI, and two private methods that are responsible for creating a random number and mapping the http response to a game object. The game data object looks like this:

// lib/models/game.dart

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

Perhaps the most important thing about FootballBloc is the gameStream, which it makes publicly available as a getter. In the next step, the UI can register on this stream to display a newly loaded game.

The management of dependencies

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.

Now the question arises, of course, how to make the BloC known to the UI? For this simple example, it would be possible to create an instance of the Footballbloc in the UI and use it to handle the calls. But as soon as the project becomes minimally more complex, and you want to use the same BloC in several widgets, this approach is no longer adequate. Therefore, it is preferable to use provider widgets to make your individual BLoCs available to the entire application.

To do this, you should create another folder called essentials in the blocs folder. This folder contains the bloc_provider.dart and the app_bloc.dart file.

// 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;
}

The AppBloc is a class that instantiates each BloC once and makes it accessible via a getter. In AppBloc the BloCs could communicate with each other if desired. For example, this can be helpful for an app with an authorisation feature. In this example the AppBloc is passed as a constructor argument to the BlocProvider widget. The approach to implement this using an AppBloc that contains all BLoCs does not necessarily have to be the best for every application.

// 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;
}

The BlocProvider is a widget by the way. The of() method allows other child widgets to get all BloCs of the AppBlocs via the BlocProvider. For this to work, the BlocProvider widget in the main.dart file must be wrapped around the MaterialApp widget.

// 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(),
      ),
    );
  }

}

After this small but useful detour, we can now move on to creating the UI. This consists of a column widget that displays the text of the currently loaded game and a button below it.

// 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();
}

Especially noteworthy are the interactions with the FootballBloc on whose stream you are registered (lines 11 to 15), and the onPressed method of the button (lines 26 to 28).

And this is what the result looks like:

Flutter Example

My impressions of Flutter

I enjoy working with Flutter. From my point of view it is a very useful framework. I am especially impressed by the fact that Flutter, unlike React Native or Xamarin, does not use the native UI components of Android and iOS, but draws the screen content completely by itself with a 2D rendering engine, and yet the Flutter apps look like native apps. Of course, as said before, the architecture patterns and a guide to the app architecture are missing, and using the various widgets also requires some practice at the beginning, but once you get the hang of it, it makes implementation significantly easier. Could the task have been solved differently? Probably. How would you have dealt with it? I’m interested in your opinion and would be happy about an exchange.

 

Notes:

[1] API of OpenLigaDB: https://www.openligadb.de/

Mark Heimer has published two more posts on the t2informatik Blog:

t2informatik Blog: App development with NativeScript

App development with NativeScript

t2informatik Blog: Tutorial: Setup Single-SPA with Angular and React

Tutorial: Setup Single-SPA with Angular and React

Mark Heimer
Mark Heimer

Mark Heimer works at t2informatik as a junior developer. He previously studied computer science at the Berlin School of Economics and Law. He writes about some of his experiences here in the blog.