How to create a Modular Application with Flutter — Part 2

Alvaro Armijos
4 min readApr 15, 2024

--

In the second part of this article, we will explore how to implement dependency injection among different packages in Flutter using the get_it package. Dependency injection is a fundamental technique for improving the modularity and testability of applications. We will see how these tools make it easier to develop and maintain our Flutter applications.

Introduction

Before you can access your objects you have to register them within GetIt typically direct in your start-up code. Usually, setting up dependency injection with GetIt involves a setup that looks like the following:

final sl = GetIt.instance;

Future<void> configureDependencies(GetIt sl) async {
//Use cases
sl.registerLazySingleton(() => GetCharactersUseCase(sl()));

//Mappers
sl.registerLazySingleton(() => const FilmMapper());

//Repository
sl.registerLazySingleton<CharactersRepository>(
() => CharactersRepositoryImpl(
sl(),
sl(),
),
);

// Api Client
sl.registerLazySingleton(
() => CatalogApiClient(sl()),
);
}

It looks simple, all we have to do is register all our dependencies. But we’re only talking about a single package. Usually, that’s your Flutter app, for example.

What if we had a project where you controlled several packages and still wanted to use this simple di solution?

Setting Up the Dependency Flow

Our example application has the following structure:

app/
app
packages/
core/
utility
ui
data/
catalog
device
features/
home

If you think about it, this makes perfect sense because App depends on all the rest of the packages. And the main pubspec.yaml file looks like this:

name: star_wars
description: A new Flutter project.

dependencies:
flutter:
sdk: flutter

##Packages
catalog:
path: packages/data/catalog/
device:
path: packages/data/device/
utility:
path: packages/core/utility/
ui:
path: packages/core/ui/
home:
path: packages/features/home/

Implementation

Setup the packages

For consistency, we’ll do the DI implementation exactly like in the above structure.

Setting up DI with GetIt

All of our packages need a few dependencies. We add the following dependency in all packages.

dependencies:
flutter:
sdk: flutter
get_it: ^7.6.0

Or you can import get_it in the utility package and export it from this package to the other modules. So instead of implementing in each module where you need GetIt or other libraries that will be repeated in different modules, you can import the utility module that already has all the libraries you need in your application.

For example, utility has the following libraries:

name: utility
description: A new Flutter package project.
version: 0.0.1
publish_to: 'none'

environment:
sdk: ">=2.17.6 <3.0.0"
flutter: ">=1.17.0"

dependencies:
flutter:
sdk: flutter

# Remote API connection
http: ^0.13.5
# Service locator
get_it: ^7.6.0
# Bloc for state management
flutter_bloc: ^8.1.4
# Value equality
equatable: ^2.0.5
# testing
bloc_test: ^9.1.5
mocktail: ^1.0.0

And to export these libraries so that other application modules can use them, we can create a file called utility.dart:

library utility;

export 'package:bloc_test/bloc_test.dart';
export 'package:equatable/equatable.dart';
export 'package:flutter_bloc/flutter_bloc.dart';
export 'package:get_it/get_it.dart';
export 'package:http/http.dart';
export 'package:mocktail/mocktail.dart';

export 'di/injection_container.dart' show configureDependencies;
export 'src/helpers/json_reader.dart';
export 'src/view_state.dart';

This helps us to have centralised dependencies, to have the same version of the libraries for all modules, and to avoid having to repeat the same library imports for each module.

And now if we want to use GetIt in the catalog module, we need to do the following

name: catalog
description: A new Flutter package project.
version: 0.0.1
publish_to: 'none'
homepage:

dependencies:
flutter:
sdk: flutter

##Packages
utility:
path: ../../core/utility/
import 'package:utility/utility.dart';

Future<void> configureDependencies(GetIt sl) async {}

Continuing with the configuration of DI, for each package, we’re going to add the following code inside di/di.dart:

import 'package:utility/utility.dart';


Future<void> configureDependencies(GetIt sl) async {
//Bloc
sl.registerLazySingleton(() => YourBloc(sl()));

//Use cases
sl.registerLazySingleton(() => UseCase(sl()));

//Mappers
sl.registerLazySingleton(() => Mapper(sl()));

//Repository
sl.registerLazySingleton<Repository>(
() => RepositoryImpl(
sl(),
sl(),
),
);

// Api Client
sl.registerLazySingleton(
() => ApiClient(sl()),
);
}

Within the configureDependencies() function we will register all the dependencies that are necessary in each module.

Then, we export this configureDependencies() function in each package, for example for home/lib/home.dart :

library home;

export 'di/injection_container.dart' show configureDependencies;

Finally, we make it all come together in app/di/injection_container.dart:

import 'package:catalog/catalog.dart' as catalog;
import 'package:home/home.dart' as home;
import 'package:utility/utility.dart' as utility;

final sl = utility.GetIt.instance;

Future<void> configureDependencies() async {
await utility.configureDependencies(sl);
await catalog.configureDependencies(sl);
await home.configureDependencies(sl);
}

We can see we’re calling each of our package’s configureDependencies functions which we just exported. We’re aliasing the imports with the as keyword to avoid naming conflicts.

One last step is we call our app‘s configureDependencies function in app/lib/main.dart:

import 'package:flutter/material.dart';

import 'app/di/injection_container.dart' as di;
import 'app/src/app.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await di.configureDependencies();
runApp(const MyApp());
}

Using our packages in the Flutter app

To access the dependencies:

import 'package:flutter/material.dart';
import 'package:home/home.dart';
import 'package:star_wars/app/di/injection_container.dart';
import 'package:ui/ui.dart';
import 'package:utility/utility.dart';

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<HomeBloc>(),
child: MaterialApp(
restorationScopeId: 'app',
home: Scaffold(
body: const HomePage(),
),
),
);
}
}

In the next article, we will look at the process of generating code coverage for the whole project.

If you like it, you can Buy Me A Coffee!

--

--

Alvaro Armijos

Electronic and Telecommunications Engineer | #Flutter Developer 💙 | Always eager to learn | https://www.linkedin.com/in/alvaro-armijos-sarango/