Skip to content

Commit 0f41054

Browse files
authored
Merge pull request #1060 from Flutterando/feat/resolve-upward
feat(di): feature modules resolve shared/core deps (7.1.0)
2 parents b2a09f7 + 8b0e54a commit 0f41054

5 files changed

Lines changed: 177 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Changelog
22

3+
## 7.1.0
4+
5+
- **Feature modules can now consume shared/core dependencies directly.** A
6+
module-level bind inside a feature (a module with a `path`) — `addSingleton`,
7+
`add`, `addLazySingleton` — now resolves dependencies registered in a
8+
root-owned shared module (a path-less `module(...)`, the "core"). Previously
9+
only page-scoped `provide` binds could reach the core; a feature's
10+
module-level binds ran in a leaf injector that couldn't see root-owned binds,
11+
forcing shared deps to be threaded in by hand. This removes that asymmetry:
12+
**a core consumer can be a `provide`, the core itself, OR a feature
13+
module-level bind** — all alike. A feature's own bind still shadows a
14+
same-typed core bind (local wins; core is the fallback).
15+
- Requires `auto_injector >= 2.2.0`, which adds the opt-in upward resolution
16+
(`addInjector(child, resolveUpward: true)`) this builds on, fixes a
17+
dispose-listener accumulation in its layer graph, and adds an
18+
`UpwardResolutionCycle` guard against mutual upward links.
19+
320
## 7.0.3
421

522
- **Customizable route transitions.** `route(transition:)` and the new app-wide

doc/docs/flutter_modular/dependency-injection.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,92 @@ with a single page, prefer page‑scoped [`provide`](./state-management.md) over
100100
bind.
101101
:::
102102

103+
## Sharing dependencies across modules
104+
105+
Put a dependency that many features need — a config, an HTTP client, a session — in
106+
a **root‑owned** (path‑less) "core" module, and depend on it **by type** anywhere.
107+
You never thread it by hand: a feature's own binds, and its page‑scoped
108+
[`provide`](./state-management.md) binds, both resolve it from the graph.
109+
110+
```dart
111+
// CORE (root-owned): the shared dependency, registered once
112+
final coreModule = createModule(
113+
register: (c) => c.addInstance<ApiConfig>(ApiConfig('https://api.example')),
114+
);
115+
116+
// FEATURE: its OWN bind depends on the core ApiConfig — supplied automatically
117+
final ordersModule = createModule(
118+
path: '/orders',
119+
register: (c) => c
120+
..add<OrdersGateway>(OrdersGateway.new) // OrdersGateway(ApiConfig) ← from core
121+
..route('/', child: (ctx, state) => const OrdersPage()),
122+
);
123+
124+
final appModule = createModule(
125+
register: (c) => c
126+
..module(coreModule) // included once at the root → visible to every feature
127+
..module(ordersModule), // takes NO parameters; resolves ApiConfig by type
128+
);
129+
```
130+
131+
You include the shared module **once at the root** — there is no need to re‑import it
132+
in each feature (simpler than Angular's per‑feature `SharedModule`). Because the binds
133+
are root‑owned, every feature sees them.
134+
135+
:::info Precedence — local shadows the core
136+
If a feature registers its **own** bind of the same type, that local bind wins; the
137+
core bind is the fallback. So a feature can override a shared default for itself
138+
without affecting the rest of the app.
139+
:::
140+
141+
:::note Requires 7.1.0+
142+
Resolving a core dependency from a feature's **module‑level** bind needs
143+
flutter_modular **7.1.0** (`auto_injector >= 2.2.0`). Page‑scoped `provide` binds
144+
resolved the core in earlier versions too.
145+
:::
146+
147+
## Async bootstrap (Hive, SharedPreferences, a DB connection)
148+
149+
`register` is **synchronous**, but some shared dependencies need an `await` to come up
150+
— opening a Hive box, reading `SharedPreferences`, connecting a database. The idiom is
151+
to do that `await` **once**, in a builder that *returns the module*, and capture the
152+
ready instances in its closure. `main` stays thin and features take no parameters:
153+
154+
```dart
155+
Future<Module> buildCoreModule() async {
156+
await Hive.initFlutter();
157+
final box = await Hive.openBox<dynamic>('app'); // awaited once, here
158+
return createModule(
159+
register: (c) => c
160+
// the raw box stays private in the closure — expose the CONTRACT
161+
..addInstance<SettingsRepository>(HiveSettingsRepository(box)),
162+
);
163+
}
164+
165+
Future<void> main() async {
166+
WidgetsFlutterBinding.ensureInitialized();
167+
final core = await buildCoreModule(); // one await, one instance
168+
runApp(ModularApp(module: buildAppModule(core), child: const AppRoot()));
169+
}
170+
171+
Module buildAppModule(Module core) => createModule(
172+
register: (c) => c
173+
..module(core) // shared, root-owned — features resolve it by type
174+
..module(ordersModule), // no parameters threaded in
175+
);
176+
```
177+
178+
Blocking on the bootstrap once, then registering the *ready* instances with
179+
`addInstance`, keeps the rest of the app synchronous — no loading states sprinkled
180+
through your widgets. Combined with cross‑module resolution above, `ordersModule`'s
181+
binds resolve `SettingsRepository` from the core with **zero** parameter threading.
182+
183+
:::warning Build the async module once
184+
`buildCoreModule()` returns a **new** module each call, and composition dedups by
185+
identity — so call it **once** and reference that single instance. Calling it twice
186+
would open the Hive box twice and register two distinct modules.
187+
:::
188+
103189
## Next
104190

105191
- Bind state to a page's lifecycle → [State management](./state-management.md)

lib/src/module/module.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ class ModuleManager {
187187
}
188188
root
189189
..uncommit()
190-
..addInjector(injector)
190+
..addInjector(injector, resolveUpward: true)
191191
..commit();
192192
}
193193

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: flutter_modular
22
description: Smart project structure with dependency injection and route management for Flutter.
3-
version: 7.0.3
3+
version: 7.1.0
44
homepage: https://github.com/Flutterando/modular
55
repository: https://github.com/Flutterando/modular
66
issue_tracker: https://github.com/Flutterando/modular/issues
@@ -9,7 +9,7 @@ environment:
99
sdk: ">=3.11.0 <4.0.0"
1010

1111
dependencies:
12-
auto_injector: ">=2.1.0 <3.0.0"
12+
auto_injector: ">=2.2.0 <3.0.0"
1313
meta: ">=1.3.0 <2.0.0"
1414
web: ">=0.5.0 <2.0.0"
1515
flutter:
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_modular/flutter_modular.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
/// A shared (root-owned) dependency, registered in a path-less "core" module.
6+
class ApiConfig {
7+
ApiConfig(this.baseUrl);
8+
final String baseUrl;
9+
}
10+
11+
/// A FEATURE module-level service whose constructor needs the core [ApiConfig].
12+
/// Before 7.1.0 this could not resolve — a feature's binds ran in a leaf
13+
/// injector blind to root-owned binds. With `auto_injector >= 2.2.0` and
14+
/// `_bind`'s `resolveUpward`, it resolves the core dep.
15+
class FeatureService {
16+
FeatureService(this.config);
17+
final ApiConfig config;
18+
}
19+
20+
final coreModule = createModule(
21+
register: (c) => c.addInstance<ApiConfig>(ApiConfig('https://api.example')),
22+
);
23+
24+
String? resolvedBaseUrl;
25+
26+
final featureModule = createModule(
27+
path: '/feature',
28+
register: (c) => c
29+
// MODULE-LEVEL bind (eager singleton), depends on the core ApiConfig.
30+
..addSingleton<FeatureService>(FeatureService.new)
31+
..route('/', child: (ctx, s) {
32+
resolvedBaseUrl = inject<FeatureService>().config.baseUrl;
33+
return const Scaffold(body: Text('feature'));
34+
}),
35+
);
36+
37+
final rootModule = createModule(
38+
register: (c) => c
39+
..route('/', child: (ctx, s) => Scaffold(
40+
body: TextButton(
41+
onPressed: () => ctx.pushNamed('/feature'),
42+
child: const Text('open'),
43+
),
44+
))
45+
..module(coreModule)
46+
..module(featureModule),
47+
);
48+
49+
void main() {
50+
testWidgets(
51+
'a feature module-level bind resolves a root-owned (core) dependency',
52+
(tester) async {
53+
resolvedBaseUrl = null;
54+
final boot = bootstrapModule(rootModule);
55+
await tester.pumpWidget(MaterialApp.router(
56+
routerConfig: modularRouterConfig(
57+
boot.routes,
58+
injector: boot.injector,
59+
manager: boot.manager,
60+
),
61+
));
62+
await tester.pumpAndSettle();
63+
64+
await tester.tap(find.text('open'));
65+
await tester.pumpAndSettle();
66+
67+
expect(find.text('feature'), findsOneWidget);
68+
expect(resolvedBaseUrl, 'https://api.example');
69+
},
70+
);
71+
}

0 commit comments

Comments
 (0)