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
8 changes: 7 additions & 1 deletion .pubignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,11 @@ doc/
devtools_options.yaml
flutter_modular.png
CONTRIBUTING.md
CLAUDE.md

tool/
# NOTE: do NOT blanket-exclude `tool/` here. `dart pub publish` applies this
# root .pubignore to nested packages too (e.g. tool/docs_mcp, published as
# `flutter_modular_docs_mcp`), so excluding the dir would also strip the files
# that package needs to publish. Exclude only build artifacts instead.
tool/**/.dart_tool/
tool/**/build/
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## 7.0.1

- **Page-scoped BLoC/Cubit support.** New `Scoped.addStreamable<T>(ctor,
(t) => t.stream, (t) => t.close())` exposes the object itself via
`context.watch<T>()` (read its synchronous `state`, call its methods) while
rebuilds are driven by its stream — flutter_modular keeps **no dependency on
the `bloc` package** (stream/close are caller callbacks). Companion
`addListenable<T>(ctor, (t) => t.listenable, (t) => t.dispose())` for objects
whose reactivity is a `Listenable` property. See the docs for a suggested
`addBloc` extension covering both BLoC and Cubit.
- **`add<T>(ctor)`** — non-reactive page-scoped object, readable via
`context.read`/`watch` and disposed on unmount when it implements
`Disposable`. **Breaking:** replaces `addDisposable`, which is removed (the
`Disposable` interface is retained).
- `addChangeNotifier` reexpressed over `addListenable`;
`watch`/`read`/`Consumer`/`Selector` now accept any `Object` (not just
`Listenable`), so a non-`Listenable` reactive object can be exposed.

## 7.0.0-dev.1

Ground-up rewrite of flutter_modular. **Breaking:** the v6 API (`Module` with
Expand Down
139 changes: 139 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# CLAUDE.md — flutter_modular

Guia rápido para trabalhar neste repositório. Explicações simples + o que fazer
e o que **não** fazer. Leia antes de mexer no código ou na documentação.

## O que é este projeto

`flutter_modular` é um pacote Flutter de **injeção de dependência + gerência de
rotas**, com **estado escopo-de-página** (page-scoped). A versão atual é a **v7**,
uma reescrita do zero (branch `v7`).

> Era um monorepo (melos). Agora é **um único pacote** na raiz. `modular_core` foi
> achatado para dentro de `flutter_modular`, e `shelf_modular` está sendo
> descontinuado.

## Estrutura do repositório

```
lib/ # o pacote flutter_modular v7 (código publicado)
flutter_modular.dart # exports públicos
src/{app,module,navigation,route,state}/
test/ # testes do pacote (flutter test)
example/ # app de demonstração (rotas aninhadas, guards, DI, etc.)
doc/ # site de documentação (Docusaurus 3.10) — NÃO é o pacote
docs/ # markdown das docs (fonte da verdade do conteúdo)
tool/docs_mcp/ # servidor MCP em Dart que SERVE as docs (pacote pub.dev separado)
art/ # identidade visual (logo Modular)
```

## API v7 (use estes idiomas — não os da v6)

```dart
// Módulo = DI + rotas, declarado de forma funcional:
final appModule = createModule(register: (c) {
c
..addSingleton<Counter>(Counter.new)
..route('/', child: (ctx, state) => const HomePage())
..route('/details/:id', child: (ctx, state) => DetailsPage(id: state.params['id']!))
..module('/admin', module: adminModule);
});

// Bootstrap (ModularApp acima do MaterialApp):
ModularApp(module: appModule, child: AppRoot());
MaterialApp.router(routerConfig: ModularApp.routerConfigOf(context));

// Navegação:
context.pushNamed('/details/42'); // empilha página (push NÃO entra na URL)
context.navigate('/'); // troca a stack (dona da URL, reseta histórico)
context.pop(result); // volta entregando resultado ao pushNamed

// Estado page-scoped (criado e descartado junto com a rota):
c.route('/counter',
provide: (s) => s.addChangeNotifier<CounterVM>(CounterVM.new),
child: (ctx, state) => const CounterPage());
final vm = context.watch<CounterVM>(); // rebuild quando notifica
```

Modelo de rotas v7: a **URL representa a base da stack** (push não aparece na
URL); rotas são **relativas** (semântica de diretório); deep-link entra via
`defaultRouteName`; `navigatorKey`/`observers` ficam no `ModularApp`.

## Comandos comuns

```sh
# Pacote (raiz):
flutter test # roda os testes
flutter analyze # lint (usa flutterando_analysis)

# Exemplo:
cd example && flutter run

# Site de docs (Docusaurus):
cd doc && yarn install && yarn start # dev em http://localhost:3000
cd doc && yarn build # build de produção

# Servidor MCP de docs:
cd tool/docs_mcp && dart test # testes do servidor
```

## ✅ Faça (DO)

- Use a **API v7** (acima). Confira `README.md`, `lib/` e `example/` como fonte
da verdade da API.
- Rode `flutter test` e `flutter analyze` antes de concluir uma mudança no pacote.
- Siga **Conventional Commits** (`feat:`, `fix:`, `docs:`, `chore:` …) — veja
`CONTRIBUTING.md`.
- Mantenha o pacote raiz enxuto: só `lib/` (mais os metadados) vai para o pub.dev.
- Para o fluxo de release do MCP, siga o passo a passo em
[`tool/docs_mcp/CLAUDE.md`](tool/docs_mcp/CLAUDE.md).

## ❌ Não faça (DON'T)

- **Não** use a API da v6 (`extends Module`, `Modular.get`, `Modular.to.push`).
É a v7 agora.
- **Não** conserte os testes do `shelf_modular` — ele está sendo descontinuado.
- **Não** confie nas docs em `doc/docs/flutter_modular/**` para a API: o prosa
ainda descreve a **v6** e contradiz o README v7. Ao escrever docs novas, use a
API v7.
- **Não** edite `tool/docs_mcp/lib/src/generated/docs_data.g.dart` à mão — é
gerado (veja lembrete abaixo).
- **Não** faça blanket-exclude de `tool/` no `.pubignore` da raiz (veja gotcha).

## ⚠️ Lembretes importantes (tipo)

**Mexeu na documentação → rebuilde o MCP e republique.** As docs ficam
**embutidas em build time** dentro do servidor MCP. Editar `doc/docs` **não muda
nada** até regenerar o índice. Sempre que adicionar/alterar conteúdo em
`doc/docs`:

1. Regenere o índice embutido:
```sh
cd tool/docs_mcp && dart run bin/build_index.dart
```
(varre `doc/docs`: `intro.md`, `platforms.md`, `flutter_modular/**`; ignora
`legacy*/` e `shelf_modular/`). Isso reescreve `lib/src/generated/docs_data.g.dart`.
2. Verifique: `dart analyze` e `dart test` (ambos limpos).
3. **Bump de versão em DOIS lugares** (precisam bater): `pubspec.yaml` → `version:`
e `lib/src/server.dart` → `const String serverVersion`; adicione entrada no
`CHANGELOG.md`.
4. Commit (o `dart pub publish` só envia arquivos versionados no git).
5. `dart pub publish --dry-run` → depois `dart pub publish`.
6. Recompile o binário local para o Claude Code pegar o conteúdo novo:
```sh
dart compile exe bin/server.dart -o ~/.local/bin/flutter_modular_docs_mcp
```
(MCP carrega no início da sessão — abra uma sessão nova do Claude Code.)

Passo a passo completo: [`tool/docs_mcp/CLAUDE.md`](tool/docs_mcp/CLAUDE.md).

**Gotcha do `.pubignore`.** `dart pub publish` aplica o `.pubignore` da **raiz**
também aos pacotes aninhados (`tool/docs_mcp`). Se a raiz fizer blanket-exclude de
`tool/`, o publish do `flutter_modular_docs_mcp` sai com o archive **vazio**
("the pubspec is hidden", "LICENSE missing", "bin/server.dart does not exist").
Exclua apenas artefatos de build sob `tool/` (`tool/**/.dart_tool/`,
`tool/**/build/`), não a pasta inteira.

**Os docs estão atrasados (v6).** A reescrita da prosa de `doc/docs` para a API
v7 é trabalho em aberto. Até lá, o MCP serve conteúdo v6 — regenerar só re-embute
o que estiver em `doc/docs`.
99 changes: 93 additions & 6 deletions doc/docs/flutter_modular/state-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final productsModule = createModule(
'/:id',
provide: (s) {
s
..addDisposable<RealtimeConnection>(RealtimeConnection.new)
..add<RealtimeConnection>(RealtimeConnection.new)
..addChangeNotifier<ProductDetailViewModel>(ProductDetailViewModel.new)
..addStream<int>(_viewersStream);
},
Expand All @@ -40,15 +40,20 @@ final productsModule = createModule(
);
```

The `Scoped` registrar (`s`) offers three kinds of registration:
The rule is **`addChangeNotifier`** (a reactive view model) and **`addStream`**
(stream‑backed state); **`add`** registers a plain non‑reactive object. The `Scoped`
registrar (`s`) offers:

| Method | For | Reactive? | Disposed on unmount? |
|---|---|---|---|
| `addChangeNotifier<T>(ctor)` | a `ChangeNotifier` view model | ✅ via `watch`/`read` | ✅ `dispose()` |
| `addDisposable<T>(ctor)` | a non‑reactive resource (socket, use‑case) | ❌ | ✅ `dispose()` |
| `addStream<T>(create)` | stream‑backed state | ✅ as `StreamValue<T>` | ✅ cancels the subscription |
| `add<T>(ctor)` | a non‑reactive object (socket, use‑case, config) | ❌ | ✅ if it implements `Disposable` |

Reactivity and lifecycle are **independent**: a thing can have either, both, or neither.
For reactive objects that don't fit the two rules above — a **BLoC**, a **Cubit**, a
controller exposing a `Listenable` — there are two escape hatches,
[`addStreamable` and `addListenable`](#exceptions-addstreamable-and-addlistenable).

### addChangeNotifier — a reactive view model

Expand All @@ -72,11 +77,12 @@ class ProductListViewModel extends ChangeNotifier {
}
```

### addDisposable — a non‑reactive resource
### add — a non‑reactive resource

For something that needs lifecycle but no reactivity — a connection, a subscription
manager, a use‑case holding a handle. It is built as a per‑page singleton, so a view
model can **inject the same instance**, and `dispose()`d on exit:
model can **inject the same instance**. If it implements `Disposable`, it is
`dispose()`d on exit — `add` **always** checks for `Disposable`:

```dart
class RealtimeConnection implements Disposable {
Expand Down Expand Up @@ -113,6 +119,87 @@ Stream<int> _viewersStream() =>
final viewers = context.watch<StreamValue<int>>().value; // latest int, or null
```

## Exceptions: addStreamable and addListenable

`addChangeNotifier` and `addStream` cover the common cases. When an object's reactivity
lives on a **property** — its `stream`, or a `Listenable` it exposes — and you want to
expose the **object itself** (to read its synchronous state and call its methods), reach
for these two escape hatches. Each takes a factory, a selector for the reactive source,
and a (required) dispose callback:

- `addStreamable<T>(ctor, (t) => t.stream, (t) => t.close())` — reactivity is a `Stream`.
`context.watch<T>()` returns the object; rebuilds fire on each emission.
- `addListenable<T>(ctor, (t) => t.someListenable, (t) => t.dispose())` — reactivity is a
`Listenable` property.

```dart
// A controller that is NOT a ChangeNotifier but exposes one:
class SearchController {
final ValueNotifier<String> query = ValueNotifier('');
void dispose() => query.dispose();
}

provide: (s) => s.addListenable<SearchController>(
SearchController.new,
(c) => c.query, // the rebuild trigger
(c) => c.dispose(), // cleanup on unmount
);
```

:::note
Prefer `addChangeNotifier`/`addStream`. Use `addStreamable`/`addListenable` only when the
reactive source is a property of the object you want to expose.
:::

## BLoC and Cubit

A **BLoC** or **Cubit** is exactly the streamable case: it exposes a synchronous `state`,
a `stream` of changes, and an async `close()`. Register it with `addStreamable` —
`context.watch<T>()` returns the **BLoC/Cubit** itself, so you read `state` directly and
rebuilds are driven by its stream:

```dart
// CounterCubit is a Cubit from the `bloc` package.
route(
'/counter',
provide: (s) => s.addStreamable<CounterCubit>(
CounterCubit.new,
(c) => c.stream,
(c) => c.close(),
),
child: (ctx, state) {
final counter = ctx.watch<CounterCubit>(); // the Cubit itself
return Text('${counter.state}'); // read its synchronous state
},
);
```

flutter_modular has **no dependency on the `bloc` package** — `addStreamable` takes the
`stream` and `close` as callbacks. To make this a one‑liner, add a small extension on
`Scoped` in your app. Because both **BLoC** and **Cubit** extend `BlocBase` (which has
`.stream` and `.close()`), a single `addBloc` covers both:

```dart
import 'package:bloc/bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';

/// Registers a page-scoped BLoC or Cubit: reactive via its stream, closed on unmount.
extension BlocScoped on Scoped {
void addBloc<B extends BlocBase<Object?>>(B Function() create) =>
addStreamable<B>(create, (b) => b.stream, (b) => b.close());
}
```

```dart
// Now registering any BLoC or Cubit is one line:
provide: (s) => s.addBloc<CounterCubit>(CounterCubit.new),
```

:::tip
With the extension above, `addBloc<MyBloc>(MyBloc.new)` works for both **BLoC** and
**Cubit** — one line to get a page‑scoped, auto‑closed, reactive instance.
:::

## Reading state: `watch` and `read`

From any descendant of the page, reach a provided `Listenable`:
Expand Down Expand Up @@ -201,7 +288,7 @@ abstract interface class Disposable {
}
```

Implement it and register with `addDisposable` (page‑scoped) — Modular builds it in the
Implement it and register with `add` (page‑scoped) — Modular builds it in the
page‑local injector and calls `dispose()` on unmount. Feature‑module binds that
implement `Disposable` (or `ChangeNotifier`) are likewise disposed when the feature
leaves the stack; see [DI lifecycle](./dependency-injection.md#bind-lifecycle).
Expand Down
4 changes: 2 additions & 2 deletions doc/publish-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# ./publish-docker.sh [TAG] # TAG defaults to "latest"
#
# Environment variables:
# DOCKER_USER Docker Hub user/org (default: flutterando)
# DOCKER_USER Docker Hub user/org (default: jacobmoura7)
# IMAGE_NAME repository name (default: modular-docs)
# PLATFORMS target platforms (default: linux/amd64,linux/arm64)
# DOCKER_PASSWORD if set (with DOCKER_USER), used for a non-interactive login
Expand All @@ -19,7 +19,7 @@ set -euo pipefail
# Always run from the doc/ directory (where the Dockerfile lives).
cd "$(dirname "$0")"

DOCKER_USER="${DOCKER_USER:-flutterando}"
DOCKER_USER="${DOCKER_USER:-jacobmoura7}"
IMAGE_NAME="${IMAGE_NAME:-modular-docs}"
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
TAG="${1:-latest}"
Expand Down
7 changes: 4 additions & 3 deletions example/lib/app/products/data/realtime_connection.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_modular/flutter_modular.dart';

/// `addDisposable` demo: a NON-reactive resource opened while the detail page is
/// alive and closed on exit. It is injected into the detail VM (the same
/// page-scoped instance), showing reactivity and lifecycle are independent.
/// `add` demo: a NON-reactive resource opened while the detail page is alive and
/// closed on exit. Registered with `add` and cleaned up because it implements
/// [Disposable]. It is injected into the detail VM (the same page-scoped
/// instance), showing reactivity and lifecycle are independent.
class RealtimeConnection implements Disposable {
bool isOpen = true;

Expand Down
4 changes: 2 additions & 2 deletions example/lib/app/products/products_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import 'viewmodels/product_list_view_model.dart';
/// live in `pages/` and `viewmodels/`.
///
/// Demonstrates: page-scoped view models (`addChangeNotifier`), params to the
/// view (`/:id`), `addDisposable` (non-reactive resource), and `addStream`.
/// view (`/:id`), `add` (non-reactive `Disposable` resource), and `addStream`.
/// ---------------------------------------------------------------------------

/// `addStream` demo: a live "people viewing" ticker.
Expand All @@ -36,7 +36,7 @@ final productsModule = createModule(
'/:id',
provide: (s) {
s
..addDisposable<RealtimeConnection>(RealtimeConnection.new)
..add<RealtimeConnection>(RealtimeConnection.new)
..addChangeNotifier<ProductDetailViewModel>(
ProductDetailViewModel.new,
)
Expand Down
6 changes: 4 additions & 2 deletions lib/flutter_modular.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
///
/// Navigator 2.0 (route matching + page stack + guards + transitions), the
/// module system (`createModule` / `ModularContext`), page-scoped state
/// (`provide` / `Scoped` + `context.watch`/`read`, `Consumer`/`Selector`,
/// `addStream`), and nested routes (`children` + `RouterOutlet`).
/// (`provide` / `Scoped` + `context.watch`/`read`, `Consumer`/`Selector`;
/// `addChangeNotifier`/`addStream` as the rule, `addStreamable`/`addListenable`
/// for BLoC/Cubit-style objects, `add` for non-reactive resources), and nested
/// routes (`children` + `RouterOutlet`).
library;

export 'src/app/modular_app.dart';
Expand Down
Loading
Loading