Skip to content

Commit 69c3c60

Browse files
committed
added doc for scoped DI
1 parent 3280c1e commit 69c3c60

File tree

4 files changed

+180
-2
lines changed

4 files changed

+180
-2
lines changed

astro.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import starlightLinksValidator from 'starlight-links-validator'
77
import react from '@astrojs/react'
88

99
import tailwind from '@astrojs/tailwind'
10-
import { sidebar } from './sidebar.ts'
10+
import sidebar from './sidebar.ts'
1111
import rehypeExternalLinks from 'rehype-external-links'
1212

1313
import icon from 'astro-icon'

sidebar.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const sidebar = [
1+
export default [
22
{
33
label: 'Intro',
44
items: ['intro', 'intro/get-started', 'intro/cli', 'intro/integrating-cms'],
@@ -26,6 +26,10 @@ export const sidebar = [
2626
slug: 'guides/events',
2727
badge: { text: 'New', variant: 'note' },
2828
},
29+
{
30+
slug: 'guides/scoped-di',
31+
badge: { text: 'New', variant: 'note' },
32+
},
2933
{
3034
label: 'CMS',
3135
items: [
Loading

src/content/docs/guides/scoped-di.mdx

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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

Comments
 (0)