Skip to content

Commit d1eef13

Browse files
authored
Merge pull request #1057 from Flutterando/v7
v7: scoped BLoC/Cubit support, docs MCP server & site, release 7.0.1
2 parents edb9cab + 4a38e70 commit d1eef13

19 files changed

Lines changed: 947 additions & 98 deletions

.pubignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,11 @@ doc/
3434
devtools_options.yaml
3535
flutter_modular.png
3636
CONTRIBUTING.md
37+
CLAUDE.md
3738

38-
tool/
39+
# NOTE: do NOT blanket-exclude `tool/` here. `dart pub publish` applies this
40+
# root .pubignore to nested packages too (e.g. tool/docs_mcp, published as
41+
# `flutter_modular_docs_mcp`), so excluding the dir would also strip the files
42+
# that package needs to publish. Exclude only build artifacts instead.
43+
tool/**/.dart_tool/
44+
tool/**/build/

CHANGELOG.md

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

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

523
Ground-up rewrite of flutter_modular. **Breaking:** the v6 API (`Module` with

CLAUDE.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# CLAUDE.md — flutter_modular
2+
3+
Guia rápido para trabalhar neste repositório. Explicações simples + o que fazer
4+
e o que **não** fazer. Leia antes de mexer no código ou na documentação.
5+
6+
## O que é este projeto
7+
8+
`flutter_modular` é um pacote Flutter de **injeção de dependência + gerência de
9+
rotas**, com **estado escopo-de-página** (page-scoped). A versão atual é a **v7**,
10+
uma reescrita do zero (branch `v7`).
11+
12+
> Era um monorepo (melos). Agora é **um único pacote** na raiz. `modular_core` foi
13+
> achatado para dentro de `flutter_modular`, e `shelf_modular` está sendo
14+
> descontinuado.
15+
16+
## Estrutura do repositório
17+
18+
```
19+
lib/ # o pacote flutter_modular v7 (código publicado)
20+
flutter_modular.dart # exports públicos
21+
src/{app,module,navigation,route,state}/
22+
test/ # testes do pacote (flutter test)
23+
example/ # app de demonstração (rotas aninhadas, guards, DI, etc.)
24+
doc/ # site de documentação (Docusaurus 3.10) — NÃO é o pacote
25+
docs/ # markdown das docs (fonte da verdade do conteúdo)
26+
tool/docs_mcp/ # servidor MCP em Dart que SERVE as docs (pacote pub.dev separado)
27+
art/ # identidade visual (logo Modular)
28+
```
29+
30+
## API v7 (use estes idiomas — não os da v6)
31+
32+
```dart
33+
// Módulo = DI + rotas, declarado de forma funcional:
34+
final appModule = createModule(register: (c) {
35+
c
36+
..addSingleton<Counter>(Counter.new)
37+
..route('/', child: (ctx, state) => const HomePage())
38+
..route('/details/:id', child: (ctx, state) => DetailsPage(id: state.params['id']!))
39+
..module('/admin', module: adminModule);
40+
});
41+
42+
// Bootstrap (ModularApp acima do MaterialApp):
43+
ModularApp(module: appModule, child: AppRoot());
44+
MaterialApp.router(routerConfig: ModularApp.routerConfigOf(context));
45+
46+
// Navegação:
47+
context.pushNamed('/details/42'); // empilha página (push NÃO entra na URL)
48+
context.navigate('/'); // troca a stack (dona da URL, reseta histórico)
49+
context.pop(result); // volta entregando resultado ao pushNamed
50+
51+
// Estado page-scoped (criado e descartado junto com a rota):
52+
c.route('/counter',
53+
provide: (s) => s.addChangeNotifier<CounterVM>(CounterVM.new),
54+
child: (ctx, state) => const CounterPage());
55+
final vm = context.watch<CounterVM>(); // rebuild quando notifica
56+
```
57+
58+
Modelo de rotas v7: a **URL representa a base da stack** (push não aparece na
59+
URL); rotas são **relativas** (semântica de diretório); deep-link entra via
60+
`defaultRouteName`; `navigatorKey`/`observers` ficam no `ModularApp`.
61+
62+
## Comandos comuns
63+
64+
```sh
65+
# Pacote (raiz):
66+
flutter test # roda os testes
67+
flutter analyze # lint (usa flutterando_analysis)
68+
69+
# Exemplo:
70+
cd example && flutter run
71+
72+
# Site de docs (Docusaurus):
73+
cd doc && yarn install && yarn start # dev em http://localhost:3000
74+
cd doc && yarn build # build de produção
75+
76+
# Servidor MCP de docs:
77+
cd tool/docs_mcp && dart test # testes do servidor
78+
```
79+
80+
## ✅ Faça (DO)
81+
82+
- Use a **API v7** (acima). Confira `README.md`, `lib/` e `example/` como fonte
83+
da verdade da API.
84+
- Rode `flutter test` e `flutter analyze` antes de concluir uma mudança no pacote.
85+
- Siga **Conventional Commits** (`feat:`, `fix:`, `docs:`, `chore:` …) — veja
86+
`CONTRIBUTING.md`.
87+
- Mantenha o pacote raiz enxuto: só `lib/` (mais os metadados) vai para o pub.dev.
88+
- Para o fluxo de release do MCP, siga o passo a passo em
89+
[`tool/docs_mcp/CLAUDE.md`](tool/docs_mcp/CLAUDE.md).
90+
91+
## ❌ Não faça (DON'T)
92+
93+
- **Não** use a API da v6 (`extends Module`, `Modular.get`, `Modular.to.push`).
94+
É a v7 agora.
95+
- **Não** conserte os testes do `shelf_modular` — ele está sendo descontinuado.
96+
- **Não** confie nas docs em `doc/docs/flutter_modular/**` para a API: o prosa
97+
ainda descreve a **v6** e contradiz o README v7. Ao escrever docs novas, use a
98+
API v7.
99+
- **Não** edite `tool/docs_mcp/lib/src/generated/docs_data.g.dart` à mão — é
100+
gerado (veja lembrete abaixo).
101+
- **Não** faça blanket-exclude de `tool/` no `.pubignore` da raiz (veja gotcha).
102+
103+
## ⚠️ Lembretes importantes (tipo)
104+
105+
**Mexeu na documentação → rebuilde o MCP e republique.** As docs ficam
106+
**embutidas em build time** dentro do servidor MCP. Editar `doc/docs` **não muda
107+
nada** até regenerar o índice. Sempre que adicionar/alterar conteúdo em
108+
`doc/docs`:
109+
110+
1. Regenere o índice embutido:
111+
```sh
112+
cd tool/docs_mcp && dart run bin/build_index.dart
113+
```
114+
(varre `doc/docs`: `intro.md`, `platforms.md`, `flutter_modular/**`; ignora
115+
`legacy*/` e `shelf_modular/`). Isso reescreve `lib/src/generated/docs_data.g.dart`.
116+
2. Verifique: `dart analyze` e `dart test` (ambos limpos).
117+
3. **Bump de versão em DOIS lugares** (precisam bater): `pubspec.yaml``version:`
118+
e `lib/src/server.dart``const String serverVersion`; adicione entrada no
119+
`CHANGELOG.md`.
120+
4. Commit (o `dart pub publish` só envia arquivos versionados no git).
121+
5. `dart pub publish --dry-run` → depois `dart pub publish`.
122+
6. Recompile o binário local para o Claude Code pegar o conteúdo novo:
123+
```sh
124+
dart compile exe bin/server.dart -o ~/.local/bin/flutter_modular_docs_mcp
125+
```
126+
(MCP carrega no início da sessão — abra uma sessão nova do Claude Code.)
127+
128+
Passo a passo completo: [`tool/docs_mcp/CLAUDE.md`](tool/docs_mcp/CLAUDE.md).
129+
130+
**Gotcha do `.pubignore`.** `dart pub publish` aplica o `.pubignore` da **raiz**
131+
também aos pacotes aninhados (`tool/docs_mcp`). Se a raiz fizer blanket-exclude de
132+
`tool/`, o publish do `flutter_modular_docs_mcp` sai com o archive **vazio**
133+
("the pubspec is hidden", "LICENSE missing", "bin/server.dart does not exist").
134+
Exclua apenas artefatos de build sob `tool/` (`tool/**/.dart_tool/`,
135+
`tool/**/build/`), não a pasta inteira.
136+
137+
**Os docs estão atrasados (v6).** A reescrita da prosa de `doc/docs` para a API
138+
v7 é trabalho em aberto. Até lá, o MCP serve conteúdo v6 — regenerar só re-embute
139+
o que estiver em `doc/docs`.

doc/docs/flutter_modular/state-management.md

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ final productsModule = createModule(
3030
'/:id',
3131
provide: (s) {
3232
s
33-
..addDisposable<RealtimeConnection>(RealtimeConnection.new)
33+
..add<RealtimeConnection>(RealtimeConnection.new)
3434
..addChangeNotifier<ProductDetailViewModel>(ProductDetailViewModel.new)
3535
..addStream<int>(_viewersStream);
3636
},
@@ -40,15 +40,20 @@ final productsModule = createModule(
4040
);
4141
```
4242

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

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

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

5358
### addChangeNotifier — a reactive view model
5459

@@ -72,11 +77,12 @@ class ProductListViewModel extends ChangeNotifier {
7277
}
7378
```
7479

75-
### addDisposable — a non‑reactive resource
80+
### add — a non‑reactive resource
7681

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

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

122+
## Exceptions: addStreamable and addListenable
123+
124+
`addChangeNotifier` and `addStream` cover the common cases. When an object's reactivity
125+
lives on a **property** — its `stream`, or a `Listenable` it exposes — and you want to
126+
expose the **object itself** (to read its synchronous state and call its methods), reach
127+
for these two escape hatches. Each takes a factory, a selector for the reactive source,
128+
and a (required) dispose callback:
129+
130+
- `addStreamable<T>(ctor, (t) => t.stream, (t) => t.close())` — reactivity is a `Stream`.
131+
`context.watch<T>()` returns the object; rebuilds fire on each emission.
132+
- `addListenable<T>(ctor, (t) => t.someListenable, (t) => t.dispose())` — reactivity is a
133+
`Listenable` property.
134+
135+
```dart
136+
// A controller that is NOT a ChangeNotifier but exposes one:
137+
class SearchController {
138+
final ValueNotifier<String> query = ValueNotifier('');
139+
void dispose() => query.dispose();
140+
}
141+
142+
provide: (s) => s.addListenable<SearchController>(
143+
SearchController.new,
144+
(c) => c.query, // the rebuild trigger
145+
(c) => c.dispose(), // cleanup on unmount
146+
);
147+
```
148+
149+
:::note
150+
Prefer `addChangeNotifier`/`addStream`. Use `addStreamable`/`addListenable` only when the
151+
reactive source is a property of the object you want to expose.
152+
:::
153+
154+
## BLoC and Cubit
155+
156+
A **BLoC** or **Cubit** is exactly the streamable case: it exposes a synchronous `state`,
157+
a `stream` of changes, and an async `close()`. Register it with `addStreamable`
158+
`context.watch<T>()` returns the **BLoC/Cubit** itself, so you read `state` directly and
159+
rebuilds are driven by its stream:
160+
161+
```dart
162+
// CounterCubit is a Cubit from the `bloc` package.
163+
route(
164+
'/counter',
165+
provide: (s) => s.addStreamable<CounterCubit>(
166+
CounterCubit.new,
167+
(c) => c.stream,
168+
(c) => c.close(),
169+
),
170+
child: (ctx, state) {
171+
final counter = ctx.watch<CounterCubit>(); // the Cubit itself
172+
return Text('${counter.state}'); // read its synchronous state
173+
},
174+
);
175+
```
176+
177+
flutter_modular has **no dependency on the `bloc` package**`addStreamable` takes the
178+
`stream` and `close` as callbacks. To make this a one‑liner, add a small extension on
179+
`Scoped` in your app. Because both **BLoC** and **Cubit** extend `BlocBase` (which has
180+
`.stream` and `.close()`), a single `addBloc` covers both:
181+
182+
```dart
183+
import 'package:bloc/bloc.dart';
184+
import 'package:flutter_modular/flutter_modular.dart';
185+
186+
/// Registers a page-scoped BLoC or Cubit: reactive via its stream, closed on unmount.
187+
extension BlocScoped on Scoped {
188+
void addBloc<B extends BlocBase<Object?>>(B Function() create) =>
189+
addStreamable<B>(create, (b) => b.stream, (b) => b.close());
190+
}
191+
```
192+
193+
```dart
194+
// Now registering any BLoC or Cubit is one line:
195+
provide: (s) => s.addBloc<CounterCubit>(CounterCubit.new),
196+
```
197+
198+
:::tip
199+
With the extension above, `addBloc<MyBloc>(MyBloc.new)` works for both **BLoC** and
200+
**Cubit** — one line to get a page‑scoped, auto‑closed, reactive instance.
201+
:::
202+
116203
## Reading state: `watch` and `read`
117204

118205
From any descendant of the page, reach a provided `Listenable`:
@@ -201,7 +288,7 @@ abstract interface class Disposable {
201288
}
202289
```
203290

204-
Implement it and register with `addDisposable` (page‑scoped) — Modular builds it in the
291+
Implement it and register with `add` (page‑scoped) — Modular builds it in the
205292
page‑local injector and calls `dispose()` on unmount. Feature‑module binds that
206293
implement `Disposable` (or `ChangeNotifier`) are likewise disposed when the feature
207294
leaves the stack; see [DI lifecycle](./dependency-injection.md#bind-lifecycle).

doc/publish-docker.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# ./publish-docker.sh [TAG] # TAG defaults to "latest"
77
#
88
# Environment variables:
9-
# DOCKER_USER Docker Hub user/org (default: flutterando)
9+
# DOCKER_USER Docker Hub user/org (default: jacobmoura7)
1010
# IMAGE_NAME repository name (default: modular-docs)
1111
# PLATFORMS target platforms (default: linux/amd64,linux/arm64)
1212
# DOCKER_PASSWORD if set (with DOCKER_USER), used for a non-interactive login
@@ -19,7 +19,7 @@ set -euo pipefail
1919
# Always run from the doc/ directory (where the Dockerfile lives).
2020
cd "$(dirname "$0")"
2121

22-
DOCKER_USER="${DOCKER_USER:-flutterando}"
22+
DOCKER_USER="${DOCKER_USER:-jacobmoura7}"
2323
IMAGE_NAME="${IMAGE_NAME:-modular-docs}"
2424
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
2525
TAG="${1:-latest}"

example/lib/app/products/data/realtime_connection.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import 'package:flutter/foundation.dart';
22
import 'package:flutter_modular/flutter_modular.dart';
33

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

example/lib/app/products/products_module.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import 'viewmodels/product_list_view_model.dart';
1212
/// live in `pages/` and `viewmodels/`.
1313
///
1414
/// Demonstrates: page-scoped view models (`addChangeNotifier`), params to the
15-
/// view (`/:id`), `addDisposable` (non-reactive resource), and `addStream`.
15+
/// view (`/:id`), `add` (non-reactive `Disposable` resource), and `addStream`.
1616
/// ---------------------------------------------------------------------------
1717
1818
/// `addStream` demo: a live "people viewing" ticker.
@@ -36,7 +36,7 @@ final productsModule = createModule(
3636
'/:id',
3737
provide: (s) {
3838
s
39-
..addDisposable<RealtimeConnection>(RealtimeConnection.new)
39+
..add<RealtimeConnection>(RealtimeConnection.new)
4040
..addChangeNotifier<ProductDetailViewModel>(
4141
ProductDetailViewModel.new,
4242
)

lib/flutter_modular.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
///
33
/// Navigator 2.0 (route matching + page stack + guards + transitions), the
44
/// module system (`createModule` / `ModularContext`), page-scoped state
5-
/// (`provide` / `Scoped` + `context.watch`/`read`, `Consumer`/`Selector`,
6-
/// `addStream`), and nested routes (`children` + `RouterOutlet`).
5+
/// (`provide` / `Scoped` + `context.watch`/`read`, `Consumer`/`Selector`;
6+
/// `addChangeNotifier`/`addStream` as the rule, `addStreamable`/`addListenable`
7+
/// for BLoC/Cubit-style objects, `add` for non-reactive resources), and nested
8+
/// routes (`children` + `RouterOutlet`).
79
library;
810

911
export 'src/app/modular_app.dart';

0 commit comments

Comments
 (0)