Skip to content

Commit

Permalink
Merge pull request #12 from MelbourneDeveloper/asynclocking
Browse files Browse the repository at this point in the history
2.0.0-beta Release Prep
  • Loading branch information
MelbourneDeveloper authored Aug 29, 2023
2 parents 9f08c16 + 66cc7ba commit 313e32d
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 18 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,6 @@ longer has the @immutable annotation. See the documentation about immutability.
- Upgrade austerity
- Documentation changes and add icon
## 1.0.12
- Fix icon link
- Fix icon link
## 2.0.0-beta
- Async locking. See documentation
91 changes: 78 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# 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)
Expand All @@ -13,6 +15,8 @@ A lightweight, flexible, and high-performance dependency injection and service l

[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)
Expand All @@ -25,8 +29,6 @@ A lightweight, flexible, and high-performance dependency injection and service l

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

[Async Initialization](#async-initialization)

[Testing](#testing)

[Add Firebase](#add-firebase)
Expand All @@ -40,6 +42,71 @@ Containers and service locators give you an easy way to lazily create the depend
## 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<String, String>? _configData;
int initCount = 0;
Future<void> initialize() async {
print('Fetching configuration data from remote server...');
// Simulate network delay
await Future<void>.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<ConfigurationService>(),
container.getAsync<ConfigurationService>(),
container.getAsync<ConfigurationService>(),
]);
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
Expand All @@ -51,7 +118,7 @@ This library makes it easy to
- 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. See the [benchmarks](https://github.com/MelbourneDeveloper/ioc_container/tree/main/benchmarks) project and results.
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.

Expand All @@ -75,7 +142,7 @@ This will add a line like this to your package's `pubspec.yaml` (and run an impl

```yaml
dependencies:
ioc_container: ^1.0.9 ## Or, latest version
ioc_container: ^2.0.0-beta ## Or, latest version
```
## Getting Started
Expand Down Expand Up @@ -116,8 +183,11 @@ void main() {
final builder = IocContainerBuilder()
//The app only has one AuthenticationService for the lifespan of the app (Singleton)
..addSingletonService(AuthenticationService())
//We mint a new UserService/ProductService for each usage
..add((container) => UserService(container<AuthenticationService>()))
//We create a new UserService/ProductService for each usage
..add((container) => UserService(
//This is shorthand for container.get<AuthenticationService>()
container<AuthenticationService>()
))
..add((container) => ProductService());

// Build the container
Expand Down Expand Up @@ -296,13 +366,6 @@ The main function creates a scope to retrieve the `UserRepository` from the scop

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

## Async Initialization
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()`.

_Warning: if you get a singleton with `getAsync()` and the call fails, the singleton will always return a `Future` with an error for the lifespan of the container._ You may need to take extra precautions by wrapping the initialization in a try/catch and using a retry. You may need to eventually cancel the operation if retrying fails. For this reason, you should probably scope the container and only use the result in your main container once it succeeds.

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.

```dart
import 'package:ioc_container/ioc_container.dart';
Expand Down Expand Up @@ -550,6 +613,8 @@ extension FlutterFireExtensions on IocContainerBuilder {
//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(
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.0.12"
version: "2.0.0-beta"
js:
dependency: transitive
description:
Expand Down
4 changes: 3 additions & 1 deletion lib/ioc_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ class IocContainer {
///so the container can store scope or singletons
final Map<Type, Object> singletons;

// ignore: strict_raw_type, avoid_field_initializers_in_const_classes
///🔒 Map of locks by type. This ensures that no async singletons execute
///more than once, unless there is an error
// ignore: strict_raw_type
final Map<Type, AsyncLock> locks;

///⌖ If true, this container is a scoped container. Scoped containers never
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: ioc_container
description: A lightweight, flexible, and high-performance dependency injection and service location library for Dart and Flutter
version: 1.0.12
version: 2.0.0-beta
repository: https://github.com/MelbourneDeveloper/ioc_container

environment:
sdk: ">=2.17.0 <3.0.0"
sdk: ">=2.17.0 <4.0.0"

dev_dependencies:
austerity: ^1.1.0
Expand Down

0 comments on commit 313e32d

Please sign in to comment.