{"id":16352347,"url":"https://github.com/melbournedeveloper/ioc_container","last_synced_at":"2025-03-16T15:31:55.355Z","repository":{"id":60309985,"uuid":"494922904","full_name":"MelbourneDeveloper/ioc_container","owner":"MelbourneDeveloper","description":"A lightweight, flexible, and high-performance dependency injection and service location library for Dart and Flutter","archived":false,"fork":false,"pushed_at":"2024-01-24T19:57:34.000Z","size":627,"stargazers_count":64,"open_issues_count":2,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-11T11:10:02.088Z","etag":null,"topics":["dart","dependency-injection","flutter","mobiledevelopment"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/ioc_container","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MelbourneDeveloper.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-22T00:18:17.000Z","updated_at":"2025-01-22T10:41:57.000Z","dependencies_parsed_at":"2024-01-07T20:52:34.064Z","dependency_job_id":"ef63ecad-afbb-4fa9-998e-5ec86e36e24b","html_url":"https://github.com/MelbourneDeveloper/ioc_container","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MelbourneDeveloper%2Fioc_container","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MelbourneDeveloper%2Fioc_container/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MelbourneDeveloper%2Fioc_container/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MelbourneDeveloper%2Fioc_container/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MelbourneDeveloper","download_url":"https://codeload.github.com/MelbourneDeveloper/ioc_container/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243822312,"owners_count":20353496,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dart","dependency-injection","flutter","mobiledevelopment"],"created_at":"2024-10-11T01:25:49.665Z","updated_at":"2025-03-16T15:31:54.988Z","avatar_url":"https://github.com/MelbourneDeveloper.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ioc_container\nA lightweight, flexible, and high-performance dependency injection and service location library for Dart and Flutter.\n\nVersion 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.\n\n![ioc_container](https://github.com/MelbourneDeveloper/ioc_container/raw/main/images/ioc_container-256x256.png)\n\n![example workflow](https://github.com/MelbourneDeveloper/ioc_container/actions/workflows/build_and_test.yml/badge.svg)\n\n\u003ca href=\"https://codecov.io/gh/melbournedeveloper/ioc_container\"\u003e\u003cimg src=\"https://codecov.io/gh/melbournedeveloper/ioc_container/branch/main/graph/badge.svg\" alt=\"codecov\"\u003e\u003c/a\u003e\n\n### Contents\n\n[Introduction](#introduction)\n\n[Dependency Injection](#dependency-injection-di)\n\n[Version 2 and Async Locking](#v2-and-async-locking)\n\n[Why Use This Library?](#why-use-this-library)\n\n[Performance And Simplicity](#performance-and-simplicity)\n\n[Installation](#installation)\n\n[Getting Started](#getting-started)\n\n[Flutter](#flutter)\n\n[Scoping and Disposal](#scoping-and-disposal)\n\n[Testing](#testing)\n\n[Add Firebase](#add-firebase)\n\n[Inspired By .NET](#inspired-by-net)\n\n[Extension Methods](#extension-methods)\n\n## Introduction\n\nContainers 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.\n\n## Dependency Injection (DI)\n[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.\n\n## V2 and Async Locking\n\nImagine 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.\n\nEnter 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.\n\nVersion 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.\n\nNotice 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. \n\n```Dart\nimport 'dart:async';\nimport 'package:ioc_container/ioc_container.dart';\n\nclass ConfigurationService {\n  Map\u003cString, String\u003e? _configData;\n  int initCount = 0;\n\n  Future\u003cvoid\u003e initialize() async {\n    print('Fetching configuration data from remote server...');\n    // Simulate network delay\n    await Future\u003cvoid\u003e.delayed(const Duration(seconds: 2));\n    _configData = {\n      'apiEndpoint': 'https://api.example.com',\n      'apiKey': '1234567890',\n    };\n    print('Configuration data fetched!');\n    initCount++;\n  }\n\n  String get apiEndpoint =\u003e _configData!['apiEndpoint']!;\n  String get apiKey =\u003e _configData!['apiKey']!;\n}\n\nvoid main() async {\n  final builder = IocContainerBuilder()\n    ..addSingletonAsync((container) async {\n      final service = ConfigurationService();\n      await service.initialize();\n      return service;\n    });\n\n  final container = builder.toContainer();\n  final stopwatch = Stopwatch()..start();\n  // Multiple parts of the application trying to initialize the service\n  // simultaneously\n  final services = await Future.wait([\n    container.getAsync\u003cConfigurationService\u003e(),\n    container.getAsync\u003cConfigurationService\u003e(),\n    container.getAsync\u003cConfigurationService\u003e(),\n  ]);\n\n  stopwatch.stop();\n\n  print('API Endpoint: ${services.first.apiEndpoint}');\n  print('API Key: ${services.first.apiKey}');\n  print('Milliseconds spent: ${stopwatch.elapsedMilliseconds}');\n  print('Init Count: ${services.first.initCount}');\n}\n```\n\nYou 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()`. \n\nCheck 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.\n\n## Why Use This Library?\nThis library makes it easy to\n- Easily replace services with mocks for testing\n- Configure the lifecycle of your services for singleton (one per app) or transient (always fresh)\n- Access factories for other services from any factory\n- Perform async initialization work inside the factories\n- Create a scope for a set of services that you can dispose of together\n- Perform lazy initialization of services\n- It's standard. It aims at being a standard dependency injector so anyone who understands DI can use this library.\n\n### Performance and Simplicity\nThis 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. \n\nThe [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.\n\nMost importantly, it has no external dependencies so you don't have to worry about it pulling down packages you don't need.\n\nYou 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.\n\n## Installation\n\nRun this command:\n\nWith Dart:\n\n `$ dart pub add ioc_container`\n\nWith Flutter:\n\n `$ flutter pub add ioc_container`\n\nThis will add a line like this to your package's `pubspec.yaml` (and run an implicit dart pub get):\n\n```yaml\ndependencies:\n  ioc_container: ^2.0.0-beta ## Or, latest version\n```\n\n## Getting Started\nThis example registers a singleton and two transient dependencies to the container. \n\n```dart\nimport 'package:ioc_container/ioc_container.dart';\n\n// These are some example services\n\nclass AuthenticationService {\n  String login(String username, String password) {\n    // Implement your authentication logic here\n    return 'Logged in';\n  }\n}\n\nclass UserService {\n  final AuthenticationService _authenticationService;\n\n  UserService(this._authenticationService);\n\n  String getUserDetails() {\n    // Implement your user details retrieval logic here\n    return 'User Details';\n  }\n}\n\nclass ProductService {\n  List\u003cString\u003e getProducts() {\n    // Implement your product retrieval logic here\n    return ['Product 1', 'Product 2', 'Product 3'];\n  }\n}\n\nvoid main() {\n  // Create a container builder and register your services\n  final builder = IocContainerBuilder()\n    //The app only has one AuthenticationService for the lifespan of the app (Singleton)\n    ..addSingleton((container) =\u003e AuthenticationService())\n    //We create a new UserService/ProductService for each usage\n    ..add((container) =\u003e UserService(\n      //This is shorthand for container.get\u003cAuthenticationService\u003e()\n      container\u003cAuthenticationService\u003e()\n      ))\n    ..add((container) =\u003e ProductService());\n\n  // Build the container\n  final container = builder.toContainer();\n\n  // Retrieve your services from the container\n  final authService = container\u003cAuthenticationService\u003e();\n  final userService = container\u003cUserService\u003e();\n  final productService = container\u003cProductService\u003e();\n\n  // Use the services\n  print(authService.login('user', 'password'));\n  print(userService.getUserDetails());\n  print(productService.getProducts());\n}\n```\n\nWe 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\u003cProductService\u003e()`.\n\n## Flutter\nYou 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. \n\nHere 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).\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:ioc_container/ioc_container.dart';\n\nclass NotificationService {\n  void sendEmail(String email, String message) {\n    // Implement your email sending logic here\n    print('Email sent to $email: $message');\n  }\n}\n\nclass OrderService {\n  void placeOrder(String item, int quantity, String email) {\n    final notificationService = serviceLocator\u003cNotificationService\u003e();\n    // Implement your order placement logic here\n    print('Order placed for $quantity x $item');\n    notificationService.sendEmail(\n        email, 'Order confirmation for $quantity x $item');\n  }\n}\n\nclass InventoryService {\n  List\u003cString\u003e getAvailableItems() {\n    // Implement your inventory retrieval logic here\n    return ['Item 1', 'Item 2', 'Item 3'];\n  }\n}\n\n// Create a builder so we can replace dependencies later\nfinal IocContainerBuilder builder = IocContainerBuilder(allowOverrides: true)\n  ..addSingleton((container) =\u003e NotificationService())\n  ..add((container) =\u003e OrderService())\n  ..addSingleton((container) =\u003e InventoryService());\n\n// Create a global service locator instance\nlate final IocContainer serviceLocator;\n\nvoid main() {\n  serviceLocator = builder.toContainer();\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    //It's safe to use the service locator here in a StatelessWidget\n    //because the InventoryService is a singleton\n    final inventoryService = serviceLocator\u003cInventoryService\u003e();\n    final availableItems = inventoryService.getAvailableItems();\n\n    return MaterialApp(\n      debugShowCheckedModeBanner: false,\n      title: 'IoC Container Demo',\n      theme: ThemeData(\n        primarySwatch: Colors.blue,\n        visualDensity: VisualDensity.adaptivePlatformDensity,\n      ),\n      home: Scaffold(\n        appBar: AppBar(title: const Text('IoC Container Demo')),\n        body: ListView.builder(\n          itemCount: availableItems.length,\n          itemBuilder: (context, index) {\n            final item = availableItems[index];\n            return ListTile(\n              title: Text(item),\n              trailing: ElevatedButton(\n                onPressed: () {\n                  serviceLocator\u003cOrderService\u003e()\n                      .placeOrder(item, 1, 'customer@example.com');\n                  ScaffoldMessenger.of(context).showSnackBar(\n                    SnackBar(content: Text('Order placed for $item')),\n                  );\n                },\n                child: const Text('Order'),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n```\n\nThe 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.\n\nCheck out the Flutter [widget tests](example/test/widget_test.dart) for the example app\n\n## Scoping and Disposal\nYou 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.\n\nA 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.\n\n```dart\nimport 'package:ioc_container/ioc_container.dart';\n\nclass DatabaseConnection {\n  final String connectionString;\n\n  DatabaseConnection(this.connectionString);\n\n  void open() {\n    print('Opening database connection');\n  }\n\n  void close() {\n    print('Closing database connection');\n  }\n}\n\nclass UserRepository {\n  final DatabaseConnection _databaseConnection;\n\n  UserRepository(this._databaseConnection);\n\n  List\u003cString\u003e getUsers() {\n    _databaseConnection.open();\n    print('Fetching users from the database');\n    return ['User 1', 'User 2'];\n  }\n\n  void dispose() {\n    _databaseConnection.close();\n  }\n}\n\nvoid main() async {\n  final builder = IocContainerBuilder()\n    ..add((container) =\u003e DatabaseConnection('my-connection-string'))\n    ..add\u003cUserRepository\u003e(\n      (container) =\u003e UserRepository(container\u003cDatabaseConnection\u003e()),\n      dispose: (userRepository) =\u003e userRepository.dispose(),\n    );\n\n  final container = builder.toContainer();\n\n  // Create a scope and use UserRepository within the scope\n  final scope = container.scoped();\n  final userRepository = scope\u003cUserRepository\u003e();\n  print(userRepository.getUsers());\n\n  // Dispose the scope, which will close the database connection\n  await scope.dispose();\n}\n```    \n\nThis 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.\n\nThe 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.\n\n*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.*\n\n```dart\nimport 'package:ioc_container/ioc_container.dart';\n\nclass DatabaseService {\n  DatabaseService(this.connectionString);\n  final String connectionString;\n\n  Future\u003cDatabaseService\u003e init() async {\n    // Simulate async initialization, such as connecting to the database.\n    await Future\u003cvoid\u003e.delayed(const Duration(milliseconds: 1500));\n    print('DatabaseService initialized');\n    return this;\n  }\n}\n\nclass UserService {\n  UserService(this._dbService);\n  final DatabaseService _dbService;\n\n  Future\u003cUserService\u003e init() async {\n    // Simulate async initialization, such as fetching user data.\n    await Future\u003cvoid\u003e.delayed(const Duration(milliseconds: 1500));\n    print('UserService initialized');\n    return this;\n  }\n}\n\nvoid main() async {\n  final builder = IocContainerBuilder()\n    ..addSingletonAsync(\n      (container) async =\u003e DatabaseService('connection_string').init(),\n    )\n    ..addSingletonAsync(\n      (container) async =\u003e\n          UserService(await container.getAsync\u003cDatabaseService\u003e()).init(),\n    );\n\n  final container = builder.toContainer();\n\n  print('Waiting for services to initialize at...${DateTime.now()}');\n\n  final userService = await container.getAsync\u003cUserService\u003e();\n\n  print('Got initialized service at at...${DateTime.now()}');\n  \n  // Use the userService instance for your application logic.\n}\n```\n\nThe 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.\n\n## Testing\nWe 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.\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:ioc_container/ioc_container.dart';\n\nabstract class AuthService {\n  Future\u003cbool\u003e authenticate(String username, String password);\n}\n\nclass RealAuthService implements AuthService {\n  @override\n  Future\u003cbool\u003e authenticate(String username, String password) async {\n    // Your real authentication logic here.\n    return username == 'bob' \u0026\u0026 password == '123';\n  }\n}\n\n//We declare the builder and container as top level variables here just to make\n//the example clearer\nfinal builder = IocContainerBuilder(allowOverrides: true)\n  ..addSingleton\u003cAuthService\u003e((container) =\u003e RealAuthService());\n\nlate IocContainer container;\n\nvoid main() {\n  container = builder.toContainer();\n  runApp(const AppRoot());\n}\n\nclass AppRoot extends StatelessWidget {\n  const AppRoot({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) =\u003e const MaterialApp(\n        debugShowCheckedModeBanner: false,\n        home: Scaffold(\n          body: LoginScreen(),\n        ),\n      );\n}\n\nclass LoginScreen extends StatefulWidget {\n  const LoginScreen({super.key});\n\n  @override\n  State\u003cLoginScreen\u003e createState() =\u003e _LoginScreenState();\n}\n\nclass _LoginScreenState extends State\u003cLoginScreen\u003e {\n  final usernameController = TextEditingController();\n  final passwordController = TextEditingController();\n  @override\n  Widget build(BuildContext context) =\u003e Scaffold(\n        body: Center(\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              TextField(\n                controller: usernameController,\n                decoration: const InputDecoration(labelText: 'Username'),\n              ),\n              TextField(\n                controller: passwordController,\n                decoration: const InputDecoration(labelText: 'Password'),\n                obscureText: true,\n              ),\n              TextButton(\n                onPressed: () async {\n                  final success = await container\u003cAuthService\u003e().authenticate(\n                    usernameController.text,\n                    passwordController.text,\n                  );\n\n                  await showDialog\u003cbool\u003e(\n                    context: context,\n                    builder: (context) =\u003e AlertDialog(\n                      title: Text(success ? 'Welcome' : 'Error'),\n                      content: Text(\n                        success ? 'Login Successful' : 'Invalid credentials',\n                      ),\n                    ),\n                  );\n                },\n                child: const Text('Login'),\n              ),\n            ],\n          ),\n        ),\n      );\n}\n```\n\nThe 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.\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:flutter_application_9/main.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass MockAuthService implements AuthService {\n  @override\n  Future\u003cbool\u003e authenticate(String username, String password) async =\u003e\n      username == 'test' \u0026\u0026 password == '1234';\n}\n\nvoid main() {\n  setUp(\n    () {\n      builder.addSingleton\u003cAuthService\u003e((container) =\u003e MockAuthService());\n      container = builder.toContainer();\n    },\n  );\n\n  testWidgets('Test LoginScreen with MockAuthService', (tester) async {\nawait tester.pumpWidget(const AppRoot());\n\n    // Enter correct credentials\n    await tester.enterText(find.byType(TextField).at(0), 'test');\n    await tester.enterText(find.byType(TextField).at(1), '1234');\n\n    // Find and tap the Login button\n    final loginButton = find.widgetWithText(TextButton, 'Login');\n    await tester.tap(loginButton);\n\n    await tester.pumpAndSettle();\n\n    // Find the AlertDialog\n    final alertDialog = find.byType(AlertDialog);\n\n    // Check if the AlertDialog is present\n    expect(alertDialog, findsOneWidget);\n\n    // Check if the AlertDialog displays the expected success message\n    final errorMessage = find.text('Login Successful');\n    expect(errorMessage, findsOneWidget);\n  });\n\n  testWidgets('Invalid login scenario', (tester) async {\n    await tester.pumpWidget(const AppRoot());\n\n    // Enter invalid credentials\n    await tester.enterText(find.byType(TextField).at(0), 'wrong_user');\n    await tester.enterText(find.byType(TextField).at(1), 'wrong_password');\n\n    // Find and tap the Login button\n    final loginButton = find.widgetWithText(TextButton, 'Login');\n    await tester.tap(loginButton);\n\n    await tester.pumpAndSettle();\n\n    // Find the AlertDialog\n    final alertDialog = find.byType(AlertDialog);\n\n    // Check if the AlertDialog is present\n    expect(alertDialog, findsOneWidget);\n\n    // Check if the AlertDialog displays the expected error message\n    final errorMessage = find.text('Invalid credentials');\n    expect(errorMessage, findsOneWidget);\n  });\n}\n```\n\nThese 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.\n\nCheck out the Flutter [widget tests](example/test/widget_test.dart) for the example app\n\n## Add Firebase\nioc_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.\n\n- ioc_container\n- firebase_core\n- firebase_auth\n- cloud_firestore\n\n### Extension Method\nAdd this file\n\n```Dart\nimport 'package:cloud_firestore/cloud_firestore.dart';\nimport 'package:firebase_auth/firebase_auth.dart';\nimport 'package:firebase_core/firebase_core.dart';\nimport 'package:flutter/material.dart';\nimport 'package:ioc_container/ioc_container.dart';\n\n///Extensions for wiring up FlutterFire. This adds\n///[FirebaseApp], [FirebaseAuth], and [FirebaseFirestore] as singletons\nextension FlutterFireExtensions on IocContainerBuilder {\n  void addFirebase() {\n    //These factories are all async because we need to ensure that Firebase is initialized\n    addSingletonAsync(\n      (container) {\n        //This is typically done  at the start of the main() function.  \n        //Be aware that this is being done to ensure that the Flutter engine is initialized before Firebase and never occurs twice\n        WidgetsFlutterBinding.ensureInitialized();\n\n        return Firebase.initializeApp(\n          options: container.get\u003cFirebaseOptions\u003e(),\n        );\n      },\n    );\n    addSingletonAsync(\n      (container) async =\u003e FirebaseAuth.instanceFor(\n        app: await container.getAsync\u003cFirebaseApp\u003e(),\n      ),\n    );\n    addSingletonAsync(\n      (container) async =\u003e FirebaseFirestore.instanceFor(\n        app: await container.getAsync\u003cFirebaseApp\u003e(),\n      ),\n    );\n  }\n}\n```\n\nCall `addFirebase()` on your builder to add the factories to your composition and add your `FirebaseOptions`.\n\n```dart\nIocContainerBuilder compose() =\u003e IocContainerBuilder(allowOverrides: true)\n  ..addFirebase()\n  //You must add your own FirebaseOptions to the composition\n  ..addSingleton\u003cFirebaseOptions\u003e((container) =\u003e DefaultOptions(\n        apiKey: apiKey,\n        appId: appId,\n        projectId: projectId,\n      ));\n```\n\nYou can now get any Firebase dependencies from the container like this and be sure that it is initialized.\n\n```dart\nfinal firebaseFirestore = await container.getAsync\u003cFirebaseFirestore\u003e();\n```\n\n### Testing\nReplace the dependencies with fakes or mocks in your tests like this.\n\n```dart\nimport 'package:cloud_firestore/cloud_firestore.dart';\nimport 'package:example_2/main.dart';\nimport 'package:firebase_auth/firebase_auth.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport '../firebase.dart';\n\nvoid main() {\n  testWidgets('Testing with Firebase', (WidgetTester tester) async {\n    final builder = compose();\n\n    //TODO: Create mocks for Firebase or use a library like firestore_fakes to \n    //mock the dependencies\n\n    var fakeFirebaseFirestore = FirebaseFirestoreFake();\n\n    //TODO: Put fake data in fakeFirebaseFirestore here. The app will consume it.\n\n    builder\n      ..addSingletonAsync\u003cFirebaseAuth\u003e((container) async =\u003e MockFirebaseAuth())\n      ..addSingletonAsync\u003cFirebaseFirestore\u003e(\n          (container) async =\u003e fakeFirebaseFirestore);\n\n    await tester.pumpWidget(MyApp(container: builder.toContainer()));\n\n    //TODO: Put your tests here\n  });\n}\n```\n\nIf you have any further issues, see the [FlutterFire documentation](https://firebase.flutter.dev/docs/overview/).\n\n## Inspired By .NET\n\nThis 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.\n\n## Extension Methods\n\nMuch 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. \n\nFor 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:\n\n```dart\n  ///Add a singleton service to the container.\n  void addSingletonService\u003cT\u003e(T service) =\u003e addServiceDefinition(\n        ServiceDefinition\u003cT\u003e(\n          (container) =\u003e service,\n          isSingleton: true,\n        ),\n      );\n```\n\n### Keyed Services\n\nYou 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\n\n```dart\nimport 'package:ioc_container/ioc_container.dart';\nimport 'package:test/test.dart';\n\n///Example service\nclass BigService {\n  final String name;\n\n  BigService(this.name);\n  Future\u003cvoid\u003e callApi() =\u003e Future\u003cvoid\u003e.delayed(Duration(seconds: 1));\n\n  ///We can check equality by the name(key)\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n\n    return other is BigService \u0026\u0026 other.name == name;\n  }\n\n  @override\n  int get hashCode =\u003e name.hashCode;\n}\n\n///These give us the functionality to add or\n///access a service by key\nextension KeyedExtensions on IocContainer {\n  T? keyedService\u003cT\u003e(String key) =\u003e get\u003cMap\u003cString, T\u003e\u003e()[key];\n  void setServiceByKey\u003cT\u003e(String key, T service) =\u003e\n      get\u003cMap\u003cString, T\u003e\u003e()[key] = service;\n}\n\nvoid main() {\n  test('Keyed Services', () {\n    var count = 0;\n\n    final builder = (IocContainerBuilder()\n      ..add((container) {\n        //Increments the name (key) of the service so they are unique\n        //Uuid would be better\n        count++;\n        var bigService = BigService(count.toString());\n        bigService;\n        container.setServiceByKey(count.toString(), bigService);\n        return bigService;\n      })\n      ..addSingleton(\n        (container) =\u003e \u003cString, BigService\u003e{},\n      ));\n\n    final container = builder.toContainer();\n\n    final bigContainerOne = container\u003cBigService\u003e();\n    final bigContainerTwo = container\u003cBigService\u003e();\n    final bigContainerThree = container\u003cBigService\u003e();\n\n    //Verifies the three names\n    expect(bigContainerOne.name, '1');\n    expect(bigContainerTwo.name, '2');\n    expect(bigContainerThree.name, '3');\n\n    //Verifies you can access these by key\n    expect(container.keyedService\u003cBigService\u003e(bigContainerOne.name),\n        bigContainerOne);\n\n    expect(container.keyedService\u003cBigService\u003e(bigContainerTwo.name),\n        bigContainerTwo);\n\n    expect(container.keyedService\u003cBigService\u003e(bigContainerThree.name),\n        bigContainerThree);\n  });\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmelbournedeveloper%2Fioc_container","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmelbournedeveloper%2Fioc_container","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmelbournedeveloper%2Fioc_container/lists"}