How to Implement Clean Architecture Using GetX and Bloc Architecture in Flutter?

Hire Flutter Developers

In this article, we are going to implement clean architecture using GetX and Bloc architecture in Flutter. This guide is easy to learn, and with prior knowledge about Flutter, one can implement clean architecture using BLoC architecture in Flutter. 

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

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. 

Read our other post on How Flutter is helpful to craft a fast user Experience

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. 

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

   extends Bloc<SimpleLoaderEvent, SimpleBlocState> {

 SimpleLoaderBloc()

Every BLoC must provide a path to load <T> resource but by overriding the load method. 

Future<T> load(SimpleLoaderEvent event);

Also, mapping between the states and events is easy. 

@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;

   }

 }

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);

}

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. 

  1. 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

Future configureInjection(String environment) async {

 $initGetIt(getIt, environment: environment); //call the service registration

}

void main() async {

 await configureInjection(Env.prod);

 runApp(MyApp());

}

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 localization of the app. 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:

  1. In your AppLocalizations.dart file, write the keys. 

class AppLocalizations {

 String get getStarted {

   return Intl.message(“Get started”, name: ‘getStarted’);

 }

}

  1. Now, place the same key in Lokalise, or you can even do that manually by providing a file. 
  1. 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

mkdir coverage

touch coverage/lcov.base.info

for module in “[email protected]

do

   echo “testing $module…”

   cd “$module” || exit

   flutter test –coverage –coverage-path=coverage/”${module}”.info

   var1=lib/

   var2=${module}/lib/

   sed -i “[email protected]${var1}@${var2}@” coverage/”${module}”.info

   cat coverage/”${module}”.info >> ../coverage/lcov.base.info

   cd ..

done

With this script, you can create coverage reports within each module 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, merge it with module reports in the lcov.base.info file. 

sh coverage.sh core data domain

Finally, 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.

Subscribe to get regular content updates, and offers

Also Read This

Best Offshore Development Team

Cost-Benefit Analysis of Outsourcing

Offshore Development Centre

Offshore Development Center in India

ODC Centre

HOW IT WORKS?