Dependency Inversion is one of the five software design principles commonly known as SOLID principles that were defined by Robert C. Martin aka Uncle Bob. The most common way of implementing it is through dependency injection, however there are other technics that can be used to achieve dependency inversion. The main purpose of DI is to ensure that high-level modules are reusable and are not affected by changes in low-level modules. For a detailed explanation of dependency inversion and SOLID software design principles in OOP I recommend you go through this article Dependency Injection. In this tutorial we shall only look at dependency injection technic of achieving dependency inversion.
The code for this tutorial is extracted from an offline music player app. This will give you a real-world example of how dependency injection is applied and used. The app is designed following the hexagonal architecture which allows you to write modularised code that is easy to maintain, test, and scale. The source code of the entire application is hosted at https://github.com/paulkavule and for details about the hexagonal architecture, please follow this link Hexagonal architecture
2 packages are used to implement dependency injection,
1. Get_It
2. Injectable
- Builder_runner
- Hive_generator
- Injectable_generator
- Hive
- Hive flutter
The hexagonal design pattern specifies that communication between the different layers of the application takes place through ports and adapters. The ports define the rules that should be adhered to for communication to happen — basically rules of engagement. In dart we can define ports using abstract classes, in other object-oriented languages like c# and Java, you can use interfaces to achieve the same.
We are going to extract the Song domain class from this project https://github.com/paulkavule and build the logic required to demonstrate how to implement DI in dart.
Open your pubspec.yaml and add the highlighted lines.
dependencies:
flutter:
sdk: flutter
get: ^4.6.5
get_it: ^7.2.0
injectable: ^2.1.0
uuid: ^3.0.7
hive: ^2.2.3
hive_flutter: ^1.1.0
shared_preferences: ^2.0.15
dev_dependencies:
flutter_test:
sdk: flutter
hive_generator
injectable_generator:
build_runner: ^2.3.3
Setting up for dependency injection.
Next, we need to configure the dependency injection container. Go to the root of your lib folder and create a dart file for this purpose. You can give it a name of your choice, I am calling mine dicontainer.dart.
Copy and paste the code below.
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'package:musicapp1/dicontainer.config.dart';
final getIT = GetIt.instance;
@InjectableInit()
Future<void> configureDependencies() async => await getIT.init();
Next, go to the main method in the main.dart file and add the following code.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureDependencies();
await Hive.initFlutter();
Hive.registerAdapter(SongAdapter());
await Hive.openBox<Song>('cpm_songsdb');
runApp(const ProviderScope(child: MyApp()));
}
Line number 3 will register objects that need to be accessed and registered before the application is spun up.
Configuring the services.
Consider the following domain class
Song.dart
part 'song_model.g.dart';
@HiveType(typeId: 1)
class Song {
@HiveField(0) final String title;
@HiveField(1) final String description;
@HiveField(2) final String url;
@HiveField(3) String coverUrl;
@HiveField(4) DateTime addDate = DateTime.now();
@HiveField(5) bool isFavourite;
@HiveField(6) List<String> playList = [];
@HiveField(7) String genera;
@HiveField(8) int id;
@HiveField(9) String uuid;
Song(
{required this.title, this.description, this.url, this.coverUrl,
this.isFavourite = false, this.genera = "", this.uuid = "", this.id = 0});
}
Out bound port
This will define the rules the domain uses to communicate with the surrounding layer(application layer)
isong_repository.dart
abstract class ISongRepository {
Future<void> saveSong(Song song);
Future<void> deleteSong(String uuid);
Future<void> updateSong(Song song);
Future<void> markAsFavourite(String uuid, bool matched);
Future<List<Song>> getSongs();
}
Inbound Port.
The inbound port defines rules for communicating with the song domain class.
isong_service.dart
abstract class ISongService {
Future<void> saveSong(Song song);
Future<void> markUnmarkAsFavourite(String uuid, bool matched);
Future<List<Song>> getMostRecentSongs({int count = 100});
Future<List<Song>> getFavouriteSongs();
Future<List<Song>> getSongs();
Future<void> deleteSong(String uuid);
}
Adapter implementation
The adapters implement the rules defined by ports.
song_repository.dart
@Injectable(as: ISongRepository)
class SongRepository implements ISongRepository {
Box<Song> songTb = Hive.box('cpm_songsdb');
SongRepository();
@override
Future<void> saveSong(Song song) async {
await songTb.put(song.uuid, song);
}
@override
Future<void> deleteSong(String uuid) async => songTb.delete(uuid);
@override
Future<List<Song>> getSongs() async => songTb.values.toList();
@override
Future<void> updateSong(Song song) async {
songTb.delete(song.uuid);
await songTb.put(song.uuid, song);
}
@override
Future<void> markAsFavourite(String uuid, bool matched) async {
var song = songTb.values.firstWhere((sg) => sg.uuid == uuid);
song.isFavourite = matched;
songTb.put(uuid, song);
}
}.
Explanation.
Please take note of the line below
@Injectable(as: ISongRepository)
This annotation when applied to a class informs injectable_runner that it should generate the code required to support dependency for the class in question.
The above implementation of the ISongRepository port uses Hive database, the implementation can be swapped with implementation with SQLite database without making any modification in the application or presentation layer. This also makes testing very easy, because it can be swapped with a dummy implementation of the port i.e it is easily
mockable.
song_service.dart
@Injectable(as: ISongService)
class SongService implements ISongService {
final songRepo = getIT<ISongRepository>();
final prefs = getIT<SharedPreferences>();
SongService() {
var loaded = prefs.getBool(LoadStatus.songsLoaded) ?? false;
if (loaded == false) {
loadInitialSongs();
prefs.setBool(LoadStatus.songsLoaded, true);
}
}
@override
Future<void> saveSong(Song song) async {
song.uuid = const Uuid().v4();
song.addDate = DateTime.now();
await songRepo.saveSong(song);
}
@override
Future<void> markUnmarkAsFavourite(String uuid, bool matched) =>
songRepo.markAsFavourite(uuid, matched);
@override
Future<List<Song>> getMostRecentSongs({int count = 1000000}) async {
var songs = await songRepo.getSongs();
songs = songs.where((sg) => File(sg.url).isValid()).take(count).toList();
songs.sort((s1, s2) => s1.addDate.compareTo(s2.addDate));
return songs;
}
@override
Future<List<Song>> getFavouriteSongs() async {
var list = await songRepo.getSongs();
return list.where((sg) => sg.isFavourite).toList();
}
@override
Future<List<Song>> getSongs() async => await songRepo.getSongs();
@override
Future<void> deleteSong(String uuid, String filePath) async {
await songRepo.deleteSong(uuid);
File(filePath).deleteSync();
}
}
Explanation.
Just like in song_repository.dart, we decorate the SongService adapter with @injectable annotation to tell the dependency injection generator that it is an injectable class. The as: IsongService part tells the dependency injector to reference ISongService as the abstraction of SongService.
On line 3, we inject the ISongRepository class into the SongService to provide the linkage between the application layer and the domain layer. When doing the injection, we are telling the dependency injection container to give us the implementation that is tagged to the ISongRespository. So whichever implementation is present at the moment will be provided. This ensures that in case the implementation of the port changes for any reason this class will not be touched. Hence we are nailing the open/close principle in the SOLID principles on the head as well.
Next, we need to run the command below to generate the required code for dependency
injection and hive adapter. Builder runner comes in handy here, it’s a powerful package that will do the heavy lifting for us in conjunction with injectable_generator and hive_generator.
flutter pub run build_runner build - delete-conflicting-outputs.
Conclusion.
Everything is tied together in the Song Service class. We see how the injection is done (lines 2 and 4) and how the injection is set up in class (line 2). In the configuration section, we see how the dependency container is set up at a global level.
Feel free to check out the documentation of Get_IT for more use cases.
get_it | Dart Package (pub.dev)
Top comments (0)