A Flutter BLoC + Clean Architecture journey to release the #1st Idean Flutter app

Etienne Cadic
Ideas by Idean
Published in
12 min readSep 21, 2020

--

In June 2020 we decided to start at Idean our 1st ever production application in Flutter for one of our brand new clients:

With a strong experience in developing and deploying Android & iOS native apps, the challenge was to make sure we could apply all of our best practices with this *quite* new framework & language. Here is (I hope) an exhaustive article about the minimum perks that a framework needs to develop serious and long-term mobile apps, and how we managed to make it happen for Flutter.

#Disclaimer : this article is written by a former Android developer thus is Android production oriented.

We are going to talk about architecture, dependency injection, internationalization, and static analysis.

Architecture (1/2) : the Clean Architecture for real

At Idean, like in many other mobile development companies, we have adopted the Clean Architecture for all our new projects since 2018. With this new Flutter project we naturally started looking for resources talking about Flutter & Clean Architecture and see what was possible to achieve.

This excellent series of videos from Matej Rešetár (ResoCoder) just made it easy for us.

But while the Clean Architecture project and the different layers (data/domain/presentation) were organized in folders inside the same module (lib/), we definitely wanted to go further and achieve a true separation between layers.

The main argument of this, and this is one of our best practices, is to prevent any newcomer to misuse code that doesn’t belong to the right layer, by actually making it impossible.

Let’s be honest: learning clean architecture is NOT straightforward and it may take some time before newcomers can tackle the project and use the proper models & use cases, at the right place, especially if they actually can do it wrong. Prevent them to do so by setting up a physical separation, and making the compiler complain will save you from many refactoring & code reviews.

And this is the very reason why we split the Clean Architecture Layers in Dart modules.

Taking inspiration directly from former Android modules, we set up as many dart modules as layers, while we dedicated the main module to embracing the presentation layer.

Our project was then split as :

lib ← presentation layer: All the UI stuff, widgets and design utils

domain layer: the business layer, manipulating pure entities through usecases.

data layer: All the input data stuff (from async sources)

core: useful to share code between layers

And today as the application is now in production, I can’t find any argument against this choice of architecture.

Architecture (2/2): Meeting the BLoC Family

While many simple apps can manage business logic using Providers, what comes out from Flutter literature and forums is that BLoC or Redux frameworks are more suited for long term apps with many screens and complex logic.

We already had been using Redux on previous React Native projects and we wanted to try something new. So we jumped in using BLoC (= Business Logic Component) inside our presentation layer to be the link between UI and our business logic world in domain layer.

Using depencency injection (we will be covering that later), the BLoCs will be the only classes in our app manipulating our use cases!

Coming from the native world, BLoC can be seen as — yet another presenter disguised in ViewModel —inserted inside your widget tree through a BlocProvider, and helping you respond to events by returning states that can be used to build the corresponding UI (with a BlocBuilder) or perform corresponding actions (with a BlocListener).

While nobody agrees on whether Blocs should be used for each screen or for a single widget we tried a pragmatic approach using inheritance and easy of use principles.

A rule of thumb we designed after 2 months writing BloCs & dozens of screens could be = do whatever you need to prevent code duplications; but mostly, follow the BLoC family principles©!

Rule of thumb #1: one BLoC per ‘Brothers and Sisters’ widgets (screens)

In almost every screen of your app, you need to display asynchronous batches of data coming from heterogeneous sources that might be a database, a remote server, the local storage; or the long computation of some complex task.

As your screen displays small pieces of those fetched data everywhere (in the title, but also maybe in the footer, in the list in the center of screen or anywhere else), we tend to fetch the data in a Bloc provided on top of those widgets.

Then, screens of your app might be seen as grown up brothers and sisters widgets, all of them equal, at the same level and requiring a dedicated BLoC for each.

“Brothers and Sisters” BLoCs

But brothers and sisters might be parented, and it’s perfectly OK if some of your BLoCs inherit the same abstract BloC class if they share the same behaviour. But as your family is wide and open, it’s also OK if they don’t.

Rule of thumb #2: one BLoC per ‘Mom’ widget

It appears that sometimes, grown-up brothers and sisters still live under the protection of their parent, taking care of them and leading them all to the right direction. So do the screens widgets that might be tunneled together to achieve a goal, like a tutorial, an onboarding or any series of steps.

If you have an onboarding process with multiple screens, it can be useful to have a global onboarding BLoC holding the onboarding generic logic, the steps, while on each screen, you might need a dedicated BLoC to perform the unique behaviour related to this screen.

/!\ Using a Nested Navigator in this particular case can be very pertinent: your ‘Mom’ BLoC will then be on top of all your nested navigator hierarchy.

Rule of thumb #3: one BLoC per ‘independent child’ widget

It also appears that sometimes growing children seek independence. Children widgets within a single screen sometimes do the same. Say you have an independent widget looking for data that only it needs, and that widget might be shared across several screens: this independent widget definitely needs a dedicated BLoC.

Rule of thumb #4: Avoid duplication by any mean

In many simple screens, the only thing you need to do is to display a loader, fetch data, then display the result or an error. As every BLoC requires a lot of stuff to be written to be working (the events, the states, and their mapping), it can become super annoying to always write the same stuff for every simple bloc.

We then provided an abstract SimpleLoaderBloc<T> dedicated for single async call and returning a result, or emitting an error state, that we used everywhere we didn’t need a custom behavior.

abstract class SimpleLoaderBloc<T>
extends Bloc<SimpleLoaderEvent, SimpleBlocState> {
SimpleLoaderBloc()

Every simple bloc extending SimpleLoaderBloc must provide the way to load the <T> resource by overriding the load method

Future<T> load(SimpleLoaderEvent event);

The mapping between events and states is quite straightforward, and always the same in simple cases:

@override
Stream<SimpleBlocState> mapEventToState(
SimpleLoaderEvent event,
) async* {
switch (event.type) {
case SimpleBlocEventType.StartLoading:
default:
yield SimpleLoadingState();
try {
final T items = await load(event);
yield SimpleSuccessEventState<T>(items);
} catch (error) {
yield SimpleErrorEventState(error);
}
break;
}
}

Finally, altogether the code is quite simple and easy to read:

part 'simple_loader_event.dart';
part 'simple_loader_state.dart';

///Simple class aimed to provide mutual logic for simple blocs that ///only do async resource fetching
abstract class SimpleLoaderBloc<T>
extends Bloc<SimpleLoaderEvent, SimpleBlocState> {
SimpleLoaderBloc() : super(SimpleInitialState());

@override
Stream<SimpleBlocState> mapEventToState(
SimpleLoaderEvent event,
) async* {
switch (event.type) {
case SimpleBlocEventType.StartLoading:
default:
yield SimpleLoadingState();
try {
final T items = await load(event);
yield SimpleSuccessEventState<T>(items);
} catch (error) {
yield SimpleErrorEventState(error);
}
break;
}
}

Future<T> load(SimpleLoaderEvent event);

}

This is far from being revolutionary but again, the key word is: pragmatism. Do whatever you can to ease your life.

But avoiding duplication is not the only way of written an easy maintainable project: dependency injection is a strong tool to ease developers‘s life when working on huge projects.

Dependency injection: GetIt + Injectable

Dependency injection was designed to answer this simple question: How can it be decent to go through the 300k+ lines of code of a software program just to change the signature of the constructor of a class that has been used everywhere?

With dependency injection, those things never happen = you say what you need in your constructor, you say what you provide, the DI links your dependencies to your classes, and you are DONE. No need for boilerplate instantiation.

GetIt

GetIt may be THE dependency injection package featured by pretty much every flutter advocate. While the easiness of use is definitely a good reason to adopt it, and the features it provides are everything a developer could dream of for writing robust and easy-to-maintain apps, getIt is verbose, and registering every service & factories consists of writing amounts of boilerplate code.

Hopefully, some people wrote a small dart package on top of it to provide getIt code generation with the use of annotations: injectable, that android developers using Dagger or angular developers might be familiar with.

Injectable

With injectable, a single annotation (and the famous flutter pub run build_runner build command) will provide you with the proper boilerplate code needed for getIt to perform DI:

@injectable
class AddUserVehicleUsecase extends CompletableUseCase<UserVehicle>{
final GetUserUsecase _getUserUsecase;
final SaveUserUsecase _saveUserUsecase;

We can see that the generated code is not far from what we would do by hand:

g.registerFactory<AddUserVehicleUsecase>(
() => AddUserVehicleUsecase(g<GetUserUsecase>(), g<SaveUserUsecase>()));

But what was not documented nor maybe expected was to share DI between multiple dart modules (remember, we are writing a clean architecture app, split in multiple layer/dart modules): and that’s what we have been achieving.

DI accross multiple dart modules

Maybe things have changed with new injectable versions, but starting with injectable 0.4.0+1, injectable code generation of classes from another module was not working and we had to help the tool a bit.

While a traditional DI initialization with injectable may look like this…

@injectableInit
Future configureInjection(String environment) async {
$initGetIt(getIt, environment: environment); //call the service registration
}
void main() async {
...
await configureInjection(Env.prod);
...
runApp(MyApp());
}

…for each dart module, we had to provide a similar mechanism, in order to make injectable generate the boilerplate in every module :

in data/lib/data_injection.dart@injectableInit
Future configureDataInjection(final getIt, String environment) async => $initGetIt(getIt, environment: environment); //this $initGetIt method gets generated for you inside your dart module.
void main() {
configureDataInjection(getIt, Environment.dev);
}

Providing a configure<Module>Injection method for each module then made things straightforward to initialize everything at app startup:

@injectableInit
Future configureInjection(String environment) async {
configureDataInjection(getIt, environment);
configureDomainInjection(getIt, environment);
$initGetIt(getIt, environment: environment);
}

NB: the main() method in every module is here just to let injectable complete its dependency graph and is not used in the app.

I10n : Arb and Lokalise

Make your app support internationalization might be less straightforward than what you think. While holding simple jsons for each Locale sounded like an easy way of handling translations, it meant to have a mapping between each json key and a magic string or const within the code.

In order to try something new (again!) we jumped in using ARB, a json extended format supported by Google. Which was okay until our project director informed us they had been heavily lobbying for our client to register to Lokalise

Lokalise is a very nice and professional tool to handle translations and we have been using it across a lot of projects with our clients. But does Lokalise support Flutter or at least ARB format? Hell NO!

As the client was going to use Lokalise anyway, we had to find a way to somehow import translations from their API and convert it to ARB… And as always with Flutter development… wait.. there is a package for it!

flutter_lokalise is a small dev package allowing you to pull strings out of your Lokalise project and convert them to arb! Following the documentation, we were able to make it -almost- work, with this process:

  1. Write the keys in your AppLocalizations.dart file
class AppLocalizations {
...
String get getStarted {
return Intl.message("Get started", name: 'getStarted');
}
...
}

2. Put the same key on Lokalise (or import it manually by providing a file)

Lokalise interface

3. Use flutter_lokalise tool to generate your arb file.

3.1. Configure flutter_lokalise to connect to your Lokalise project:

//inside your pubspec.yaml
flutter_lokalise:
api_token: <your api token>
project_id: <your project id>

3.2 Simply call download command:

flutter pub run flutter_lokalise download

You will get your arb file with Lokalise keys:

lib/l10/intl_en.arb
{
"@@locale": "en",
...
"getStarted": "Get started",
...
}
lib/l10/intl_fr.arb
{
"@@locale": "fr",
...
"getStarted": "Démarrer",
...
}

Then simply prompt the flutter internationalization commands to generate dart translations files from arb:

flutter pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/intl/app_localizations.dart lib/l10n/intl_*.arb

Handling plurals in Lokalise + ARB

The only problem with this process is when you need to give variables to your strings. Say you have this plural:

String homeDetailsBookMaintenanceAddAdvise(int nbMonths) {
return Intl.plural(nbMonths,
one: "Set the same reminder in one month ?",
other: "Set the same reminder in $nbMonths months ?",
name: "homeDetailsBookMaintenanceAddAdvise",
args: [nbMonths]
);
}

Your Lokalise project won’t know that your AppLocalization.dart expect a variable nbMonths. If you just run the dowload command, your arb file will look like this:

"homeDetailsBookMaintenanceAddAdvise": "{homeDetailsBookMaintenanceAddAdvise, plural, one { Set the same reminder in one month ?} other { Set the same reminder in {homeDetailsBookMaintenanceAddAdvise} months ?}}",

As your program expect a variable called “nbMonths”, the code generation will fail. With no custom key for plurals, flutter_lokalise will just put the key of the plural itself. The current workaround is to set up in Lokalise the proper key you expect:

While this is not ideal, it can force your to discuss more with your product owner, and your clients ;)

Static analysis and code coverage

Of course, you always write high quality code. But how can your project director or your client actually know? Static analysis of your code helps you be confident about the code you write and at the same time, share your KPIs with your teams.

Static analysis with Sonar

At Idean, using static analysis with Sonar is one of our best practices, that we do on all of our projects, whatever the language. Using Sonar offers you the ability to automate your reports within a continuous integration, easing life of developers and giving leads and directors a way to monitor code quality (as well as code coverage).

We have been deeply involved in several open-source plugins like SonarSwift , or more recently SonarDart for Flutter projects. While the static analysis might sometimes raise false positives or “unjustified critical issues” depending of the analysis profile (which is for the moment non configurable for sonar-dart), it definitely gives you valuable insights about your code (and you can decide afterwards if you need to change the severity of some issues).

Code coverage with Flutter & LCOV in a multi-modules project

While code coverage is pretty much straightforward with the current flutter tools (I have already been writing an article about the Flutter tests reports & coverage state of art), things tend to be harder with a multi module project, like the ones we have following Clean Architecture with multiple dart modules.

flutter test --coverage --coverage-path=<your path>

Flutter offers you the possibility to merge coverage reports, but we had to sweat a bit before making it working.

#!/bin/bash

mkdir coverage
touch coverage/lcov.base.info

for module in "$@"
do
echo "testing $module..."
cd "$module" || exit
flutter test --coverage --coverage-path=coverage/"${module}".info
var1=lib/
var2=${module}/lib/

sed -i "s@${var1}@${var2}@" coverage/"${module}".info
cat coverage/"${module}".info >> ../coverage/lcov.base.info
cd ..
done

Using this simple script will make you generate coverage reports inside every module, and append them altogether in a base lcov.base.info file (and editing relative paths as well, so that global report generated afterwards is properly linked to each class).

Finally, using…

flutter test --coverage --merge-coverage

…will make Flutter compute coverage for your main lib project and merge it with your module reports contained in the lcov.base.info file.

sh coverage.sh core data domain

In the end; just call the script with all the modules you have in parameters, before exporting the report to a more readable format like html or through Sonar.

And that’s pretty much everything for this article, that I didn’t expect to be so long… What came out of this journey is that Flutter is definitely ready for production, and that it is possible to write code in Flutter, without forgetting all your good practices you used to apply in your former languages.

--

--