|
| 1 | +--- |
| 2 | +title: Scoped DI |
| 3 | +description: |
| 4 | + Manage dependencies for a single route without affecting the global DI |
| 5 | + container |
| 6 | +--- |
| 7 | + |
| 8 | +import { Aside } from '@astrojs/starlight/components' |
| 9 | +import { Image } from 'astro:assets' |
| 10 | +import routeLifecycleImage from './images/route-lifecycle.png' |
| 11 | + |
| 12 | +Dependencies can be managed within the Vyuh framework using a Dependency |
| 13 | +Injection (DI) container. We use [GetIt](https://pub.dev/packages/get_it) |
| 14 | +internally as our default plugin for managing DI. The plugin itself can be |
| 15 | +referenced with `vyuh.di` and can be used to set up and lookup dependencies. |
| 16 | + |
| 17 | +However, all the dependencies that you're creating are managed at a global |
| 18 | +level. This means we have a single DI container globally in which you put all of |
| 19 | +your dependencies. |
| 20 | + |
| 21 | +Sometimes it may be necessary that some of the dependencies are only at specific |
| 22 | +**route levels**. As long as the route is active, you want the dependencies to |
| 23 | +be active. _Implicitly, the lifetime of the dependencies is linked with the |
| 24 | +lifetime of the route_. |
| 25 | + |
| 26 | +To manage such a scenario, we have introduced the concept of a **Scoped DI**. |
| 27 | +This allows you to create a scoped-DI container for a widget-subtree within |
| 28 | +which you can manage your dependencies. Currently, the default support exists |
| 29 | +for a Route, but you can also apply this scoping for your own widget trees. |
| 30 | + |
| 31 | +This keeps your dependencies local to the route (or Widget), and if you look up |
| 32 | +a dependency within that widget tree, it will first try to find something at the |
| 33 | +`Route` level (or your own `Widget` level). If it doesn't find it, then it goes |
| 34 | +to the global container (`vyuh.di`) as a fallback. |
| 35 | + |
| 36 | +## Looking up scoped dependencies |
| 37 | + |
| 38 | +You can look up a scoped dependency using the `context.di` API. The `context.di` |
| 39 | +looks up a scoped DI-container up the widget ancestry and finds the nearest one |
| 40 | +. If none is found, it will fall back to `vyuh.di`. In any case, you will be |
| 41 | +able to look up a dependency either locally within the tree or go up the |
| 42 | +hierarchy until you find the one inside the global container. |
| 43 | + |
| 44 | +> Note that you do need the `context` parameter to look up the scope-di |
| 45 | +> container. This is by design, as you only want this lookup to happen within a |
| 46 | +> widget tree. |
| 47 | +
|
| 48 | +Notice the use of `context.di` to look up the `TestStore` from the scoped di |
| 49 | +container. |
| 50 | + |
| 51 | +```dart showLineNumbers title="layout.dart" {2,14} |
| 52 | +Widget build(BuildContext context, Card content) { |
| 53 | + final title = context.di.get<TestStore>().title; |
| 54 | +
|
| 55 | + final theme = Theme.of(context); |
| 56 | +
|
| 57 | + return f.Card( |
| 58 | + child: Padding( |
| 59 | + padding: const EdgeInsets.all(8.0), |
| 60 | + child: Column( |
| 61 | + crossAxisAlignment: CrossAxisAlignment.start, |
| 62 | + children: [ |
| 63 | + Text('The below text is read from a Route-Scoped DI Store', |
| 64 | + style: theme.textTheme.labelSmall), |
| 65 | + Text(title, |
| 66 | + style: theme.textTheme.titleMedium?.apply( |
| 67 | + fontWeightDelta: 2, |
| 68 | + color: theme.colorScheme.primary, |
| 69 | + )), |
| 70 | + ], |
| 71 | + ), |
| 72 | + ), |
| 73 | + ); |
| 74 | +} |
| 75 | +
|
| 76 | +``` |
| 77 | + |
| 78 | +## Setting a scoped dependency |
| 79 | + |
| 80 | +Since the scope dependency is very limited to the Route, there has to be a way |
| 81 | +to inject the dependencies just at the time of initializing the route. We do |
| 82 | +this with the use of the `RouteLifecycleConfiguration`. |
| 83 | + |
| 84 | +This lifecycle configuration can be set up at the CMS level, or you can also do |
| 85 | +it explicitly if you are managing the route locally within your code. |
| 86 | + |
| 87 | +The `RouteLifecycleConfiguration` has the `init(BuildContext, RouteBase)` and |
| 88 | +`dispose()` methods which are invoked whenever the route is about to be |
| 89 | +initialized or about to be disposed. This is a perfect opportunity for you to |
| 90 | +set up scoped-dependencies since you also have access to the `context` within |
| 91 | +the `init()` method. Let's see how this is done for the `TestStore`. |
| 92 | + |
| 93 | +```dart showLineNumbers title="lifecycle.dart" {8,22-24 } |
| 94 | +final class TestStore { |
| 95 | + final String title; |
| 96 | +
|
| 97 | + TestStore(this.title); |
| 98 | +} |
| 99 | +
|
| 100 | +@JsonSerializable() |
| 101 | +final class DIRegistrationLifecycleHandler extends RouteLifecycleConfiguration { |
| 102 | + DIRegistrationLifecycleHandler() |
| 103 | + : super(schemaType: 'misc.lifecycleHandler.diRegistration'); |
| 104 | +
|
| 105 | + static const schemaName = 'misc.lifecycleHandler.diRegistration'; |
| 106 | + static final typeDescriptor = TypeDescriptor( |
| 107 | + schemaType: schemaName, |
| 108 | + fromJson: DIRegistrationLifecycleHandler.fromJson, |
| 109 | + title: 'DI Registration Lifecycle Handler'); |
| 110 | +
|
| 111 | + factory DIRegistrationLifecycleHandler.fromJson(Map<String, dynamic> json) => |
| 112 | + _$DIRegistrationLifecycleHandlerFromJson(json); |
| 113 | +
|
| 114 | + @override |
| 115 | + Future<void> init(BuildContext context, RouteBase route) async { |
| 116 | + context.di.register(TestStore('Hello Scoped DI')); |
| 117 | + } |
| 118 | +
|
| 119 | + @override |
| 120 | + Future<void> dispose() async {} |
| 121 | +} |
| 122 | +
|
| 123 | +``` |
| 124 | + |
| 125 | +Typically, you will setup these lifecycle handlers within the CMS, as shown in |
| 126 | +the image below. This is done in a `Route` where such dependencies are needed. |
| 127 | + |
| 128 | +<Image src={routeLifecycleImage} alt={'Route lifecycle setup'} /> |
| 129 | + |
| 130 | +You would also need to register the `TypeDescriptor` for the lifecycle-handler |
| 131 | +inside the Feature. Notice how this is done within the `RouteDescriptor` using |
| 132 | +the `lifecycleHandlers` property. |
| 133 | + |
| 134 | +The `init()` of the lifecycle-handler is automatically invoked as part of the |
| 135 | +`Route` deserialization. |
| 136 | + |
| 137 | +```dart showLineNumbers {11-16} |
| 138 | +final feature = FeatureDescriptor( |
| 139 | + name: 'misc', |
| 140 | + title: 'Misc', |
| 141 | + description: |
| 142 | + 'Miscellaneous feature showing all capabilities of the Vyuh Framework.', |
| 143 | + icon: Icons.miscellaneous_services_outlined, |
| 144 | + // ... |
| 145 | + extensions: [ |
| 146 | + ContentExtensionDescriptor( |
| 147 | + // ... |
| 148 | + RouteDescriptor( |
| 149 | + lifecycleHandlers: [ |
| 150 | + SimulatedDelayLifecycleHandler.typeDescriptor, |
| 151 | + DIRegistrationLifecycleHandler.typeDescriptor, |
| 152 | + ], |
| 153 | + ), |
| 154 | + ), |
| 155 | + ], |
| 156 | +); |
| 157 | +
|
| 158 | +``` |
| 159 | + |
| 160 | +<Aside type={'note'} title={'Resetting the Scoped DI'}> |
| 161 | + The scoped DI container is automatically reset whenever the route is |
| 162 | + refreshed. |
| 163 | +</Aside> |
| 164 | + |
| 165 | +## Summary |
| 166 | + |
| 167 | +The introduction of the **Scoped-DI** container gives you new abilities to |
| 168 | +localize your dependencies within a route. This avoids polluting the global DI |
| 169 | +container and also takes care of cleaning up the dependencies after they're |
| 170 | +done. |
| 171 | + |
| 172 | +By linking the lifetime of the dependencies to the lifetime of your route, you |
| 173 | +get better memory management, better performance and makes your routes more |
| 174 | +efficient. |
0 commit comments