A fast, flexible, IoC library for Dart and Flutter

Overview

Qinject

A fast, flexible, IoC library for Dart and Flutter

Qinject helps you easily develop applications using DI (Dependency Injection) and Service Locator based approaches to IoC (Inversion of Control), or often a mix of the two.

Makefile CI

Key Features

  • Support for DI (Dependency Injection)
  • Support for Service Locator
  • Implicit dependency chain resolution; no need to define and maintain depends on relationships between registered dependencies
  • Simple, yet extremely flexible dependency registration and resolution mechanics
  • Register any type as a dependency, from Flutter Widgets and functions to simple classes
  • Simple, but powerful, unit testing tooling

Installation

The Qinject package is available at pub.dev/packages/qinject

It can be installed with

  dart pub add qinject

or

  flutter pub add qinject

Quick Start

The following example shows a basic application making use of both DI and Service Locator patterns alongside Singleton and Resolver dependency registration. These topics are covered in more detail later in this document.

Note: A fully featured demonstration application is provided in the ./example directory. This can be run by executing make example from the repo root.

main() {
  // Use the Qinject instance to support DI by passing it as a constructor
  // argument to classes defining dependencies
  final qinjector = Qinject.instance();

  Qinject.registerSingleton(() => stdout);
  Qinject.register((_) =>
      DateTimeReader()); // Note the type argument is ignored as the resolver does not use it and the type arguments required by `register` are omitted due to dart's type inference
  Qinject.register((_) => Greeter(qinjector));

  Qinject.use<void, Greeter>().greet(); // Resolve a Greeter instance and invoke its greet method
}

class Greeter {
  // Below the service locator style of resolution is used. This may be
  // preferable for some trivial dependencies even when DI is being used elsewhere
  final Stdout _stdout = Qinject.use<Greeter, Stdout>();

  // Here the DI style of dependency resolution is used. This is typically
  // more flexible when writing unit tests
  final DateTimeReader _dateTimeReader;

  Greeter(Qinjector qinjector)
      : _dateTimeReader = qinjector.use<Greeter, DateTimeReader>();

  void greet() => _stdout.writeln("Right now it's ${_dateTimeReader.now()}");
}

class DateTimeReader {
  DateTime now() => DateTime.now();
}

Dependency Registration

There are two high level types of dependency registration; Singleton and Resolver. Resolvers are covered in detail in the dedicated Resolvers section. A Singleton is evaluated once, the first time it is resolved, and then the same instance is returned for the lifetime of the application.

Singletons are registered as shown below:

  Qinject.registerSingleton(() => MyClass());

When use<TConsumer, MyClass>() is invoked for the first time, a new instance of MyClass is returned. When use<TConsumer, MyClass>() is subsequently invoked, that same, original instance is returned; for the lifetime of the application.

Dependency Usage

All dependencies of any type are resolved with the same method: use<TConsumer, TDependency>. This may be invoked off the Qinject Service Locator or an via instance of Qinjector injected into a class. Both approaches can be used interchangeably as any dependencies registered are accesible via either route.

Using Service Locator

The Service Locator is a simple pattern for decoupling dependencies. Its usage is predicated on a globally accessible Service Locator instance; in the case of Qinject this is the Qinject type itself which exposes a static use<TConsumer, TDependency> method.

Dependencies can be resolved in the below manner from anywhere that can access the Qinject type:

    final DependencyType _dependency = Qinject.use<ConsumerType, DependencyType>();

Some complex applications may become difficult to test when Service Locator is used to decouple dependencies. Especially where concurrently executing logic is concerned. This is due to the single global instance, which needs configuring with test doubles appropriate for all tests in a given test run, or at least repeatedly configured and cleared down. For such applications, DI may be a better choice, with Service Locator either not used, or reserved for trivial dependencies, such as stdout as shown in the Quick Start example

For many applications however, Service Locator is a time-served, simple and effective choice.

Using DI (Dependency Injection)

Adopting DI sees classes declare their dependencies as variables, typically of some abstract type, and expose setters to these variables in their constructors (typically). These variables are then set by an external entity which is responsible for choosing, creating and managing the lifetime of the concrete implementations of those abstract dependency types.

Done naively, this quickly becomes a very complex dependency graph to manage, so the majority of projects that adopt this approach use an IoC Library or Framework to manage this complexity. This is what Qinject offers for Dart and Flutter projects.

Qinject takes a slightly different approach to this than comparable frameworks in other languages, however, by injecting an abstract Service Locator instance into the constructor. This is then immediately used by the constructor to acquire any dependencies.

An example is shown below, illustrating how dependencies are declared in the constructor and populated by the injected Qinjector instance.

main() {
    // Access the Qinjector instance
    final qinjector = Qinject.instance();

    Qinject.register((_) => ConsumerClass(qinjector)); // Note the ConsumerClass can be registered before its dependencies
    Qinject.register((_) => DependencyA(qinjector)); // Note that DependencyA requires a Qinjector also. We hide this from ConsumerClass by providing it in the Resolver closure; this is purely for brevity and clarity however
    Qinject.register((_) => DependencyB());
    Qinject.register((_) => DependencyC(qinjector));
    
    final consumerClass = Qinject.use<void, ConsumerClass>(); // Resolve an instance of ConsumerClass

    // do something with consumerClass
}


class ConsumerClass {
  final DependencyA _dependencyA;
  final DependencyB _dependencyB;
  final DependencyC _dependencyC;

  ConsumerClass(Qinjector qinjector)
      : _dependencyA = qinjector.use<ConsumerClass, DependencyA>(),
        _dependencyB = qinjector.use<ConsumerClass, DependencyB>(),
        _dependencyC = qinjector.use<ConsumerClass, DependencyC>();

  // Do something with dependencies
}

Complex applications can be easier to test when DI is used to decouple dependencies. In Qinject this is due to the interface representing the mechanism of dependency resolution that is passed into each consuming class. In testing scenarios, this can be replaced with a different implementation of Qinjector created specifically for the test and scoped to it, and it alone. This prevents the configuration for one test leaking into an other and brittle dependencies forming around the ordering of test execution. These are subtle benefits, but on larger projects they often pay dividends.

Unit Testing with Qinjector

A TestQinjector instance can be used to register Test Doubles for dependencies to assist in Unit Testing. The TestQinjector instance can then be passed to dependency consumers instead of the default Qinjector instance returned from Qinject.instance().

For example, if testing the sample Qinjector application above, the following may be defined

main() {
    test('ConsumerClass behaves as expected', () {
        // Create a TestQinjector instance that implements the Qinjector interface
        final qinjector = TestQinjector();

        // Register stubs or mocks as required against the TestQinjector instance
        qinjector.registerTestDouble<DependencyA>((_) => DependencyAStub()); // Note the TDependency type argument is set explictly here to DependencyA otherwise Dart's type inference would cause the registration to be assigned to the type DependencyAStub and then dependency resolution would fail in ConsumerClass
        qinjector.registerTestDouble<DependencyB>((_) => DependencyBMock()); 
        qinjector.registerTestDouble<DependencyC>((_) => DependencyCStub());
        
        final consumerClass = ConsumerClass(qinjector); // Create instance of ConsumerClass using the TestQInjector

        // do some assertions against consumerClass
    }
}

Resolvers

Dependencies are registered by assigning a Resolver delegate to a dependency type, labelled as TDependency. A Resolver delegate is any function that accepts a single Type argument and returns an instance of TDependency.

This takes the form:

Qinject.register<TDependency>(TDependency Function (Type consumer));

The type of TDependency can be any Type recognised by the type system; not just a class. For example, functions are often registered as Factory Resolvers

The Resolver delegate is not invoked at the point of registration. As such, any dependencies that the TDependency requires need not yet be registered within Qinject. The only requirement for successful resolution is that the full dependency graph of a given TDependency is registered, in any order, before use<TConsumer, TDependency>() is called for that particular TDependency.

The Type argument can often be ignored during the dependency resolution process; it is available purely to allow different implementations of the same interface to be returned to different consumers, should that be required. The argument passed to Type is the type that was passed as TConsumer in the call to use<TConsumer, TDependency> that caused the Resolver delegate to be invoked. This is typically set to the Type of the consumer itself, however it can be any type.

In cases where there is no meaningful enclosing Type, or it is not relevant, void may be passed.

It is important to remember that there is only one fundamental type of Resolver; and that is any function that meets the Resolver signature. This means your dependency resolution process and lifetime management can be as simple or as complex as your needs require. However, some common forms of Resolver are described in the following sections:

Transient Resolvers

A Transient Resolver is the simplest form of Resolver. It looks like the below

Qinject.register((_) => MyClass()); // note the Type argument is ignored with _ as it is not used in this example (though it could be required if the resolution process)

Whenever use<MyClass>() is invoked, a new instance of MyClass is returned.

Type Sensitive Resolvers

A Type Sensitive Resolver returns a different implementation of an interface depending on the type passed as the consumer argument.

Qinject.register((Type consumer) => consumer.runtimeType == TypeA
? ImplementationA()
: ImplementationB());

Whenever use<TConsumer, TypeA>() is invoked, a new instance of ImplementationA is returned. When use<TConsumer, NotTypeA>() is invoked where the NotTypeA is, as the name suggests, anything other than TypeA, then a new instance of ImplementationB is returned.

Note that both ImplementationA and ImplementationB must implement the same interface.

Async Resolvers

An Async Resolver is similar to any other form of resolver aside from the return type being a Future<TDependency> rather than a TDependency. It looks like the below:

Qinject.register((_) async { // note the async annotation
  await Future.delayed(Duration(milliseconds: 100)); // Simulate some async activity
  return MyClass();
});

Whenever use<TConsumer, Future<MyClass>>() is invoked, a Future<MyClass> is returned, which can be awaited on if required, as with any Future. For example:

final myClass = await Qinject.use<void, Future<MyClass>>();

Factory Resolvers

A Factory Resolver returns a factory function rather than a dependency instance. This is commonly used for classes with runtime-variable constructor arguments, such as Flutter Widgets.

    Qinject.register((_) => (String variableArg) => MyClass(variableArg));

Whenever use<TConsumer, MyClass Function(String)>() is invoked, a function is returned that accepts a String argument and returns an instance of MyClass. This function would typically be assigned to a variable in the consumer and repeatedly invoked with different arguments; in the manner a Flutter Widget constructor may be invoked many times within a parent Widget.

For example:

Qinject.register((_) => (String msg) => MessageWidget(msg)); // Register the MessageWidget Factory Resolver

class ConsumerWidget extends StatelessWidget {
  final MessageWidget Function(String) _messageWidget; // The ConsumerWidget has a dependency on the MessageWidget expressed as a Factory Function

  ConsumerWidget(Qinjector qinjector, {Key? key})
      : _messageWidget =
            // Resolve the MessageWidget dependency using Qinjector
            qinjector.use<ConsumerWidget, MessageWidget Function(String)>(), // 
        super(key: key);

  @override
  Widget build(BuildContext context) => {
     // Use the _messageWidget factory function as many times as required, in place of the MessageWidget constructor
     _messageWidget("Hello you!"); 
     _messageWidget("Hello World!");
     _messageWidget("Hello Universe!");
}

class MessageWidget extends StatelessWidget {
  final String _message;

  const MessageWidget(this._message, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => Text(_message);
}

Cached Resolvers

A Cached Resolver returns the same instance of TDependency for a defined period, before refreshing it and using the new instance for the next defined period, ad infinitum. This form of Resolver is a good example of the simple, flexible nature of Qinject's approach to dependency resolution.

  // In registration section of app
  var cachedDataTimeStamp = DateTime.now();
  var cachedData = populateCachedData();

  Qinject.register((_) {
    if (DateTime.now().difference(cachedDataTimeStamp).inSeconds > 60) {
      var cachedDataTimeStamp = DateTime.now();
      var cachedData = populateCachedData();
    }

    return cachedData;
  });

  // Elsewhere in main app codebase
  CachedData populateCachedData() {
    // omitted for brevity; maybe fetched from a network service or local db
    CachedData();
  }

  class CachedData {
    // omitted for brevity; would house various data attributes
  }

Logging & Diagnostics

By default, Qinject logs records of potentially relevant activity to the console. In some cases, this may need to be overridden to redirect logs or to apply some form of pre-processing to them.

This can be achieved by setting the Quinject.log field to a custom logging delegate.

The below example routes logs to Flutter's debugPrint

  Qinject.log = (message) => debugPrint(message);

Contributions

Pull requests are welcome, particularly for any bug fixes or efficiency improvements.

API changes will be considered, however, while there are a number of shorthand or helper methods that can readily be imagined, the aim is to keep Qinject light, simple, and flexible. As such, changes of that manner may be rejected, but not with any negative judgement associated with the contribution

You might also like...

Fast and idiomatic UUIDs in Dart.

neouuid Fast and idiomatic UUIDs (Universally Unique Identifiers) in Dart. This library decodes and generates UUIDs, 128-bits represented as 32 hexade

Aug 23, 2022

Fast math TeX renderer for Flutter written in Dart.

Fast math TeX renderer for Flutter written in Dart.

CaTeX is a Flutter package that outputs TeX equations (like LaTeX, KaTeX, MathJax, etc.) inline using a widget and Flutter only - no plugins, no web v

Sep 16, 2022

Lucifer is a fast, light-weight web framework in dart.

Lucifer is a fast, light-weight web framework in dart.

Lucifer Lightbringer Lucifer is a fast, light-weight web framework in dart. It's built on top of native HttpServer to provide a simple way to fulfill

Sep 12, 2022

flutter_thrio makes it easy and fast to add flutter to existing mobile applications, and provide a simple and consistent navigator APIs.

flutter_thrio makes it easy and fast to add flutter to existing mobile applications, and provide a simple and consistent navigator APIs.

中文文档 英文文档 问题集 原仓库不再维护,代码已经很老了 最近版本更新会很快,主要是增加新特性,涉及到混合栈的稳定性的问题应该不多,可放心升级,发现问题加 QQ 群号码:1014085473,我会尽快解决。 不打算好好看看源码的使用者可以放弃这个库了,因为很多设定是比较死的,而我本人不打算花时间写

Sep 20, 2022

flutter_thrio makes it easy and fast to add flutter to existing mobile applications, and provide a simple and consistent navigator APIs.

flutter_thrio makes it easy and fast to add flutter to existing mobile applications, and provide a simple and consistent navigator APIs.

本仓库不再维护,可移步新仓库 https://github.com/flutter-thrio/flutter_thrio 中文文档 问题集 QQ 群号码:1014085473 The Navigator for iOS, Android, Flutter. Version 0.2.2 requir

Sep 21, 2022

Flutter makes it easy and fast to build beautiful apps for mobile and beyond

Flutter makes it easy and fast to build beautiful apps for mobile and beyond

Flutter is Google's SDK for crafting beautiful, fast user experiences for mobile, web, and desktop from a single codebase. Flutter works with existing

Sep 22, 2022

A fast, extra light and synchronous key-value storage to Get framework

A fast, extra light and synchronous key-value storage to Get framework

get_storage A fast, extra light and synchronous key-value in memory, which backs up data to disk at each operation. It is written entirely in Dart and

Sep 29, 2022

Memory Cache is simple, fast and global in-memory cache with CRUD features.

Memory Cache Memory Cache is simple, fast and global in-memory cache. Features Create, read, update, delete and invalidate cache. Expirable Cache Gett

Aug 13, 2022

Flutter widget to create a group of buttons fast

Flutter widget to create a group of buttons fast

Flutter widget to create a group of buttons fast 🚀 Included Radio and CheckBox

Dec 28, 2021
Comments
  • Conisder a more liberal license

    Conisder a more liberal license

    I just realized this is GPL-3.0 and unfortunately I'm not permitted to use it. I'm actually only working on open source things at the moment, but I think there is concern that if we allow a copyleft in, then we make a mistake and use it where we shouldn't.

    Looking at my project in progress, I have about 25 dependencies and this is the first copyleft I've come across.

    opened by jibbers42 0
  • `GetIt.asNewInstance()` equivalent?

    `GetIt.asNewInstance()` equivalent?

    Hi, I was taking a look at your new lib and was wondering if there is a GetIt.asNewInstance() equivalent? My concern is using this in a lib and some lib client happens to also use Qinject, and then one of us does a Qinject.reset(). 😯 I assume that would wipe out registrations for both the lib and the client, correct? (I haven't tested it)

    It would be nice if libs can isolate their usage of Qinject. 🙂

    opened by jibbers42 0
Owner
Technical Lead
null
Fluro is a Flutter routing library that adds flexible routing options like wildcards, named parameters and clear route definitions.

English | Português The brightest, hippest, coolest router for Flutter. Features Simple route navigation Function handlers (map to a function instead

Luke Pighetti 3.5k Sep 30, 2022
Simple and fast Entity-Component-System (ECS) library written in Dart.

Fast ECS Simple and fast Entity-Component-System (ECS) library written in Dart. CPU Flame Chart Demonstration of performance for rotation of 1024 enti

Stanislav 7 Jul 22, 2022
QR.Flutter is a Flutter library for simple and fast QR code rendering via a Widget or custom painter.

QR.Flutter is a Flutter library for simple and fast QR code rendering via a Widget or custom painter. Need help? Please do not submit an issue for a "

Yakka 600 Sep 16, 2022
A Very Flexible Widget that can Implement Material Sheets on all Directions, both modal and persistent, and consequently a Material Navigation Drawer

Flutter_MaterialSheetAndNavigationDrawer If this project helped you reduce developement time or you just want to help me continue making useful tools

Bryan Cancel 30 Dec 4, 2021
FLUTTER API: Video Editor allows trim, crop, rotate and scale video with a super flexible UI Design

video_editor My other APIs Scroll Navigation Video Viewer Helpers Features Super flexible UI Design. Support actions: Crop Trim Scale Rotate Cover sel

Luis Felipe Murguia Ramos 186 Sep 30, 2022
A more flexible version of Flutter's ColorScheme.fromSeed

FlexSeedScheme A more flexible version of Flutter's ColorScheme.fromSeed. Use this package like ColorScheme.fromSeed with the following additional cap

Rydmike 6 Sep 20, 2022
Create dart data classes easily, fast and without writing boilerplate or running code generation.

Dart Data Class Generator Create dart data classes easily, fast and without writing boilerplate or running code generation. Features The generator can

null 186 Feb 28, 2022
Lightweight and blazing fast key-value database written in pure Dart.

Fast, Enjoyable & Secure NoSQL Database Hive is a lightweight and blazing fast key-value database written in pure Dart. Inspired by Bitcask. Documenta

HiveDB 3.2k Sep 21, 2022
Lightweight and blazing fast key-value database written in pure Dart.

Fast, Enjoyable & Secure NoSQL Database Hive is a lightweight and blazing fast key-value database written in pure Dart. Inspired by Bitcask. Documenta

HiveDB 3.2k Sep 25, 2022
Fast and productive web framework provided by Dart

See https://github.com/angulardart for current updates on this project. Packages Source code Published Version angular angular_forms angular_router an

Angular Dart Open Source Packages 1.9k Sep 20, 2022