Skip to content

Scoped DI

Dependencies can be managed within the Vyuh framework using a Dependency Injection (DI) container. We use GetIt 🔗 internally as our default plugin for managing DI. The plugin itself can be referenced with vyuh.di and can be used to set up and lookup dependencies.

However, all the dependencies that you’re creating are managed at a global level. This means we have a single DI container globally in which you put all of your dependencies.

Sometimes it may be necessary that some of the dependencies are only at specific route levels. As long as the route is active, you want the dependencies to be active. Implicitly, the lifetime of the dependencies is linked with the lifetime of the route.

To manage such a scenario, we have introduced the concept of a Scoped DI. This allows you to create a scoped-DI container for a widget-subtree within which you can manage your dependencies. Currently, the default support exists for a Route, but you can also apply this scoping for your own widget trees.

This keeps your dependencies local to the route (or Widget), and if you look up a dependency within that widget tree, it will first try to find something at the Route level (or your own Widget level). If it doesn’t find it, then it goes to the global container (vyuh.di) as a fallback.

Looking up scoped dependencies

You can look up a scoped dependency using the context.di API. The context.di looks up a scoped DI-container up the widget ancestry and finds the nearest one . If none is found, it will fall back to vyuh.di. In any case, you will be able to look up a dependency either locally within the tree or go up the hierarchy until you find the one inside the global container.

Note that you do need the context parameter to look up the scope-di container. This is by design, as you only want this lookup to happen within a widget tree.

Notice the use of context.di to look up the TestStore from the scoped di container.

layout.dart
Widget build(BuildContext context, Card content) {
final title = context.di.get<TestStore>().title;
final theme = Theme.of(context);
return f.Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('The below text is read from a Route-Scoped DI Store',
style: theme.textTheme.labelSmall),
Text(title,
style: theme.textTheme.titleMedium?.apply(
fontWeightDelta: 2,
color: theme.colorScheme.primary,
)),
],
),
),
);
}

Setting a scoped dependency

Since the scope dependency is very limited to the Route, there has to be a way to inject the dependencies just at the time of initializing the route. We do this with the use of the RouteLifecycleConfiguration.

This lifecycle configuration can be set up at the CMS level, or you can also do it explicitly if you are managing the route locally within your code.

The RouteLifecycleConfiguration has the init(BuildContext, RouteBase) and dispose() methods which are invoked whenever the route is about to be initialized or about to be disposed. This is a perfect opportunity for you to set up scoped-dependencies since you also have access to the context within the init() method. Let’s see how this is done for the TestStore.

lifecycle.dart
final class TestStore {
final String title;
TestStore(this.title);
}
@JsonSerializable()
final class DIRegistrationLifecycleHandler extends RouteLifecycleConfiguration {
DIRegistrationLifecycleHandler()
: super(schemaType: 'misc.lifecycleHandler.diRegistration');
static const schemaName = 'misc.lifecycleHandler.diRegistration';
static final typeDescriptor = TypeDescriptor(
schemaType: schemaName,
fromJson: DIRegistrationLifecycleHandler.fromJson,
title: 'DI Registration Lifecycle Handler');
factory DIRegistrationLifecycleHandler.fromJson(Map<String, dynamic> json) =>
_$DIRegistrationLifecycleHandlerFromJson(json);
@override
Future<void> init(BuildContext context, RouteBase route) async {
context.di.register(TestStore('Hello Scoped DI'));
}
@override
Future<void> dispose() async {}
}

Typically, you will setup these lifecycle handlers within the CMS, as shown in the image below. This is done in a Route where such dependencies are needed.

Route lifecycle setup

You would also need to register the TypeDescriptor for the lifecycle-handler inside the Feature. Notice how this is done within the RouteDescriptor using the lifecycleHandlers property.

The init() of the lifecycle-handler is automatically invoked as part of the Route deserialization.

final feature = FeatureDescriptor(
name: 'misc',
title: 'Misc',
description:
'Miscellaneous feature showing all capabilities of the Vyuh Framework.',
icon: Icons.miscellaneous_services_outlined,
// ...
extensions: [
ContentExtensionDescriptor(
// ...
RouteDescriptor(
lifecycleHandlers: [
SimulatedDelayLifecycleHandler.typeDescriptor,
DIRegistrationLifecycleHandler.typeDescriptor,
],
),
),
],
);

Summary

The introduction of the Scoped-DI container gives you new abilities to localize your dependencies within a route. This avoids polluting the global DI container and also takes care of cleaning up the dependencies after they’re done.

By linking the lifetime of the dependencies to the lifetime of your route, you get better memory management, better performance and makes your routes more efficient.