Context Hierarchy#
WiretapContext supports a parent-child relationship, allowing you to create hierarchies of injection contexts. A child context can inherit and override dependencies from its parent, enabling more flexible dependency management in complex applications.
Creating a Child Context#
To create a child context, pass a parent context to the parent parameter:
final parentContext = WiretapContext(
providers: [
apiClientToken.provideOverride((_) => ApiClient()),
],
);
final childContext = WiretapContext(
parent: parentContext,
);
// Child can access parent's dependencies
final client = childContext.inject(apiClientToken);
Dependency Resolution in Hierarchies#
When you request a token from a child context, the resolution follows this order:
- Child's local providers — If the token is provided in the child context, its value is used
- Parent's values — If not found locally, the parent context is checked (recursively up the chain)
- Fallback factory — If still not found, the token's fallback factory is called
final configToken = createToken<Config>((_) => Config.defaults());
final parentContext = WiretapContext(
providers: [
configToken.provideOverride((_) => Config.production()),
],
);
final childContext = WiretapContext(parent: parentContext);
// Uses parent's provider
final config = childContext.inject(configToken);
Important: Fallback factories are created at the root context only. If a fallback is triggered, it's stored in the root context and shared across all descendants:
final root = WiretapContext();
final child = WiretapContext(parent: root);
final grandchild = WiretapContext(parent: child);
final token = createToken<Service>((_) => Service()); // has fallback
final result1 = grandchild.inject(token); // fallback created at root
final result2 = root.inject(token); // same instance!
Modifying Dependencies Without Affecting the Parent#
Child contexts can override dependencies from their parent without affecting the parent:
final envToken = createToken<String>();
final parentContext = WiretapContext(
providers: [envToken.provideOverride((_) => 'production')],
);
final childContext = WiretapContext(
providers: [envToken.provideOverride((_) => 'test')],
parent: parentContext,
);
expect(childContext.inject(envToken), equals('test'));
expect(parentContext.inject(envToken), equals('production'));
Dependency Chains Across Contexts#
Providers in a child context can depend on values from the parent context:
final databaseUrlToken = createToken<String>();
final databaseToken = createToken<Database>();
final parentContext = WiretapContext(
providers: [
databaseUrlToken.provideOverride((_) => 'postgres://prod.example.com'),
],
);
final childContext = WiretapContext(
providers: [
databaseToken.provideOverride((inject) {
// Can access parent's database URL
final url = inject(databaseUrlToken);
return Database(url, poolSize: 5);
}),
],
parent: parentContext,
);
When to Use Context Hierarchies#
Good Use Cases#
Testing: Create a child context per test that overrides real implementations with mocks:
final appContext = WiretapContext(providers: [
userServiceToken.provideOverride((_) => UserService()),
apiClientToken.provideOverride((_) => HttpApiClient()),
]);
// In your test
final testContext = WiretapContext(
providers: [
apiClientToken.provideOverride((_) => MockApiClient()),
],
parent: appContext,
);
App navigation and sub-sections: In Flutter apps, create child contexts for different screens or sections that refine dependencies for that scope. For example, a shopping cart screen might inherit the app's database and user service, but override the cart repository with a fresh instance:
final appContext = createAppContext();
// Create context for shopping cart section
final cartContext = WiretapContext(
providers: [
cartRepositoryToken.provideOverride((_) => ShoppingCartRepository()),
notificationServiceToken.provideOverride((_) => CartNotificationService()),
],
parent: appContext,
);
This allows different parts of your app to have specialized dependencies while still sharing common infrastructure like logging, analytics, and API clients.
Use with Care#
While context hierarchies are powerful, they significantly increase the complexity of your dependency graph. When you introduce parent-child relationships, dependencies can be resolved from multiple levels, making it harder to understand where a value comes from and what overrides might affect it. Specifics like "fallback factories are only created at the root" or "collection providers don't affect parents" aren't immediately clear and add cognitive overhead.
Before using hierarchies, consider whether you can achieve the same result with simpler flat contexts or by restructuring your code. Factories with parameters offer a powerful and simpler alternative — instead of nested contexts, you can pass configuration to factory functions that create the services you need. Hierarchies are most valuable in specific scenarios like testing or app-wide navigation, but using them everywhere quickly becomes difficult to reason about.
If you do use hierarchies, keep them shallow (1-3 levels) and be clear about the use-case.