Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/melbournedeveloper/ioc_container

A lightweight, flexible, and high-performance dependency injection and service location library for Dart and Flutter
https://github.com/melbournedeveloper/ioc_container

dart dependency-injection flutter mobiledevelopment

Last synced: 12 days ago
JSON representation

A lightweight, flexible, and high-performance dependency injection and service location library for Dart and Flutter

Awesome Lists containing this project

README

        

# ioc_container
A lightweight, flexible, and high-performance dependency injection and service location library for Dart and Flutter.

Version 2 of the library introduces the groundbreaking [async locking](#v2-and-async-locking) feature for singletons, a feature that's set to revolutionize the way you handle asynchronous initialization in Dart and Flutter! ioc_container is the only known container that offers this feature.

![ioc_container](https://github.com/MelbourneDeveloper/ioc_container/raw/main/images/ioc_container-256x256.png)

![example workflow](https://github.com/MelbourneDeveloper/ioc_container/actions/workflows/build_and_test.yml/badge.svg)

codecov

### Contents

[Introduction](#introduction)

[Dependency Injection](#dependency-injection-di)

[Version 2 and Async Locking](#v2-and-async-locking)

[Why Use This Library?](#why-use-this-library)

[Performance And Simplicity](#performance-and-simplicity)

[Installation](#installation)

[Getting Started](#getting-started)

[Flutter](#flutter)

[Scoping and Disposal](#scoping-and-disposal)

[Testing](#testing)

[Add Firebase](#add-firebase)

[Inspired By .NET](#inspired-by-net)

[Extension Methods](#extension-methods)

## Introduction

Containers and service locators give you an easy way to lazily create the dependencies that your app requires. As your app grows in complexity, you will find that static variables or global factories start to become cumbersome and error-prone. Containers give you a consistent approach to managing the lifespan of your dependencies and make it easy to replace services with mocks for testing. ioc_container embraces the [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) pattern, and offers an approach that is standard across programming languages and frameworks. The implementation of this approach transcends Dart or Flutter. It is a proven and reliable method employed by developers across various technologies for well over a decade.

## Dependency Injection (DI)
[Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) (DI) allows you to decouple concrete classes from the rest of your application. Your code can depend on abstractions instead of concrete classes. It allows you to easily swap out implementations without changing your code. This is great for testing, and it makes your code more flexible. You can use test doubles in your tests, so they run quickly and reliably.

## V2 and Async Locking

Imagine a scenario where you need to initialize a service, like Firebase, connect to a database, or perhaps fetch some initial configuration data. These operations are asynchronous, and in a complex app, there's always a risk of inadvertently initializing the service multiple times, leading to redundant operations, wasted resources, and potential bugs.

Enter async locking: With this feature, you can perform your async initialization with the confidence that it will only ever run once. No matter how many times you request the service, the initialization logic is executed just a single time. This is not just about efficiency; it's about ensuring the consistency and reliability of your services.

Version 2 brings this powerful new feature. This is perfect for initializing Firebase, connecting to a database, or any other async initialization work. You can initialize anywhere in your code and not worry that it might happen again. Furthermore, the singleton never gets added to the container until the initialization completes successfully. This means that you can retry as many times as necessary without the container holding on to a service in an invalid state.

Notice that this example calls the initialization method three times. However, it doesn't run the work three times. It only runs once. The first call to `getAsync()` starts the initialization work. The second and third calls to `getAsync()` wait for the initialization to complete.

```Dart
import 'dart:async';
import 'package:ioc_container/ioc_container.dart';

class ConfigurationService {
Map? _configData;
int initCount = 0;

Future initialize() async {
print('Fetching configuration data from remote server...');
// Simulate network delay
await Future.delayed(const Duration(seconds: 2));
_configData = {
'apiEndpoint': 'https://api.example.com',
'apiKey': '1234567890',
};
print('Configuration data fetched!');
initCount++;
}

String get apiEndpoint => _configData!['apiEndpoint']!;
String get apiKey => _configData!['apiKey']!;
}

void main() async {
final builder = IocContainerBuilder()
..addSingletonAsync((container) async {
final service = ConfigurationService();
await service.initialize();
return service;
});

final container = builder.toContainer();
final stopwatch = Stopwatch()..start();
// Multiple parts of the application trying to initialize the service
// simultaneously
final services = await Future.wait([
container.getAsync(),
container.getAsync(),
container.getAsync(),
]);

stopwatch.stop();

print('API Endpoint: ${services.first.apiEndpoint}');
print('API Key: ${services.first.apiKey}');
print('Milliseconds spent: ${stopwatch.elapsedMilliseconds}');
print('Init Count: ${services.first.initCount}');
}
```

You can do initialization work when instantiating an instance of your service. Use `addAsync()` or `addSingletonAsync()` to register the services. When you need an instance, call the `getAsync()` method instead of `get()`.

Check out the [retry package](https://pub.dev/packages/retry) to add resiliency to your app. Check out the [Flutter example](https://github.com/MelbourneDeveloper/ioc_container/blob/f92bb3bd03fb3e3139211d0a8ec2474a737d7463/example/lib/main.dart#L74) that displays a progress indicator until the initialization completes successfully.

## Why Use This Library?
This library makes it easy to
- Easily replace services with mocks for testing
- Configure the lifecycle of your services for singleton (one per app) or transient (always fresh)
- Access factories for other services from any factory
- Perform async initialization work inside the factories
- Create a scope for a set of services that you can dispose of together
- Perform lazy initialization of services
- It's standard. It aims at being a standard dependency injector so anyone who understands DI can use this library.

### Performance and Simplicity
This library is objectively fast and holds up to comparable libraries in terms of performance. These [benchmarks](https://github.com/MelbourneDeveloper/ioc_container/tree/main/benchmarks) are currently out of data for v2 beta but new benchmarks and performance options are coming.

The [source code](https://github.com/MelbourneDeveloper/ioc_container/blob/main/lib/ioc_container.dart) is a fraction of the size of similar libraries and has no dependencies. According to [codecov](https://app.codecov.io/gh/melbournedeveloper/ioc_container), it weighs in at 81 lines of code, which makes it the lightest container I know about. It is stable and has 100% test coverage. At least three apps in the stores use this library in production.

Most importantly, it has no external dependencies so you don't have to worry about it pulling down packages you don't need.

You can copy/paste it anywhere, including Dartpad (as long as you follow the license), and it's simple enough to understand and change if you find an issue. Global factories get complicated when you need to manage the lifecycle of your services or replace services for testing. This library solves that problem.

## Installation

Run this command:

With Dart:

`$ dart pub add ioc_container`

With Flutter:

`$ flutter pub add ioc_container`

This will add a line like this to your package's `pubspec.yaml` (and run an implicit dart pub get):

```yaml
dependencies:
ioc_container: ^2.0.0-beta ## Or, latest version
```

## Getting Started
This example registers a singleton and two transient dependencies to the container.

```dart
import 'package:ioc_container/ioc_container.dart';

// These are some example services

class AuthenticationService {
String login(String username, String password) {
// Implement your authentication logic here
return 'Logged in';
}
}

class UserService {
final AuthenticationService _authenticationService;

UserService(this._authenticationService);

String getUserDetails() {
// Implement your user details retrieval logic here
return 'User Details';
}
}

class ProductService {
List getProducts() {
// Implement your product retrieval logic here
return ['Product 1', 'Product 2', 'Product 3'];
}
}

void main() {
// Create a container builder and register your services
final builder = IocContainerBuilder()
//The app only has one AuthenticationService for the lifespan of the app (Singleton)
..addSingleton((container) => AuthenticationService())
//We create a new UserService/ProductService for each usage
..add((container) => UserService(
//This is shorthand for container.get()
container()
))
..add((container) => ProductService());

// Build the container
final container = builder.toContainer();

// Retrieve your services from the container
final authService = container();
final userService = container();
final productService = container();

// Use the services
print(authService.login('user', 'password'));
print(userService.getUserDetails());
print(productService.getProducts());
}
```

We define the services: `AuthenticationService`, `UserService`, and `ProductService`. Then, we create an `IocContainerBuilder` and register these services using [`addSingleton()`](https://pub.dev/documentation/ioc_container/latest/ioc_container/IocContainerBuilder/addSingleton.html) and [`add()`](https://pub.dev/documentation/ioc_container/latest/ioc_container/IocContainerBuilder/add.html) methods. Finally, we build the container and retrieve the services to use them in our application like this: `container()`.

## Flutter
You can use ioc_container as a service locator by declaring a global instance and using it anywhere. This is a good alternative to get_it. You can access it inside or outside the widget tree. Or, you can use the [flutter_ioc_container](https://pub.dev/packages/flutter_ioc_container) package to add your container to the widget tree as an [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). This is a good alternative to Provider, which can get complicated when you need to manage the lifecycle of your services or replace services for testing.

Here is a Flutter example that uses a container as a service locator. You can also see the Flutter pub dev example app [here](https://pub.dev/packages/ioc_container/example).

```dart
import 'package:flutter/material.dart';
import 'package:ioc_container/ioc_container.dart';

class NotificationService {
void sendEmail(String email, String message) {
// Implement your email sending logic here
print('Email sent to $email: $message');
}
}

class OrderService {
void placeOrder(String item, int quantity, String email) {
final notificationService = serviceLocator();
// Implement your order placement logic here
print('Order placed for $quantity x $item');
notificationService.sendEmail(
email, 'Order confirmation for $quantity x $item');
}
}

class InventoryService {
List getAvailableItems() {
// Implement your inventory retrieval logic here
return ['Item 1', 'Item 2', 'Item 3'];
}
}

// Create a builder so we can replace dependencies later
final IocContainerBuilder builder = IocContainerBuilder(allowOverrides: true)
..addSingleton((container) => NotificationService())
..add((container) => OrderService())
..addSingleton((container) => InventoryService());

// Create a global service locator instance
late final IocContainer serviceLocator;

void main() {
serviceLocator = builder.toContainer();
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
//It's safe to use the service locator here in a StatelessWidget
//because the InventoryService is a singleton
final inventoryService = serviceLocator();
final availableItems = inventoryService.getAvailableItems();

return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'IoC Container Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
appBar: AppBar(title: const Text('IoC Container Demo')),
body: ListView.builder(
itemCount: availableItems.length,
itemBuilder: (context, index) {
final item = availableItems[index];
return ListTile(
title: Text(item),
trailing: ElevatedButton(
onPressed: () {
serviceLocator()
.placeOrder(item, 1, '[email protected]');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Order placed for $item')),
);
},
child: const Text('Order'),
),
);
},
),
),
);
}
}
```

The Flutter app above defines three services (`NotificationService`, `OrderService`, and `InventoryService`) and registers them in the container using the `builder`. We create the `serviceLocator` to access these services as needed in the application. In the `StatelessWidget` `MyApp`, we use the `InventoryService` to retrieve available items, and the `OrderService` to place an order, which in turn uses the `NotificationService` to send an email.

Check out the Flutter [widget tests](example/test/widget_test.dart) for the example app

## Scoping and Disposal
You might require scoping and disposal when working with dependencies that require proper cleanup. Scoping refers to limiting the lifespan of resources or objects to a specific block of code or function. This prevents unintended access or manipulation. Disposal ensures that we properly release resources or objects after we use them. This can be important for memory management to prevent resource leaks but is often not necessary for common Dart and Flutter objects that the garbage collector will destroy for you.

A scoped container does not create more than one object instance of each registration. Even if you get the service twice, the same instance will be returned. This example demonstrates a typical case where you may need to dispose of a database connection.

```dart
import 'package:ioc_container/ioc_container.dart';

class DatabaseConnection {
final String connectionString;

DatabaseConnection(this.connectionString);

void open() {
print('Opening database connection');
}

void close() {
print('Closing database connection');
}
}

class UserRepository {
final DatabaseConnection _databaseConnection;

UserRepository(this._databaseConnection);

List getUsers() {
_databaseConnection.open();
print('Fetching users from the database');
return ['User 1', 'User 2'];
}

void dispose() {
_databaseConnection.close();
}
}

void main() async {
final builder = IocContainerBuilder()
..add((container) => DatabaseConnection('my-connection-string'))
..add(
(container) => UserRepository(container()),
dispose: (userRepository) => userRepository.dispose(),
);

final container = builder.toContainer();

// Create a scope and use UserRepository within the scope
final scope = container.scoped();
final userRepository = scope();
print(userRepository.getUsers());

// Dispose the scope, which will close the database connection
await scope.dispose();
}
```

This example above defines a `DatabaseConnection` class that represents a connection to a database, and a `UserRepository` class that uses the `DatabaseConnection` to fetch user data. We use the container to manage the lifecycle of these services. We create an `IocContainerBuilder` to register the `DatabaseConnection` and `UserRepository`. We specify a `dispose` function for the `UserRepository` that will close the database connection when we dispose of the scope.

The main function creates a scope to retrieve the `UserRepository` from the scoped container. We fetch the user data and then dispose of the scope. Disposing of the scope will invoke the `dispose()` function for `UserRepository`, which in turn closes the DatabaseConnection.

*Note: all services in the scoped container exist for the lifespan of the scope. They act in a way that is similar to singletons, but when we call `dispose()` on the scope, it calls `dispose()` on each service registration.*

```dart
import 'package:ioc_container/ioc_container.dart';

class DatabaseService {
DatabaseService(this.connectionString);
final String connectionString;

Future init() async {
// Simulate async initialization, such as connecting to the database.
await Future.delayed(const Duration(milliseconds: 1500));
print('DatabaseService initialized');
return this;
}
}

class UserService {
UserService(this._dbService);
final DatabaseService _dbService;

Future init() async {
// Simulate async initialization, such as fetching user data.
await Future.delayed(const Duration(milliseconds: 1500));
print('UserService initialized');
return this;
}
}

void main() async {
final builder = IocContainerBuilder()
..addSingletonAsync(
(container) async => DatabaseService('connection_string').init(),
)
..addSingletonAsync(
(container) async =>
UserService(await container.getAsync()).init(),
);

final container = builder.toContainer();

print('Waiting for services to initialize at...${DateTime.now()}');

final userService = await container.getAsync();

print('Got initialized service at at...${DateTime.now()}');

// Use the userService instance for your application logic.
}
```

The example above uses a container to manage async initialization for two services: `DatabaseService` and `UserService`. It simulates time-consuming initialization tasks for each service. It uses `addSingletonAsync()` to register the services. When the `getAsync()` call completes, the app can use the `UserService` instance because the initialization is complete.

## Testing
We compose the container with a builder. You can replace services in the builder if the `allowOverrides` flag is set to true. This is useful for testing. Expose the builder in a location where the tests can access it, add new mock/fake registrations, and call `toContainer()` to get the container with test doubles.

```dart
import 'package:flutter/material.dart';
import 'package:ioc_container/ioc_container.dart';

abstract class AuthService {
Future authenticate(String username, String password);
}

class RealAuthService implements AuthService {
@override
Future authenticate(String username, String password) async {
// Your real authentication logic here.
return username == 'bob' && password == '123';
}
}

//We declare the builder and container as top level variables here just to make
//the example clearer
final builder = IocContainerBuilder(allowOverrides: true)
..addSingleton((container) => RealAuthService());

late IocContainer container;

void main() {
container = builder.toContainer();
runApp(const AppRoot());
}

class AppRoot extends StatelessWidget {
const AppRoot({
super.key,
});

@override
Widget build(BuildContext context) => const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: LoginScreen(),
),
);
}

class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});

@override
State createState() => _LoginScreenState();
}

class _LoginScreenState extends State {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: usernameController,
decoration: const InputDecoration(labelText: 'Username'),
),
TextField(
controller: passwordController,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
),
TextButton(
onPressed: () async {
final success = await container().authenticate(
usernameController.text,
passwordController.text,
);

await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(success ? 'Welcome' : 'Error'),
content: Text(
success ? 'Login Successful' : 'Invalid credentials',
),
),
);
},
child: const Text('Login'),
),
],
),
),
);
}
```

The code above defines a simple Flutter app with a login screen that uses an IoC container to manage its dependencies. The app has an `AuthService` to authenticate users, with the `RealAuthService` registered in the container. This is how we can mock the dependencies and replace the `RealAuthService` with `MockAuthService` in our tests.

```dart
import 'package:flutter/material.dart';
import 'package:flutter_application_9/main.dart';
import 'package:flutter_test/flutter_test.dart';

class MockAuthService implements AuthService {
@override
Future authenticate(String username, String password) async =>
username == 'test' && password == '1234';
}

void main() {
setUp(
() {
builder.addSingleton((container) => MockAuthService());
container = builder.toContainer();
},
);

testWidgets('Test LoginScreen with MockAuthService', (tester) async {
await tester.pumpWidget(const AppRoot());

// Enter correct credentials
await tester.enterText(find.byType(TextField).at(0), 'test');
await tester.enterText(find.byType(TextField).at(1), '1234');

// Find and tap the Login button
final loginButton = find.widgetWithText(TextButton, 'Login');
await tester.tap(loginButton);

await tester.pumpAndSettle();

// Find the AlertDialog
final alertDialog = find.byType(AlertDialog);

// Check if the AlertDialog is present
expect(alertDialog, findsOneWidget);

// Check if the AlertDialog displays the expected success message
final errorMessage = find.text('Login Successful');
expect(errorMessage, findsOneWidget);
});

testWidgets('Invalid login scenario', (tester) async {
await tester.pumpWidget(const AppRoot());

// Enter invalid credentials
await tester.enterText(find.byType(TextField).at(0), 'wrong_user');
await tester.enterText(find.byType(TextField).at(1), 'wrong_password');

// Find and tap the Login button
final loginButton = find.widgetWithText(TextButton, 'Login');
await tester.tap(loginButton);

await tester.pumpAndSettle();

// Find the AlertDialog
final alertDialog = find.byType(AlertDialog);

// Check if the AlertDialog is present
expect(alertDialog, findsOneWidget);

// Check if the AlertDialog displays the expected error message
final errorMessage = find.text('Invalid credentials');
expect(errorMessage, findsOneWidget);
});
}
```

These tests validate the login functionality of the app with fake authentication services. One test checks for a successful login scenario, ensuring the "Login Successful" message is displayed. The other test examines the invalid login scenario, verifying that the "Invalid credentials" error message appears.

Check out the Flutter [widget tests](example/test/widget_test.dart) for the example app

## Add Firebase
ioc_container makes accessing, initializing, and testing Firebase easy. Configure Firebase with the [official documentation](https://firebase.google.com/docs/flutter/setup?platform=ios), and make sure your `pubspec.yaml` has these dependencies.

- ioc_container
- firebase_core
- firebase_auth
- cloud_firestore

### Extension Method
Add this file

```Dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:ioc_container/ioc_container.dart';

///Extensions for wiring up FlutterFire. This adds
///[FirebaseApp], [FirebaseAuth], and [FirebaseFirestore] as singletons
extension FlutterFireExtensions on IocContainerBuilder {
void addFirebase() {
//These factories are all async because we need to ensure that Firebase is initialized
addSingletonAsync(
(container) {
//This is typically done at the start of the main() function.
//Be aware that this is being done to ensure that the Flutter engine is initialized before Firebase and never occurs twice
WidgetsFlutterBinding.ensureInitialized();

return Firebase.initializeApp(
options: container.get(),
);
},
);
addSingletonAsync(
(container) async => FirebaseAuth.instanceFor(
app: await container.getAsync(),
),
);
addSingletonAsync(
(container) async => FirebaseFirestore.instanceFor(
app: await container.getAsync(),
),
);
}
}
```

Call `addFirebase()` on your builder to add the factories to your composition and add your `FirebaseOptions`.

```dart
IocContainerBuilder compose() => IocContainerBuilder(allowOverrides: true)
..addFirebase()
//You must add your own FirebaseOptions to the composition
..addSingleton((container) => DefaultOptions(
apiKey: apiKey,
appId: appId,
projectId: projectId,
));
```

You can now get any Firebase dependencies from the container like this and be sure that it is initialized.

```dart
final firebaseFirestore = await container.getAsync();
```

### Testing
Replace the dependencies with fakes or mocks in your tests like this.

```dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:example_2/main.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_test/flutter_test.dart';
import '../firebase.dart';

void main() {
testWidgets('Testing with Firebase', (WidgetTester tester) async {
final builder = compose();

//TODO: Create mocks for Firebase or use a library like firestore_fakes to
//mock the dependencies

var fakeFirebaseFirestore = FirebaseFirestoreFake();

//TODO: Put fake data in fakeFirebaseFirestore here. The app will consume it.

builder
..addSingletonAsync((container) async => MockFirebaseAuth())
..addSingletonAsync(
(container) async => fakeFirebaseFirestore);

await tester.pumpWidget(MyApp(container: builder.toContainer()));

//TODO: Put your tests here
});
}
```

If you have any further issues, see the [FlutterFire documentation](https://firebase.flutter.dev/docs/overview/).

## Inspired By .NET

This library takes inspiration from DI in [.NET MAUI](https://learn.microsoft.com/en-us/dotnet/architecture/maui/dependency-injection) and [ASP .NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0). You register your dependencies with the `IocContainerBuilder` which is a bit like [`IServiceCollection`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection?view=dotnet-plat-ext-7.0) in ASP.NET Core. Then you build it with the `toContainer()` method, which is like the [`BuildServiceProvider()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectioncontainerbuilderextensions.buildserviceprovider?view=dotnet-plat-ext-6.0) method in ASP.NET Core. DI is an established pattern on which the whole .NET ecosystem and many other ecosystems depend. This library does not reinvent the wheel, it just makes it easy to use in Flutter and Dart.

## Extension Methods

Much of the functionality comes from extension methods. Extension methods are better because they don't pollute the core public interface. It is very easy to implement your own `IocContainer` because it only has 4 properties. You can add as many extension methods as you need. The library doesn't come with extensions that are not necessary.

For example, in version V1, there was a `addSingletonService` extension. This was removed in V2 because it is not necessary, but you can easily add it back for backwards compatibility or convenience. This is the extension:

```dart
///Add a singleton service to the container.
void addSingletonService(T service) => addServiceDefinition(
ServiceDefinition(
(container) => service,
isSingleton: true,
),
);
```

### Keyed Services

You may need to use keys to store multiple instances of the same type. You can use extensions to implement this functionality. This example demonstrates how to use extensions to add keyed services to ioc_container

```dart
import 'package:ioc_container/ioc_container.dart';
import 'package:test/test.dart';

///Example service
class BigService {
final String name;

BigService(this.name);
Future callApi() => Future.delayed(Duration(seconds: 1));

///We can check equality by the name(key)
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;

return other is BigService && other.name == name;
}

@override
int get hashCode => name.hashCode;
}

///These give us the functionality to add or
///access a service by key
extension KeyedExtensions on IocContainer {
T? keyedService(String key) => get>()[key];
void setServiceByKey(String key, T service) =>
get>()[key] = service;
}

void main() {
test('Keyed Services', () {
var count = 0;

final builder = (IocContainerBuilder()
..add((container) {
//Increments the name (key) of the service so they are unique
//Uuid would be better
count++;
var bigService = BigService(count.toString());
bigService;
container.setServiceByKey(count.toString(), bigService);
return bigService;
})
..addSingleton(
(container) => {},
));

final container = builder.toContainer();

final bigContainerOne = container();
final bigContainerTwo = container();
final bigContainerThree = container();

//Verifies the three names
expect(bigContainerOne.name, '1');
expect(bigContainerTwo.name, '2');
expect(bigContainerThree.name, '3');

//Verifies you can access these by key
expect(container.keyedService(bigContainerOne.name),
bigContainerOne);

expect(container.keyedService(bigContainerTwo.name),
bigContainerTwo);

expect(container.keyedService(bigContainerThree.name),
bigContainerThree);
});
}
```