Description

This package provides a dependency injection solution for the Dart language, as well as a application server based on dart_router_extended.

Features

  • Simple injection: register an object instance. This will basically be treated as a Singleton object. Each injection call will return the same object
  • Lazy injection: register a builder function producing an object. This function will be called when the first injection will be executed, then the same object will be returned on any subsequent injections
  • Factory injection: register a factory function producing objects on each injection call
  • Qualified name injection: the container supports qualified injection so you can provide a name for your dependency
  • Injection profiles: you can register a certain object for a number of profiles, then inject or don't inject the value according to the selected profile. This feature is helpful if you want to run your application with different injection profiles
  • Value injection: inject simple named values into the container
  • Web server configuration
  • Web routes and controllers support
  • Web routes security using route guard
  • CORS configuration support
  • Scheduled tasks support
  • Eventing support

Usage

Simple injection

var myObject = MyClass();
var myProperty = "Prop value";
// Register an object with the container
$().generic(object: myObject)
   // then register a value
   .value("myProperty", myProperty)
   // then register a named object. This also works with builders and factories
   .generic(object: myObject, name: "alias");

// Retrieve object
MyClass injectedObject = $().get();
// Retrieved the qualified object
MyClass injectedObjectAlias = $().get(name: "alias");

// Retrieve object if present
MyClass? injectedObjectIfPresent = $().getIfPresent();
// Retrieve the qualified object 
MyClass? injectedAliasObjectIfPresent = $().getIfPresent(name: "alias");

// You can also use shortcut methods
MyClass injectedObject = $get();
MyClass injectedObjectAlias = $get(name: "alias");
MyClass? injectedObjectIfPresent = $$get();

// Retreieve values
String property = $().getValue("myProperty");
String? propertyIfPresent = $().getValueIfPresent("myProperty");

//or use the shortcut methods
String property = $val("myProperty");
String? propertyIfPresent = $$val("myProperty");

Builder and factory injection

Both the builder and the factories are practically methods that are used to build the container object, with the difference that with a factory, the method is called every time the object is retrieved from the container, while with the builder it is only built once, then the same instance returned, making it basically a lazy buildable singleton.

class SimpleObj {
    final String timestamp;
    SimpleObj(this.timestamp);
}
// Register with the container
$()
    //Inject the builder function that will only be called once to create the container object
    .generic(builder: () => MyClass())
    .generic(factory: () => SimpleObj(DateTime.now().microsecondsSinceEpoch.toString()));

// Retrieve object
MyClass injectedObject = $().get();
// Produce object using the injected factory
SimpleObj injectedObjectIfPresent = $().getIfPresent();

// You can also use shortcut methods
MyClass injectedObject = $get();
SimpleObj injectedObjectIfPresent = $$get();

Conditional callbacks

class SimpleObj {
    final String timestamp;
    SimpleObj(this.timestamp);
}
// Register with the container
$()
    //Inject the builder function that will only be called once to create the container object
    .generic(builder: () => MyClass())
    .generic(factory: () => SimpleObj(DateTime.now().microsecondsSinceEpoch.toString()));

// Retrieve object
MyClass injectedObject = $().get();
// Produce object using the injected factory
SimpleObj? injectedObjectIfPresent = $().getIfPresent();

// You can also use shortcut methods
MyClass injectedObject = $get();
SimpleObj? injectedObjectIfPresent = $$get();

// Conditional callback, call some code only if an object is present in the container
Container().ifPresentThen<MyClass>((MyClass obj) {
    print(obj);
});
// Or by using the shortcut method
$then<MyClass>((MyClass obj) {
    print(obj);
});

// Conditional callback. Call some code only if a value is present in the container
$().ifValuePresentThen("valueKey", (value) {
    print(value);
});
// Or by using the shortcut method
$valThen("valueKey", (value) {
    print(value);
});

// Conditional callback with multiple dependencies. The container will invoke the callback
// only if all dependencies are found.
$().ifAllPresentThen([
    Lookup.object(MyClass), 
    Lookup.object(SimpleObject), 
    Lookup.value("valueKey")
    ], (list) {
        MyClass? myClass;
        SimpleObject? simpleObject;
        String? value;
        [myClass, simpleObject, value] = list;
});
// Or by calling the shortcut method
$allThen([
    $look(MyClass), 
    $look(SimpleObject), 
    $lookVal("valueKey")
    ], (list) {
        MyClass? myClass;
        SimpleObject? simpleObject;
        String? value;
        [myClass, simpleObject, value] = list;
});

Using profiles

var myObject = MyClass();
var myProperty = "Prop value";

// Register with the container
$()
    .generic(object: myObject, profiles: ["test", "run"])
    .value("myProperty", myProperty, profiles: ["test", "run"])
    // Setting the active profile
    .profile("run");

// Retrieve object. The injection always uses the active profile when injecting any registered objects or provided values
// If the object is not present in the container for the active profile, this method will throw an exception
MyClass injectedObject = $().get();
// Retrieve object if present. 
// If the object is not present in the container for the active profile, this method will return null
MyClass? injectedObjectIfPresent = $().getIfPresent();

// Retreieve values. If the value does not exist on the active profile, this method will throw an exception
String property = $().getValue("myProperty");
// If the value does not exist on the active profile, this method will return null
String? propertyIfPresent = $().getValueIfPresent("myProperty");

Injecting objects for interfaces


class MyInterface {
    void doSomething() {}
}

class MyClass implements MyInterface {
    @override
    void doSomething() {
        print("Something");
    }
}

var myObject = MyClass();

// Register with the container for the interface instead of the type
$().typed(MyInterface, object: myObject);

// If the object is not present in the container for the active profile, this method will throw an exception
MyInterface injectedObject = $().get();
// Retrieve object if present. 
// If the object is not present in the container for the active profile, this method will return null
MyInterface? injectedObjectIfPresent = $().getIfPresent();

Autostartable objects

Sometimes you might need to run some code, or start a webserver (the build in web server is also an AutoStart implementation). This iw why dart_container implements autostartable functionality.

class AutoStartMock implements AutoStart {
  @override
  void init() {
    print("Init called");
  }

  @override
  void run() {
    print("Run called");
  }
}

$().generic(builder: () => AutoStartMock(), autoStart: true)
  // Once autostart is called, the init method is called first for the AutoStart objects,
  // then the run method is called asynchronously, to avoid blocking the container and any other functionality
  .autoStart();

Scheduled jobs

Configuring the scheduler

// Sets the timer polling interval to the specified duration
// The polling interval is useful if you have scheduled tasks running at long periods
// of time. Longer periods will lessen the CPU load but will also reduce trigger time accuracy
// The default value, if not specified, is 10 seconds
$().schedulerPollingInterval(Duration(seconds: 1));

// Will delay all tasks from starting by the specified duration
$().schedulerInitialDelay(Duration(seconds: 10));

One time scheduled job

class OneTimeScheduledJob implements ScheduledJob {
  bool hasRun = false;
  @override
  Duration? getDuration() => Duration(seconds: 1);

  @override
  ScheduledJobType getType() => ScheduledJobType.oneTime;

  @override
  void run() {
    hasRun = true;
  }

  @override
  DateTime? getStartTime() => null;
}

// Will run scheduled task after 1 second, as provided by the getDuration implementation
$().schedule(oneTime).autoStart();

Periodic scheduled job

class PeriodicScheduledJob implements ScheduledJob {
  int runTimes = 0;
  @override
  Duration? getDuration() => Duration(seconds: 1);

  @override
  ScheduledJobType getType() => ScheduledJobType.periodic;

  @override
  DateTime? getStartTime() => null;

  @override
  void run() {
    runTimes++;
  }
}

// Will run immediately, then at every 1 second as specified by the getDuration implementation
PeriodicScheduledJob periodic = PeriodicScheduledJob();
$().schedule(periodic).autoStart();

At exact time scheduled job

class AtExactTimeScheduledJob implements ScheduledJob {
  bool ran = false;
  @override
  Duration? getDuration() => null;

  @override
  DateTime? getStartTime() => DateTime.now().add(Duration(seconds: 3));

  @override
  ScheduledJobType getType() => ScheduledJobType.atExactTime;

  @override
  void run() {
    ran = true;
  }
}

AtExactTimeScheduledJob atTime = AtExactTimeScheduledJob();

// Will run after 3 seconds
$().schedulerPollingInterval(Duration(seconds: 1))
  .schedule(atTime)
  .autoStart();

At exact time repeating scheduled job

class AtExactTimeRepeatingScheduledJob extends ScheduledJob {
  bool ran = false;
  @override
  Duration? getDuration() => Duration(seconds: 2);

  @override
  DateTime? getStartTime() => DateTime.now().add(Duration(seconds: 3));

  @override
  ScheduledJobType getType() => ScheduledJobType.atExactTime;

  @override
  void run() {
    ran = true;
  }
}

AtExactTimeScheduledJob atTime = AtExactTimeScheduledJob();

// Will run after 3 seconds, then run every other 2 seconds
$().schedulerPollingInterval(Duration(seconds: 1))
  .schedule(atTime)
  .autoStart();

Web server

dart_container includes a built in webserver that can be easily configured using Controllers and Routes.

class StausController extends Controller {
  StatusController()
      : super(
        // All the routes under a controller are mounted under the controller prefix
          pathPrefix: "/status",
          routes: [
            GetStatusRoute(),
            PostStatusRoute(),
          ],
          // The guard allows secured access to routes. If you don't want anyone accessing a route or
          // controller, you can implement a guard
          guard: StatusGuard(),
        );
}

class StatusGuard extends RouteGuard {
  @override
  bool isSecure(Request request) {
    return true;
  }
}

class GetStatusRoute extends AbstractRoute {
  GetStatusRoute()
      : super(
        // All the route parsing is fully compatible with shelf_router since that is the actual library used
          ["/<key>"],
          Method.get,
        );
  
  @override
  Function buildHandler() {
    return _respond;
  }

  Response _respond(Request req, String key) {
    return JsonResponse.okJson({key: "requested"});
  }
}

class PostStatusRoute extends AbstractRoute {
  PostStatusRoute()
      : super(
          ["/<key>"],
          Method.post,
        );

  @override
  Function buildHandler() {
    return _respond;
  }

  Response _respond(Request req, String key) async {
    return JsonResponse.ok({key: "posted"});
  }

}

$().webServerConfig(
  // First of all, we need a not found handler
    (req) => Response.notFound(
        "Route not found for ${req.method}:${req.requestedUri}"),
    // the host
    'localhost',
    // and the port
    env.httpPort,
    shared: true,
    profiles: ["test", "run"],
  )
  // Provide the list of controllers
  .controllers([
    StatusController();
  ])
  // and/or provide a list of routes
  .routes([
    GetStatusRoute(),
    PostStatusRoute(),
  ])
  // set the active profile
  .profile("run")
  // the call autostart to boot up the web server
  .autoStart();

  // If you do not want to use the autostartables and still want to boot up the web server, you can
  Future.delayed(Duration.zero, () async => $get<WebServer>().run());
  .

Eventing

With dart-container you get a simple publish/subscribe framework for passing along messages, and building reactive applications. For the time being, the support is limited to exact matching topics.

Simple topics


// First subscribe to the topic
$().subscribe("topic", (topic, event) {
  print("Got message on topic $topic with event value $event");
});

// Then publish to the topic
$().publishEvent(["topic"], "newValue");

// You can also subscribe and publish to multiple topics

// Publishing to multiple subscribers
$().subscribe("topic1", (topic, event) {
  print("Subscriber 1 : Got message on topic $topic with event value $event");
});

$().subscribe("topic2", (topic, event) {
  print("Subscriber 2 : Got message on topic $topic with event value $event");
});

// or use the shortcut
// $sub("topic2", (topic, event) {
//  print("Subscriber 2 : Got message on topic $topic with event value $event");
//});


// Publish a message to multiple topics.
// In this case, both subscribers will get the new message.
$().publishEvent(["topic1", "topic2"], "newValue");
// or use the shortcut
// $pub(["topic1", "topic2"], "newValue");

Wildcards and subtopics topics


$().subscribe("topic/*", (topic, event) {
  // Will receive messages from both subtopics
});

// Publish message to first subtopic
$().publishEvent(["topic/subtopic1"], "newValue1");
// Publish message to second subtopic
$().publishEvent(["topic/subtopic2"], "newValue2");

$().subscribe("topic/*", (topic, event) {
  // Will receive messages from both subtopics
});

// Publish message to first subtopic
$().publishEvent(["topic/subtopic1", "topic/subtopic1"], "newValue1");
// IMPORTANT: When publishing a value to multiple subtopics, the subscriber is only called once
// with the topic parameter having the value of the first matching topic from the publish list

Libraries

dart_container
Support for doing something awesome.