Skip to content
Merged

V7 #1055

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
2 changes: 2 additions & 0 deletions .pubignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ doc/
devtools_options.yaml
flutter_modular.png
CONTRIBUTING.md

tool/
45 changes: 25 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,36 @@ If it's for the Flutterando version of the template just send a message to us (o
-->


<h1 align="center">Flutter Modular</h1>


<!-- PROJECT LOGO -->
<!-- PROJECT LOGO & BRAND -->
<br />
<div align="center">
<a href="https://github.com/othneildrew/Best-README-Template">
<img src="https://raw.githubusercontent.com/Flutterando/modular/master/flutter_modular.png" alt="Logo" width="170" style=" padding-right: 30px;">
</a>
<a href="https://github.com/Flutterando/README-Template/">
<img src="https://raw.githubusercontent.com/Flutterando/README-Template/master/readme_assets/logo-flutterando.png" alt="Logo" width="95">
<a href="https://modular.flutterando.com.br">
<img src="art/modular-logo.png" alt="Modular" width="120">
</a>

<br />
<h1 align="center" style="letter-spacing: -0.02em;">Flutter Modular</h1>

<p align="center">
Welcome to Flutter Modular!
A smart project structure.
<br>
<br>
<a href="https://modular.flutterando.com.br/docs/intro">View Example</a>
<strong>A smart, modular project structure</strong>
<br />
Route management, dependency injection and scoped state — organized by feature, the Flutter way.
</p>

<p align="center">
<a href="https://pub.dev/packages/flutter_modular"><img src="https://img.shields.io/badge/flutter__modular-v7-a77b00?style=for-the-badge&labelColor=241606" alt="flutter_modular v7"></a>
<a href="https://pub.dev/packages/flutter_modular"><img src="https://img.shields.io/pub/v/flutter_modular?style=for-the-badge&label=pub&color=a77b00&labelColor=241606" alt="pub version"></a>
<a href="https://github.com/Flutterando/modular/blob/master/LICENSE"><img src="https://img.shields.io/github/license/Flutterando/modular?style=for-the-badge&color=a77b00&labelColor=241606" alt="license"></a>
</p>

<p align="center">
<a href="https://modular.flutterando.com.br/docs/intro">Documentation</a>
·
<a href="https://github.com/Flutterando/modular/issues">Report Bug</a>
·
<a href="https://github.com/Flutterando/modular/issues">Request Feature</a>
</p>

<sub>Maintained by <a href="https://www.flutterando.com.br">Flutterando</a></sub>
</div>

<br>
Expand Down Expand Up @@ -168,10 +173,10 @@ Go to the next topic and start your journey towards an intelligent structure.
- Yes, the dependency injection system is agnostic to any kind of class
including the reactivity that makes up state management.

- Can I use dynamic routes or Wildcards?
- Yes! The entire route tree responds as on the Web. Therefore, you can use dynamic parameters,
query, fragments or simply include a wildcard to enable a redirect
to a 404 page for example.
- Can I use dynamic routes?
- Yes! The entire route tree responds as on the Web. You can use dynamic `:params`,
query strings, relative paths, nested routes and persistent shells (`RouterOutlet`).
Use a route guard to redirect (e.g. to a 404 or a login page).

- Do I need to create a Module for all features?
- No. You can create a module only when you think it's necessary or when the feature is no longer a part of
Expand All @@ -186,7 +191,7 @@ Go to the next topic and start your journey towards an intelligent structure.

```yaml
dependencies:
flutter_modular: ^7.0.0-dev.1
flutter_modular: ^7.0.0
```

or run `flutter pub add flutter_modular`.
Expand Down
Binary file added art/modular-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions doc/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Keep the build context small and reproducible.
node_modules
build
.docusaurus
.cache-loader
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
30 changes: 30 additions & 0 deletions doc/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# syntax=docker/dockerfile:1

# Modular documentation site (Docusaurus 3) — built once, served as static files.

# ---- Build stage: compile the Docusaurus static site ----------------------
FROM node:20-alpine AS build
WORKDIR /app

# Install dependencies first so this layer is cached unless the lockfile changes.
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# Build the static site into /app/build.
COPY . .
RUN yarn build

# ---- Runtime stage: serve the static output with nginx --------------------
FROM nginx:1.27-alpine AS runtime

# Clean-URL / caching config (Docusaurus emits per-route folders + a 404.html).
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Static build output from the build stage.
COPY --from=build /app/build /usr/share/nginx/html

EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -q --spider http://localhost/ || exit 1

CMD ["nginx", "-g", "daemon off;"]
4 changes: 2 additions & 2 deletions doc/docs/flutter_modular/_category_.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"label": "Flutter Modular",
"position": 3,
"label": "flutter_modular",
"position": 2,
"collapsed": false
}
225 changes: 72 additions & 153 deletions doc/docs/flutter_modular/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,186 +2,105 @@
sidebar_position: 3
---

# Dependency Injection
# Dependency injection

We generally code with maintainability and scalability in mind, applying project-specific patterns
to a given function and improving the structure of our code. We must pay attention to our code,
otherwise it can become a hidden problem. Let's look at a practical example:
Modular's DI is powered by [`auto_injector`](https://pub.dev/packages/auto_injector).
You register constructors on a module's `ModularContext`; Modular resolves each
dependency's own constructor arguments from the graph automatically.

```dart
class Client {
void sendEmail(String email, String title, String body){
final xpto = XPTOEmail();
xpto.sendEmail(email, title, body);
}
}
```

Here we have a **Client** class with a method called **sendEmail()** running the send routine on **XPTOEmail** class instance.
Despite being a simple and functional approach, having a class instance within the method, it presents some problems:

- Makes it impossible to replace the instance `xpto`.
- Makes Unit Tests more difficult, as you would not be able to create `XPTOEmail()` Fake/Mock instance.
- Entirely dependent on the functioning of an external class.

We call it "Dependency Coupling" when we use an outer class in this way, because the *Client* class
is totally dependent on the functioning of the **XPTOEmail** object.

To break a class's bond with its dependency, we generally prefer to "inject" the dependency instances through a constructor, setters, or methods. That's what we call "Dependency Injection".

Let's fix the **Customer** class by injecting the **XPTOEmail** instance by constructor:

```dart
class Client {

final XPTOEmail xpto;
Client(this.xpto);
## Registering dependencies

void sendEmail(String email, String title, String body){
xpto.sendEmail(email, title, body);
}
}
```
This way, we reduce the coupling **XPTOEmail** object has to the **Client** object.

We still have a problem with this implementation. Despite *cohesion*, the Client class has a dependency on an external source, and even being injected by constructor, replacing it with another email service would not be a simple task.
Our code still has coupling, but we can improve this using `interfaces`. Let's create an interface
to define a signature, or "contract" for the **sendEmail** method. With this in place, any class that implements this interface can be injected into the class **Client**:
Inside `register`, declare dependencies on the context (`c`):

```dart
abstract class EmailService {
void sendEmail(String email, String title, String body);
}

class XPTOEmailService implements EmailService {

final XPTOEmail xpto;
XPTOEmailService(this.xpto);

void sendEmail(String email, String title, String body) {
xpto.sendEmail(email, title, body);
}
}
final coreModule = createModule(
register: (c) {
c
..addSingleton<ProductService>(ProductService.new)
..addSingleton<ProductRepository>(ProductRepository.new) // gets ProductService injected
..addSingleton<AppSession>(AppSession.new);
},
);
```

So we can create implementations of any email services. Finally, let's replace the dependency on
XPTOEmail by the EmailService interface:
| Method | Builds | Lifetime |
|---|---|---|
| `add<T>(T Function(...))` | a **new** instance every time it is resolved | per resolution |
| `addSingleton<T>(T Function(...))` | once, **eagerly** at registration | one shared instance |
| `addLazySingleton<T>(T Function(...))` | once, on **first** resolution | one shared instance |
| `addInstance<T>(T value)` | nothing — registers an existing object | the value you pass |

```dart
class Client {

final EmailService service;
Client(this.service);
You pass a **constructor reference** (e.g. `ProductRepository.new`), not an instance.
`auto_injector` reads its parameters and supplies them from the graph, so a repository
that takes a service in its constructor just works once both are registered.

void sendEmail(String email, String title, String body){
service.sendEmail(email, title, body);
}
}
```
## Resolving with `inject<T>()`

Then We create the **Client** instance:
Constructor injection covers most needs — a class declares what it needs as constructor
parameters and the graph fills them in. When you need to reach a dependency from a place
that has no constructor — a route **guard**, a callback, a top‑level function — use
`inject<T>()`:

```dart
//dependencies
final xpto = XPTOEmail();
final service = XPTOEmailService(xpto)

// instance
final client = Client(service);
c.route(
'/settings/secret',
// inject<T>() reads DI at guard-eval time without exposing the injector object.
guards: [(state) => inject<AppSession>().unlocked ? null : '/home/settings'],
child: (ctx, state) => const SecretPage(),
);
```

This object creation method solves coupling issues but may increase instance creation complexity, as we can see in the **Client** class. The **flutter_modular** Dependency Injection System solves this problem simply and effectively.

## Instance registration
`inject<T>()` resolves `T` from the **live** module graph (the injector object itself
stays private — Angular‑style). It throws a `StateError` if no Modular app has
bootstrapped yet, and because it reads the live graph, a feature module's dependencies
are reachable through it only while that feature is active.

The strategy for building an instance with its dependencies comprise register all objects in a module and
manufactures them on demand or in single-instance form(singleton). All instance registration process
is managed by [auto_injector](https://pub.dev/packages/auto_injector)
## Bind lifecycle

There are a few ways to build a Bind to register object instances:
*Where* a dependency is registered decides *how long* it lives — this is the other half
of the module `path` rule from [Modules](./module.md#a-modules-path-decides-what-it-is).

### Root‑owned (shared DI, never disposed)

- *injector.add*: Build an instance on demand (Factory).
- *injector.addSingleton*: Build an instance only once when the module starts.
- *injector.addLazySingleton*: Build an instance only once when prompted.
- *injector.addInstance*: Adds an existing instance.

We register the binds in **AppModule**:

```dart
class AppModule extends Module {
@override
void binds(i) {
i.add(XPTOEmail.new);
i.add<EmailService>(XPTOEmailService.new);
i.addSingleton(Client.new);

// Register with Key
i.addSingleton(Client.new, key: 'OtherClient');
}
...
}
```
The dependencies of these instances will be resolved automatically using the [auto_injector](https://pub.dev/packages/auto_injector) mechanisms.

To get a resolved instance use `Modular.get`:
Dependencies in a **path‑less** module are committed **eagerly** at bootstrap and live
for the whole app. They are your durable truth — repositories, services, sessions. A
leaving route never disposes them.

```dart
final client = Modular.get<Client>();

// or set a default value
final client = Modular.get<Client>(defaultValue: Client());

// or use tryGet
Client? client = Modular.tryGet<Client>();

// or get with key
Client client = Modular.get(key: 'OtherCLient');
final coreModule = createModule( // no path → root-owned
register: (c) => c.addSingleton<ProductRepository>(ProductRepository.new),
);
```

## Auto Dispose

The lifetime of a Bind singleton ends when its module 'dies'. But there are some objects that, by default,
run an instance destruction routine and are automatically removed from memory. Here they are:

- Stream/Sink (Dart Native).
- ChangeNotifier/ValueNotifier (Flutter Native).
### Feature‑scoped (bound on entry, disposed on exit)

For registered objects that are not part of this list, we can use **BindConfig** or implement a **Disposable** interface on the instance where we want to run an algorithm before dispose:

**Using BindConfig**:

The dispose of an instance can be set directly in `Register` by implementing the `onDispose` property:
Dependencies in a module **with a `path`** are bound lazily when the feature's **first
route enters** the stack, and disposed when its **last route leaves**. Disposal is
automatic for `ChangeNotifier`s and [`Disposable`](./state-management.md#disposable)s —
their `dispose()` is called.

```dart
@override
void binds(i) {
i.addSingleton<MyBloc>(MyBloc.new, config: BindConfig(
onDispose: (bloc) => bloc.close(),
));
}
final productsModule = createModule(
path: '/products', // feature → binds disposed when it leaves
register: (c) {
c.add<ProductSearchController>(ProductSearchController.new);
// ...routes...
},
);
```

**Using Disposable interface**

Doing this does not require **BindConfig**, but creates a link between the package and the class.
This mirrors how the "active path list" worked in 6.x: a feature's resources exist only
while the feature is on screen.

```dart
class MyController implements Disposable {
final controller = StreamController();

@override
void dispose() {
controller.close();
}
}
```
:::tip Where should a dependency live?
Put the **source of truth** (repositories, services, app session) in a root‑owned
module — it must outlive any single page. Put **feature‑local** machinery in the
feature's own module so it is cleaned up when the user leaves. For state that is 1:1
with a single page, prefer page‑scoped [`provide`](./state-management.md) over a module
bind.
:::

## Next

**flutter_modular** also offers a singleton removal option from the dependency injection system
by calling the **Modular.dispose**() method even with an active module:

```dart
Modular.dispose<MySingletonBind>();
```

- Bind state to a page's lifecycle → [State management](./state-management.md)
- Use dependencies in guards → [Navigation › Guards](./navigation.md#guards)
5 changes: 5 additions & 0 deletions doc/docs/flutter_modular/legacy-6/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"label": "(Legacy) Modular 6",
"position": 31,
"collapsed": true
}
Loading
Loading