codete building with blocs sharing the code main 2c18029b40
Codete Blog

Building with BLoCs, Sharing the Code

avatar male f667854eaa

19/07/2018 |

9 min read

Łukasz Huculak

In my previous post you could gain some knowledge about Flutter and how you can build client applications for both Android and iOS devices at the same time. Today, we will create single-room chat client apps for iOS, Android and web browsers with a shared code in the form of BLoCs. The whole application will be developed in Dart using Flutter as a mobile framework and React for the web with Firebase Cloud Firestore as backend.

 

Project structure

Generally speaking, every client application has some UI rendering code, some rules that steer the UI behind that and bridge code to external services or data sources. UI rendering is specific to the platform and bridge services are often platform-specific too. But you can try to share business logic if you want to bring the same features both to a mobile app and a web app. The following diagram shows (in green) what parts of application code can be shared across all clients.

codete building with blocs sharing the code graph1 de58e37e34

 

BLoC in a nutshell

Business Logic Components (or BLoCs) contain all the business logic of the client application leaving the UI to to just simply render its outputs and give simple input data to them. BLoC should be designed according to the following rules:

  • all inputs and outputs should be simple Sinks/Streams,
  • dependencies have to be injectable and platform agnostic,
  • logic flow implementation cannot depend on any platform specific feature.

UI components should also follow certain rules:

  • each “complex” component should have its corresponding BLoC,
  • input data shouldn’t be converted in any way before passing to the BLoC,
  • data from BLoC outputs should be displayed “as is” – if possible without applying any changes,
  • all UI logic should depend only on data from BLoCs without external sources.

 

Initial setup

The project consists of three packages: shared, web and mobile. shared package has all the business logic that is independent of the platform. web and mobile packages contain code that renders UI and is responsible for Firebase Cloud communication service (which is platform-specific). This way we can share the most important part of the client (business logic) and make platform-specific code as thin as possible.

 

Implementation

Reading username

At first, the user has to enter his/her nickname. Let’s create a BLoC that would allow usersuser to set a nickname. The interface of such BLoC may look like this:

import 'dart:async';

import 'package:chat_shared/blocs/bloc.dart';

/// Interface of BLOC that collects user's name
abstract class UserBloc extends Bloc {
  Sink<String> get username;
  Stream<bool> get usernameSet;
}

As you can see there is one input (Sink<String>) for passing the nickname to the UserBloc and one output (Stream<bool>) that will be used to trigger UI to move to the chat screen. Implementation of this interface will be used both in web and mobile applications and can be found here. Then we need to create UI components for that BLoC. In the case of Flutter client, it will be the whole screen. The screen may be implemented as follows: 

import 'dart:async';

import 'package:flutter/material.dart';

import 'package:chat_shared/blocs/user_bloc.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Chat App',
      theme: new ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or press Run > Flutter Hot Reload in IntelliJ). Notice that the
        // counter didn't reset back to zero; the application is not restarted.
        primarySwatch: Colors.deepOrange,
      ),
      home: UsernamePage(),
    );
  }
}

class UsernamePage extends StatefulWidget {
  @override
  UsernamePageState createState() {
    return new UsernamePageState();
  }
}

class UsernamePageState extends State<UsernamePage> {
  final GlobalKey<FormState> _usernameForm = GlobalKey();

  UserBloc bloc;

  Future<Null> _usernameSet;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("User"),
      ),
      body: Form(
        key: _usernameForm,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 8.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextFormField(
                autofocus: true,
                autocorrect: false,
                onSaved: (username) {
                  bloc.username.add(username);
                },
                decoration: InputDecoration(
                  labelText: "Nick",
                  hintText: "Pick your nickname",
                ),
              ),
              SizedBox(
                height: 8.0,
              ),
              RaisedButton(
                child: Text("OK"),
                onPressed: () {
                  _usernameForm.currentState.save();
                },
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    bloc = UserBlocImpl();
    _usernameSet =
        bloc.usernameSet.skipWhile((event) => !event).first.then((_) {
      print("Name set. TODO: trigger navigation");
    }, onError: (e) {
      print("$e happened");
    });
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }
}

As you can notice, UI code is simple and all interactions are done through UserBloc instance. The web UI implementation can be found here (notice I’m using React Dart package to create components. It could be also implemented using AngularDart or just by using Dart for the web build-in features). As a result, we have a web page and a mobile app that allows users to put their nickname.

 

Showing chat messages

In the next step we build components that would display a list of messages coming from MessageService. MessagesBloc contains only single output Stream<List<Message>>. That output is a list of messages that is presented by the UI. It uses MessageService interface injected through the constructor parameter but it does not depend on any implementation. In fact during development I used an implementation of MessageService that just echoed entered messages. The MessagesBloc implementation looks like this:

import 'dart:async';

import 'package:chat_shared/blocs/bloc.dart';
import 'package:chat_shared/data/message.dart';
import 'package:chat_shared/services/message_service.dart';
import 'package:rxdart/rxdart.dart';

/// Provides a stream of messages list to be displayed
abstract class MessagesBloc extends Bloc {
  Stream<List<Message>> get messages;
}

class MessagesBlocImpl extends MessagesBloc {
  final MessageService messageService;
  final BehaviorSubject<List<Message>> _messages;

  MessagesBlocImpl(this.messageService)
      : assert(messageService != null),
        _messages = new BehaviorSubject(seedValue: []) {
    messageService.setOnMessagesUpdatedCallback((list) {
      _messages.add(list);
    });
  }

  @override
  void dispose() {
    messageService.setOnMessagesUpdatedCallback(null);
    _messages.close();
  }

  @override
  Stream<List<Message>> get messages => _messages.asBroadcastStream();
}

Again, quite simple logic that allows us to build a simple UI component. Here is the web version of MessagesComponent that uses MessagesBloc. The mobile version is using the StreamBuilder widget provided by Flutter to build UI easily from a stream of messages. The whole MessagesList widget is implemented as follows:

import 'package:chat_shared/blocs/messages_bloc.dart';
import 'package:chat_shared/data/message.dart';
import 'package:flutter/material.dart';

class MessagesList extends StatelessWidget {
  final MessagesBloc bloc;

  const MessagesList({Key key, @required this.bloc})
      : assert(bloc != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<Message>>(
      stream: bloc.messages,
      builder: _buildContent,
    );
  }

  Widget _buildContent(
      BuildContext context, AsyncSnapshot<List<Message>> snapshot) {
    if (snapshot.hasData && snapshot.data.length > 0) {
      return _buildList(context, snapshot.data);
    } else {
      return _buildNoDataView(context);
    }
  }

  Widget _buildNoDataView(BuildContext context) => Center(
        child: Column(
          children: <Widget>[
            Text(
              "No messages yet.",
              style: TextStyle(
                fontSize: 30.0,
              ),
            ),
            Text(
              "Write first one!",
              style: TextStyle(
                fontSize: 20.0,
              ),
            ),
          ],
        ),
      );

  Widget _buildList(BuildContext context, List<Message> data) {
    var reversed = data.reversed.toList();
    return ListView.builder(
      reverse: true,
      itemCount: reversed.length,
      itemBuilder: (context, index) {
        Message m = reversed[index];
        return _MessageItem(
          message: m,
        );
      },
    );
  }
}

class _MessageItem extends StatelessWidget {
  const _MessageItem({
    Key key,
    @required this.message,
  }) : super(key: key);

  final Message message;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            message.username,
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          SizedBox(
            height: 4.0,
          ),
          Text(
            message.content,
            textAlign: TextAlign.left,
          ),
        ],
      ),
    );
  }
}

I will skip the presentation of the SendMessage components implementation as they are similar to what you have already seen before and move directly to the implementation of MessageService using a remote database.

 

Backend service

We want to send all messages to the Cloud Firestorage – this way we have synchronized message storage that also works while offline. But there is not a single package available to provide Firebase API for both web and mobile applications. Because of that we have to implement MessagesService twice. You can check the implementation for the web and mobile. Both are very similar. They obtain messages from the Firestorage collection limiting results to 15 recent entries and mapping documents to Message data objects. After that, they call the provided callback whenever a new list of Messages comes from the backend.

 

Closing thoughts

As you could see we were able to write client applications for web and both mobile platforms using almost only Dart (some boilerplate being written/generated in HTML/CSS, Swift and Kotlin). We were able to have roughly a little above 30% of lines of code (including BLoC tests) shared between all platforms with almost 40% lines of code being part of Flutter module (Flutter is quite verbose when counting lines – many of them just contain closing bracket) and the remaining 30% in web application module. You may expect an even larger amount of shared code if your application does contain a lot of business logic and doesn’t depend on the platform-specific solutions. Also if you heavily test your business logic (as I did in the example) then you don’t need to repeat implementing it for the web/mobile separately.

 

Problems

What happened is that I needed to use the older firebase package, because of the dependency conflicts with the other packages. Lot of Dart packages are evolving rapidly and change language/package dependencies often making it difficult to combine the right versions sometimes. As a way of solving this problem you can set a package version to any and allow pub to resolve them for you. Then you can check pubspec.lock for used versions and copy the values back to pubspec.yaml

Sources:

 

Rated: 5.0 / 1 opinions
avatar male f667854eaa

Łukasz Huculak

Senior Flutter Developer

Our mission is to accelerate your growth through technology

Contact us

Codete Global
Spółka z ograniczoną odpowiedzialnością

Na Zjeździe 11
30-527 Kraków

NIP (VAT-ID): PL6762460401
REGON: 122745429
KRS: 0000983688

Get in Touch
  • icon facebook
  • icon linkedin
  • icon instagram
  • icon youtube
Offices
  • Kraków

    Na Zjeździe 11
    30-527 Kraków
    Poland

  • Lublin

    Wojciechowska 7E
    20-704 Lublin
    Poland

  • Berlin

    Bouchéstraße 12
    12435 Berlin
    Germany