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 form of BLoCs (Business Logic Components). The whole application will be developed in Dart using Flutter as a mobile framework and React for the web with Firebase Cloud Firestore as a 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.

Business Logic Components

BLoC (Business Logic Components) in a nutshell

Business Logic Components (or BLoCs) contain all the business logic of the client application leaving UI 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 the platform-specific code as thin as possible.

Implementation

Reading username

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

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 case of Flutter client, it will be the whole screen. The screen may be implemented as follows:

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 user to put their nickname.

Showing chat messages

In the next step we build components that would display 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 implementation of MessageService that just echoed entered messages. The MessagesBloc implementation looks like this:

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

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 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 provided callback whenever a new list of Messages comes from 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 even large 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 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.

Demo:

Sources:

lukaszhuculak

I work with Android since Donut. I believe in solving people's everyday problems with mobile technologies.