Service Container
The Magic service container is a string-keyed IoC container for registering factory closures and resolving singletons across your application without relying on reflection.
On this page
Introduction#
The Magic service container is a powerful tool for managing class dependencies and performing dependency injection. Like Laravel's container, it allows you to register bindings and resolve them throughout your application.
// Register a service
Magic.bind('mailer', () => MailService());
// Resolve it anywhere
final mailer = Magic.make<MailService>('mailer');
Binding Services#
Simple Bindings#
Use Magic.bind() to register a service. Each resolve creates a new instance:
Magic.bind('api', () => ApiClient());
// Each call creates a new instance
final api1 = Magic.make<ApiClient>('api');
final api2 = Magic.make<ApiClient>('api');
// api1 != api2
Singletons#
Use Magic.singleton() to register a service that's instantiated only once:
Magic.singleton('mailer', () => MailService());
// Always returns the same instance
final mailer1 = Magic.make<MailService>('mailer');
final mailer2 = Magic.make<MailService>('mailer');
// mailer1 == mailer2
Rebinding, and where fakes go#
Binding a key that has already been resolved discards the instance the old
factory produced, so the new binding takes effect immediately. That is what
makes an override work: a starter package binds a key in its provider, your app
binds the same key to its own implementation, and the next make() gets yours
even if something resolved the key in between.
Magic.singleton('svc', () => StarterService());
Magic.make<Service>('svc'); // StarterService
Magic.singleton('svc', () => AppService());
Magic.make<Service>('svc'); // AppService
Binding in Service Providers#
The recommended way to register services is in a service provider:
class AppServiceProvider extends ServiceProvider {
@override
void register() {
// Register services
app.bind('api', () => ApiClient(Config.get('api.url')));
app.singleton('mailer', () => MailService());
}
@override
Future<void> boot() async {
// Perform initialization after all services are registered
final api = app.make<ApiClient>('api');
await api.warmUp();
}
}
Resolving Services#
Using Magic.make()#
// With type inference
final service = Magic.make<MyService>('myservice');
// Without type (returns dynamic)
final service = Magic.make('myservice');
Checking Existence#
if (Magic.bound('api')) {
final api = Magic.make<ApiClient>('api');
}
Automatic Resolution#
Magic facades automatically resolve their underlying services. You don't need to manually resolve common services:
// These facades auto-resolve from the container:
Cache.get('key'); // Resolves CacheManager
Auth.check(); // Resolves AuthManager
Http.get('/users'); // Resolves the NetworkDriver
Using Controllers#
Magic provides a convenient way to manage controller instances:
findOrPut Pattern#
The recommended pattern for controllers is using Magic.findOrPut():
class UserController extends MagicController with MagicStateMixin<List<User>> {
// Singleton accessor
static UserController get instance => Magic.findOrPut(UserController.new);
// Controller logic...
}
// Usage anywhere in your app
final controller = UserController.instance;
How It Works#
- First call: Creates a new controller instance and stores it
- Subsequent calls: Returns the existing instance
- Automatic cleanup: Controller's
onClose()is called when disposed
Manual Controller Management#
// Register a controller
Magic.put(UserController());
// Find an existing controller
final controller = Magic.find<UserController>();
// Delete a controller
Magic.delete<UserController>();
Magic.delete disposes what it removes when it is a ChangeNotifier: every MagicController, and a plain ChangeNotifier or ValueNotifier registered with Magic.put too. Do not dispose such a notifier yourself as well; a second dispose() throws in a debug build. Magic.flush() clears the registry without disposing.
Controller Lifecycle#
class UserController extends MagicController {
@override
void onInit() {
// Called once, the first time a stateful view backs the controller.
// super.onInit() marks the controller initialised; without it a remount
// runs onInit (and loadUsers) again.
super.onInit();
loadUsers();
}
@override
void onClose() {
// Called when controller is disposed
// Clean up subscriptions, streams, etc.
}
}
Related Documentation#
- Facades: the static stubs that resolve these bindings.
- Service Providers: where bindings are registered and booted.
- Request Lifecycle: when the container is built during
Magic.init(). - Controllers:
MagicControllerand its lifecycle.