How to Implement Clean Architecture Using Bloc Architecture in Flutter?

How to Implement Clean Architecture Using Bloc Architecture in Flutter?

Quick Summary:
Clean architecture and the Bloc pattern are powerful tools for building maintainable and scalable Flutter apps. This article provides an overview of clean architecture and demonstrates how to implement it using the Bloc library in your Flutter project.

Flutter has quickly become a popular choice for building cross-platform mobile apps. However, as your app grows in complexity, it becomes challenging to maintain and scale. This is where Clean Architecture and the Bloc pattern can help.

Clean Architecture is an architectural approach that emphasizes the separation of concerns, while the Bloc pattern provides a way to manage the flow of data and events in your app. In this article, we'll explore how to implement Clean Architecture using the Bloc library in Flutter to build maintainable, scalable, and testable apps.

So, without further ado, let’s start learning! 

Bloc Architecture in Flutter

Architecture 1/2: The Clean Architecture

Before starting anything, it is crucial to understand that learning clean architecture is not that easy. You will need some time to tackle the project and utilize proper models and uses cases to make it work.  It is essential to set up physical separation and make the compiler complain from code reviews and refactoring. It is one of the most prominent reasons why Clean Architecture Layers need to be split in the Dart modules. So, you will need to set up as many dart modules as layers. 

So, the clean architecture would include all the UI stuff, widgets, and design utils. Then, in the domain layer, we have the manipulating pure entities through use cases and the business layer. Next, the data layer will have all the input data stuff from async sources. Finally, the core is useful for sharing the code between the layers. 

Architecture 2/2: The BLoC

BLoC or Redux frameworks complement the long-term apps with complex logic and many screens. BLoC or Business Logic Component acts as a link between UI and the business logic world in the domain layer. The BLoCs will be the only class in the app that will manipulate the use case using the dependency injection. 

You can consider BLoC as the presenter disguised in ViewModel, which is inserted inside the widget tree using a BLocProvider. It will let you respond to the events by returning states, which can further be utilized to build the corresponding UI. 

Bloc Architecture in Flutter - Thumb Rules to Keep in Mind

Flutter App Development Services

1. One BLoC for Sibling Widget

It is important to display asynchronous batches of data obtained from heterogeneous sources, including a remote server, local storage, database, or lengthy computation of a complicated task on every screen. The app screen displays petite chunks of that obtained data everywhere, so we tend to gather the data in BLoC on top of those widgets. Eventually, it will make the screen look good with every widget in place and placed where it is required.

2. Mom Widget for one BLoC

Sibling widgets might still function under their parent’s protection, handling them and directing them in the right direction. Even the screen widgets are tunneled together to attain a specific goal like onboarding or a series of steps. You might need a global onboarding BLoC that holds the onboarding generic logic. But for every screen, you will need a dedicated BLoC to display and behave related to the screen. To keep the Mom BLoC on top, you need to use a Nested Navigator. 

3. One BLoC for Every Child Independent Widget

The children widget on a single screen needs independence too. For that, it indeed requires a dedicated BLoC for an independent widget looking for data that is only needed by this widget. However, it will be shared on different screens. 

4. Say No to Duplication

For many apps, there are just a few things that they need to display, including the fetch data, a loader, and display it all without any error. However, writing the same thing for every BLoC can be quite tiresome. Also, there are a lot of things in BLoC to be worked upon, like the states, the events, and their mapping. We can simply provide an abstract SimpleLoaderBloc<T> which is dedicated to a single async call and returning the outcome without any error. 

abstract class SimpleLoaderBloc
extends Bloc {
SimpleLoaderBloc()
//Every BLoC must provide a path to load resource but by overriding the load method.
Future load(SimpleLoaderEvent event);
//Also, mapping between the states and events is easy.
@override
Stream mapEventToState(
SimpleLoaderEvent event,
) async* {
switch (event.type) {
case SimpleBlocEventType.StartLoading:
default:
yield SimpleLoadingState();
try {
final T items = await load(event);
yield SimpleSuccessEventState(items);
} catch (error) {
yield SimpleErrorEventState(error);
}
break;
}
}
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
extends Bloc {
SimpleLoaderBloc() : super(SimpleInitialState());
@override
Stream mapEventToState(
SimpleLoaderEvent event,
) async* {
switch (event.type) {
case SimpleBlocEventType.StartLoading:
default:
yield SimpleLoadingState();
try {
final T items = await load(event);
yield SimpleSuccessEventState(items);
} catch (error) {
yield SimpleErrorEventState(error);
}
break;
}
}
Future load(SimpleLoaderEvent event);
}

When developing the app, it is not just the duplication you have to take care of but also the dependency injection, which apparently is a robust tool to simplify complex and huge projects for developers. 

GetIt and Injectable for Dependency Injection

The purpose of dependency injection is to seamlessly scan the thousands of code lines of a software program to do the modification. All you need to do is say what you need in your constructor, the DI will link the dependencies to your classes, and that’s all. There is no requirement for any boilerplate instantiation. 

1. Injectable

Using it, a single annotation will help you provide the boilerplate code, which is essential for getIt to perform DI. 

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

After seeing this, we can easily say that the code that is generated is something similar to what we can write. 

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

However, the DI might not be shared as expected between multiple dart modules. 

2. GetIt

The best about GetIt is its easiness to use, along with some superb features that it provides for simplifying the app development task for the developers. Besides, it helps them to write strong and easy-to-maintain apps. 

GetIt package is a sort of simple service locator, where you will have a central registry. In that registry, when we register the class, we can get an instance of that specific class. We usually use it rather than the inherited widget we use to access the object Is from the UI. Besides, dependency injection and service locator are forms of IOC or Inversion of Control, allowing requests from anywhere from registering the class type to getting access to the container. 

DI Across Different Dart Modules

Flutter App Development

A conventional way of doing DI initialization with injectable appears something like this;

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

We need to provide a similar mechanism to make injectable bring about the boilerplate in each 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);
}
We have to provide a configure <Module>Injection method for every module to simplify everything for the app development.

@injectableInit
Future configureInjection(String environment) async {
configureDataInjection(getIt, environment);
configureDomainInjection(getIt, environment);
$initGetIt(getIt, environment: environment);
}
Every module incorporates the main() method to allow injectable to complete its dependency graph while not being used in the app.

Arb and Lokalize

For your app to support internationalization, it needs to have a mapping between each magic string and json key or const within the code.

Now, we will proceed with the following steps. In this step, we will generate the arb file. Flutter localize is a small dev package that lets you pull strings out of your Lokalize project before transforming them to ard. Here’s how we do it:

In your AppLocalizations.dart file, write the keys.
class AppLocalizations {
...
String get getStarted {
return Intl.message("Get started", name: 'getStarted');
}
...
}
Now, place the same key in Lokalise, or you can even do that manually by providing a file.
In this step, we will generate the arb file by using the flutter_lokalise
Configure flutter_lokalise to help it connect to the Lokalise project.
//inside your pubspec.yaml
flutter_lokalise:
api_token: <your api token>
project_id: <your project id>
Call download command
flutter pub run flutter_lokalise download

Now, you can get your arb file using the Lokalise keys.

lib/l10/intl_en.arb {
"@@locale": "en",
...
"getStarted": "Get started",
...
}

lib/l10/intl_fr.arb {
"@@locale": "fr",
...
"getStarted": "Démarrer",
...
}
Now, prompt the flutter internationalization commands to create dart translations files.

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

Code Coverage and Static Analysis

Hire Flutter Developers

With the help of Static Analysis, you can actually analyze your code and be more confident about its quality and even share KPIs with the team. For code coverage with Flutter is quite simple. However, it could be challenging when you are working on a multi-module project. 

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

Flutter lets you merge coverage reports like this: 

#!/bin/bash<br>mkdir coverage<br>touch coverage/lcov.base.info<br>for module in "$@"<br>do<br> &nbsp;echo "testing $module..."<br> &nbsp;cd "$module" || exit<br><br>flutter test --coverage --coverage-path=coverage/"${module}".info<br> &nbsp;var1=lib/<br> &nbsp;var2=${module}/lib/<br> &nbsp;sed -i "s@${var1}@${var2}@" coverage/"${module}".info<br> &nbsp;cat coverage/"${module}".info &gt;&gt; ../coverage/lcov.base.info<br> &nbsp;cd ..

done

With this script, you can create coverage reports within each module and add them to the base lcov.base.info file. 

flutter test --coverage --merge-coverage

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

sh coverage.sh core data domain

Finally, flutter developers  just need to call the script with every module before exporting the report to Html or a readable format. 

At your Team in India, we have a team of flutter experts. If you want to hire Developers or have any questions on what all services we offer at Your team in India– Click here to contact us.

Frequently Asked Questions (FAQs)

What is Clean Architecture, and why is it important?
Clean Architecture is an architectural approach that emphasizes separation of concerns, making it easier to maintain and scale your app over time. By separating your app into layers with well-defined responsibilities, you can ensure that changes in one part of your app don't cause unintended consequences elsewhere.
What is the Bloc pattern, and how does it relate to Clean Architecture?
The Bloc pattern provides a way to manage the flow of data and events in your app, making it easier to implement Clean Architecture principles. With Bloc, you can define the inputs and outputs for each component in your app, making it easier to reason about the behavior of your app as a whole.
How do I get started with implementing Clean Architecture using Bloc in Flutter?
To get started, you'll need to install the Bloc library and set up your app's architecture into separate layers, such as presentation, domain, and data layers. From there, you can use the Bloc pattern to manage the flow of data and events between these layers, making it easier to test and maintain your app over time.
What are the benefits of using Clean Architecture and Bloc in Flutter?
By using Clean Architecture and the Bloc pattern in Flutter, you can build maintainable, scalable, and testable apps that are easier to maintain over time. These tools help you separate concerns in your app, making it easier to reason about the behavior of your app as a whole and avoid unintended consequences. Additionally, using the Bloc pattern can simplify your code and make it easier to manage state and events in your app.

Mangesh Gothankar

Mangesh Gothankar

Seasoned technology professional with over 19 years of experience leading technology innovation and execution with startups and MNCs. Experience in hiring and creating multiple world-class technology teams. Results-oriented with a passion for technology, recognized for successfully planning and executing major initiatives.