How to create a Modular Application with Flutter — Part 2
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.
Here you can find the complete example:
If you like it, you can Buy Me A Coffee!