Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## 7.1.0

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

## 7.0.3

- **Customizable route transitions.** `route(transition:)` and the new app-wide
Expand Down
86 changes: 86 additions & 0 deletions doc/docs/flutter_modular/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,92 @@ with a single page, prefer page‑scoped [`provide`](./state-management.md) over
bind.
:::

## Sharing dependencies across modules

Put a dependency that many features need — a config, an HTTP client, a session — in
a **root‑owned** (path‑less) "core" module, and depend on it **by type** anywhere.
You never thread it by hand: a feature's own binds, and its page‑scoped
[`provide`](./state-management.md) binds, both resolve it from the graph.

```dart
// CORE (root-owned): the shared dependency, registered once
final coreModule = createModule(
register: (c) => c.addInstance<ApiConfig>(ApiConfig('https://api.example')),
);

// FEATURE: its OWN bind depends on the core ApiConfig — supplied automatically
final ordersModule = createModule(
path: '/orders',
register: (c) => c
..add<OrdersGateway>(OrdersGateway.new) // OrdersGateway(ApiConfig) ← from core
..route('/', child: (ctx, state) => const OrdersPage()),
);

final appModule = createModule(
register: (c) => c
..module(coreModule) // included once at the root → visible to every feature
..module(ordersModule), // takes NO parameters; resolves ApiConfig by type
);
```

You include the shared module **once at the root** — there is no need to re‑import it
in each feature (simpler than Angular's per‑feature `SharedModule`). Because the binds
are root‑owned, every feature sees them.

:::info Precedence — local shadows the core
If a feature registers its **own** bind of the same type, that local bind wins; the
core bind is the fallback. So a feature can override a shared default for itself
without affecting the rest of the app.
:::

:::note Requires 7.1.0+
Resolving a core dependency from a feature's **module‑level** bind needs
flutter_modular **7.1.0** (`auto_injector >= 2.2.0`). Page‑scoped `provide` binds
resolved the core in earlier versions too.
:::

## Async bootstrap (Hive, SharedPreferences, a DB connection)

`register` is **synchronous**, but some shared dependencies need an `await` to come up
— opening a Hive box, reading `SharedPreferences`, connecting a database. The idiom is
to do that `await` **once**, in a builder that *returns the module*, and capture the
ready instances in its closure. `main` stays thin and features take no parameters:

```dart
Future<Module> buildCoreModule() async {
await Hive.initFlutter();
final box = await Hive.openBox<dynamic>('app'); // awaited once, here
return createModule(
register: (c) => c
// the raw box stays private in the closure — expose the CONTRACT
..addInstance<SettingsRepository>(HiveSettingsRepository(box)),
);
}

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final core = await buildCoreModule(); // one await, one instance
runApp(ModularApp(module: buildAppModule(core), child: const AppRoot()));
}

Module buildAppModule(Module core) => createModule(
register: (c) => c
..module(core) // shared, root-owned — features resolve it by type
..module(ordersModule), // no parameters threaded in
);
```

Blocking on the bootstrap once, then registering the *ready* instances with
`addInstance`, keeps the rest of the app synchronous — no loading states sprinkled
through your widgets. Combined with cross‑module resolution above, `ordersModule`'s
binds resolve `SettingsRepository` from the core with **zero** parameter threading.

:::warning Build the async module once
`buildCoreModule()` returns a **new** module each call, and composition dedups by
identity — so call it **once** and reference that single instance. Calling it twice
would open the Hive box twice and register two distinct modules.
:::

## Next

- Bind state to a page's lifecycle → [State management](./state-management.md)
Expand Down
2 changes: 1 addition & 1 deletion lib/src/module/module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ class ModuleManager {
}
root
..uncommit()
..addInjector(injector)
..addInjector(injector, resolveUpward: true)
..commit();
}

Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_modular
description: Smart project structure with dependency injection and route management for Flutter.
version: 7.0.3
version: 7.1.0
homepage: https://github.com/Flutterando/modular
repository: https://github.com/Flutterando/modular
issue_tracker: https://github.com/Flutterando/modular/issues
Expand All @@ -9,7 +9,7 @@ environment:
sdk: ">=3.11.0 <4.0.0"

dependencies:
auto_injector: ">=2.1.0 <3.0.0"
auto_injector: ">=2.2.0 <3.0.0"
meta: ">=1.3.0 <2.0.0"
web: ">=0.5.0 <2.0.0"
flutter:
Expand Down
71 changes: 71 additions & 0 deletions test/feature_resolves_core_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:flutter_test/flutter_test.dart';

/// A shared (root-owned) dependency, registered in a path-less "core" module.
class ApiConfig {
ApiConfig(this.baseUrl);
final String baseUrl;
}

/// A FEATURE module-level service whose constructor needs the core [ApiConfig].
/// Before 7.1.0 this could not resolve — a feature's binds ran in a leaf
/// injector blind to root-owned binds. With `auto_injector >= 2.2.0` and
/// `_bind`'s `resolveUpward`, it resolves the core dep.
class FeatureService {
FeatureService(this.config);
final ApiConfig config;
}

final coreModule = createModule(
register: (c) => c.addInstance<ApiConfig>(ApiConfig('https://api.example')),
);

String? resolvedBaseUrl;

final featureModule = createModule(
path: '/feature',
register: (c) => c
// MODULE-LEVEL bind (eager singleton), depends on the core ApiConfig.
..addSingleton<FeatureService>(FeatureService.new)
..route('/', child: (ctx, s) {
resolvedBaseUrl = inject<FeatureService>().config.baseUrl;
return const Scaffold(body: Text('feature'));
}),
);

final rootModule = createModule(
register: (c) => c
..route('/', child: (ctx, s) => Scaffold(
body: TextButton(
onPressed: () => ctx.pushNamed('/feature'),
child: const Text('open'),
),
))
..module(coreModule)
..module(featureModule),
);

void main() {
testWidgets(
'a feature module-level bind resolves a root-owned (core) dependency',
(tester) async {
resolvedBaseUrl = null;
final boot = bootstrapModule(rootModule);
await tester.pumpWidget(MaterialApp.router(
routerConfig: modularRouterConfig(
boot.routes,
injector: boot.injector,
manager: boot.manager,
),
));
await tester.pumpAndSettle();

await tester.tap(find.text('open'));
await tester.pumpAndSettle();

expect(find.text('feature'), findsOneWidget);
expect(resolvedBaseUrl, 'https://api.example');
},
);
}
Loading