Skip to content

Commit 3d8c7c3

Browse files
committed
feat(flowr): export FrState presets for mvvm state
1 parent 030bb8e commit 3d8c7c3

12 files changed

Lines changed: 117 additions & 42 deletions

File tree

packages/flowr/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ class CounterPage extends StatelessWidget {
9494

9595
`FlowR` follows bloc equality semantics. Do not rely on in-place model mutation plus `put/update`; return a new state instance when the UI should rebuild.
9696

97+
For immutable page state modeled with Freezed, `flowr` also exports two
98+
recommended presets:
99+
100+
- `@FrState`: enables `toJson()` for debug snapshots without implying restore
101+
semantics.
102+
- `@FrStateJson`: enables both `toJson()` and `fromJson()` for state that must
103+
be restored from serialized JSON.
104+
97105
For GetIt DI, register a ViewModel and then read it with `context.read<T>()`.
98106
`context.read<T>()` reads Provider first, then GetIt.
99107

packages/flowr/lib/flowr_mvvm.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export 'package:flowr/src/view_model.dart';
2525
export 'package:flowr/src/view.dart';
2626
export 'package:flowr/src/view/value_stream_widget.dart';
2727
export 'package:flowr/src/provider.dart';
28+
export 'package:flowr/src/annotations/fr_state.dart';
2829

2930
/// FlowR-Union
3031
export 'package:flowr/src/fr_union.dart';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
3+
/// Recommended Freezed preset for page-local immutable state that should be
4+
/// easy to inspect in logs and debug tools.
5+
///
6+
/// This enables `toJson()` for snapshotting state, but keeps `fromJson()`
7+
/// disabled so ordinary UI state does not implicitly claim restore semantics.
8+
// ignore: constant_identifier_names
9+
const FrState = Freezed(
10+
copyWith: true,
11+
equal: true,
12+
toStringOverride: true,
13+
fromJson: false,
14+
toJson: true,
15+
);
16+
17+
/// Recommended Freezed preset for page-local immutable state that must be
18+
/// restored from serialized JSON, such as persisted or recoverable UI state.
19+
///
20+
/// Use this only when the state class genuinely needs `fromJson()` in
21+
/// addition to debug-friendly `toJson()`.
22+
// ignore: constant_identifier_names
23+
const FrStateJson = Freezed(
24+
copyWith: true,
25+
equal: true,
26+
toStringOverride: true,
27+
fromJson: true,
28+
toJson: true,
29+
);

packages/flowr/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies:
3333
provider: ^6.1.5
3434
stack_trace: ^1.11.1
3535
async: ^2.11.0 # fixed; adapt flutter 3.16.7;
36+
freezed_annotation: ">=2.4.4 <4.0.0"
3637

3738
dev_dependencies:
3839
flutter_test:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'package:flowr/flowr_mvvm.dart';
2+
import 'package:freezed_annotation/freezed_annotation.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
void main() {
6+
test('FrState exposes the debug-state preset', () {
7+
expect(FrState, isA<Freezed>());
8+
expect(FrState.copyWith, isTrue);
9+
expect(FrState.equal, isTrue);
10+
expect(FrState.toStringOverride, isTrue);
11+
expect(FrState.fromJson, isFalse);
12+
expect(FrState.toJson, isTrue);
13+
});
14+
15+
test('FrStateJson exposes the restorable-state preset', () {
16+
expect(FrStateJson, isA<Freezed>());
17+
expect(FrStateJson.copyWith, isTrue);
18+
expect(FrStateJson.equal, isTrue);
19+
expect(FrStateJson.toStringOverride, isTrue);
20+
expect(FrStateJson.fromJson, isTrue);
21+
expect(FrStateJson.toJson, isTrue);
22+
});
23+
}

packages/fr_acdd/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ class NotificationsScreenDataModel with _$NotificationsScreenDataModel {
2424

2525
Use `@FrAcddFreezed`, `@FrAcddFreezedJSON`, or `@Freezed(...)` for extractable
2626
DTOs. Keep page-local state on non-DTO models without `@FrAcddDto`. When the
27-
page is scaffolded by `fr-mvvm-contract`, that usually means the generated
28-
`@FrState` preset; use plain `@Freezed(...)` only when the state model holds
27+
page is scaffolded by `fr-mvvm-contract`, that usually means FlowR's exported
28+
`@FrState` preset; use `@FrStateJson` only when the state model truly needs
29+
`fromJson()`, and use plain `@Freezed(...)` when the state model holds
2930
runtime-only or non-JSON-serializable fields.
3031

3132
`@FrAcddFreezed` is the minimal extraction preset, not a claim that every DTO

skills/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ any temporary JSON spec is just generator input and should not be committed as
4545
a parallel design artifact. Temporary page specs now require `page.figmaUrl`
4646
and `page.api`, with optional `page.apiContract` when `page.api` is
4747
`BFF`. Non-DTO page-local models now default to the generated `@FrState`
48-
Freezed preset so `toJson()` is available during debugging; use
49-
`models[].preset = "plain"` when a model contains runtime-only or
50-
non-JSON-serializable fields. Generated pages now require
51-
`freezed_annotation`, `freezed`, and `build_runner` in the target project. If
52-
the target project has not installed those yet, use
48+
Freezed preset exported by `flowr` so `toJson()` is available during
49+
debugging; use `models[].preset = "state_json"` only when a model must be
50+
restored from JSON, or `models[].preset = "plain"` when it contains
51+
runtime-only or non-JSON-serializable fields. Generated pages now require
52+
`freezed_annotation`, `freezed`, `build_runner`, and a `flowr` version that
53+
exports `FrState` / `FrStateJson` in the target project. If the target
54+
project has not installed those yet, use
5355
`skills/flowr-dart-usage/references/freezed-install.md` first. See
5456
`skills/fr-mvvm-contract/SKILL.md` for the required spec shape. When a
5557
contract page uses `bff` mode, the target project also needs `fr_acdd`; use

skills/fr-mvvm-contract/SKILL.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ This skill is intentionally strict:
2424
- Only generate `FrBlocViewModel<GeneratedEvent, GeneratedModel>`.
2525
- Generate page models with Freezed-based presets, not handwritten
2626
`copyWith`. Non-DTO state models default to the generated `@FrState`
27-
annotation so they expose `toJson()` for debugging.
27+
annotation exported by `flowr` so they expose `toJson()` for debugging.
2828
- Do not add `FrViewModel` / method-mode content here.
2929
- Treat the contract dart file as the authoritative spec for the generated
3030
parts.
@@ -64,6 +64,9 @@ page.
6464
- `part '<contract_name>.g.dart';` when a theme or any state model enables
6565
generated JSON helpers. With the default `@FrState` preset on page-local
6666
models, that usually means the contract file includes `.g.dart`.
67+
- Target projects must use a `flowr` version that exports `FrState` and
68+
`FrStateJson`. The generator no longer injects local `const FrState = ...`
69+
definitions into the contract file.
6770
- Target project runtime deps need `freezed_annotation`.
6871
- Target project dev deps need `freezed` and `build_runner`.
6972
- `bff` pages also need `fr_acdd` in the target package dependencies.
@@ -254,7 +257,8 @@ part '<contract_name>.vm.dart';
254257
- Also declare `part '<contract_name>.g.dart';` whenever the contract library
255258
enables generated JSON helpers. The default `@FrState` preset on page-local
256259
models does this automatically; `preset: plain` is the opt-out when a model
257-
contains non-serializable runtime fields.
260+
contains non-serializable runtime fields, and `preset: state_json` is the
261+
opt-in when a model must restore itself from JSON.
258262

259263
- Keep the contract doc comments above the root widget in this order:
260264
- Figma
@@ -318,8 +322,10 @@ part '<contract_name>.vm.dart';
318322
- Always use `FrBlocViewModel<GeneratedEvent, GeneratedModel>`.
319323
- Events are generated under one sealed base class in the contract library.
320324
- Non-DTO page models are generated in the contract file with `@FrState` by
321-
default. `@FrState` is a contract-local `Freezed` preset that enables JSON
322-
hooks so `toJson()` is available for debug snapshots.
325+
default. `flowr` exports `@FrState` as a shared `Freezed` preset that
326+
enables `toJson()` for debug snapshots without implying restore semantics.
327+
- Use `@FrStateJson` only when the state class genuinely needs
328+
`factory Xxx.fromJson(...)` so it can be restored from serialized JSON.
323329
- In `bff` mode, only backend-transfer DTOs should use `@FrAcddDto`. Keep
324330
page-local state in page models or view-model members instead of annotating
325331
it as DTO state.
@@ -415,15 +421,18 @@ If `theme` is present, it supports:
415421
- the generated primary model's private constructor
416422
- the generated primary model's `const factory`
417423
- `preset` defaults to `state`.
418-
- `preset: state` emits a contract-local `@FrState` preset equivalent to
419-
`@Freezed(copyWith: true, equal: true, toStringOverride: true, fromJson: true, toJson: true)`,
420-
adds `factory Xxx.fromJson(...)`, and requires `part '<contract_name>.g.dart';`.
424+
- `preset: state` emits `@FrState`, equivalent to
425+
`@Freezed(copyWith: true, equal: true, toStringOverride: true, fromJson: false, toJson: true)`,
426+
and requires `part '<contract_name>.g.dart';`.
427+
- `preset: state_json` emits `@FrStateJson`, adds
428+
`factory Xxx.fromJson(...)`, and requires `part '<contract_name>.g.dart';`.
421429
- `preset: plain` falls back to
422430
`@Freezed(copyWith: true, equal: true, toStringOverride: true, fromJson: false, toJson: false)`.
423431
- `copyWith`, equality, debug `toString`, and the state-model debug `toJson()`
424432
snapshot are provided by `Freezed`, not handwritten by this script.
425433
- Generated state models deliberately enable JSON hooks so `toJson()` is
426-
available during debugging. If a model contains runtime-only or
434+
available during debugging. Only `preset: state_json` enables restore
435+
semantics through `fromJson()`. If a model contains runtime-only or
427436
non-JSON-serializable fields, set `preset: plain` or move those fields out
428437
of the immutable page model to avoid hidden build failures.
429438
- `@FrAcddDto` does not imply runtime JSON serialization by itself. In this

skills/fr-mvvm-contract/references/fr-acdd.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ fvm dart run fr_acdd:extract_bff --format json5 --input lib/page/notifications_p
6262
Freezed unions for extractable DTOs.
6363
- `@FrAcddDto` is only for backend-transfer DTOs. Do not annotate page-local
6464
state classes as DTO kinds. In `fr-mvvm-contract`, page-local models now
65-
default to the generated `@FrState` preset so `toJson()` is available for
66-
debugging; fall back to plain `@Freezed(...)` only when the model contains
65+
default to FlowR's exported `@FrState` preset so `toJson()` is available for
66+
debugging. Use `@FrStateJson` only when the state model truly needs
67+
`fromJson()`, and fall back to plain `@Freezed(...)` when the model contains
6768
runtime-only or non-JSON-serializable fields.
6869
- If a DTO really does cross a runtime JSON boundary, keep `@FrAcddDto` and
6970
prefer `@FrAcddFreezedJSON`. Use explicit `@Freezed(...)` only when that

skills/fr-mvvm-contract/scripts/new_page.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
ARTIFACT_JSON = "JSON"
2828
ARTIFACT_PROTO = "PROTO"
2929
MODEL_PRESET_STATE = "state"
30+
MODEL_PRESET_STATE_JSON = "state_json"
3031
MODEL_PRESET_PLAIN = "plain"
3132
SKIP_DIRS = {".dart_tool", ".git", ".idea", ".vscode", "build", "ios/Pods"}
3233

@@ -478,8 +479,12 @@ def parse_model_preset(value: Any, path: str) -> str:
478479
if value is None:
479480
return MODEL_PRESET_STATE
480481
preset = require_str(value, path).lower()
481-
if preset not in (MODEL_PRESET_STATE, MODEL_PRESET_PLAIN):
482-
raise SpecError(f"{path} must be `state` or `plain`")
482+
if preset not in (
483+
MODEL_PRESET_STATE,
484+
MODEL_PRESET_STATE_JSON,
485+
MODEL_PRESET_PLAIN,
486+
):
487+
raise SpecError(f"{path} must be `state`, `state_json`, or `plain`")
483488
return preset
484489

485490

@@ -964,6 +969,8 @@ def render_model_class(model: dict[str, Any]) -> str:
964969
parts.append(comment)
965970
if model["preset"] == MODEL_PRESET_STATE:
966971
parts.append("@FrState")
972+
elif model["preset"] == MODEL_PRESET_STATE_JSON:
973+
parts.append("@FrStateJson")
967974
else:
968975
parts.append("@Freezed(")
969976
parts.append(" copyWith: true,")
@@ -975,7 +982,7 @@ def render_model_class(model: dict[str, Any]) -> str:
975982
parts.append(f"class {model['name']} with _${model['name']} {{")
976983
parts.append(f" const {model['name']}._();")
977984
parts.append("")
978-
if model["preset"] == MODEL_PRESET_STATE:
985+
if model["preset"] == MODEL_PRESET_STATE_JSON:
979986
parts.append(
980987
f" factory {model['name']}.fromJson(Map<String, dynamic> json) => "
981988
f"_${model['name']}FromJson(json);"
@@ -1138,25 +1145,11 @@ def theme_uses_generated_part(theme: dict[str, Any] | None) -> bool:
11381145

11391146

11401147
def model_uses_generated_part(model: dict[str, Any]) -> bool:
1141-
if model["preset"] == MODEL_PRESET_STATE:
1148+
if model["preset"] in (MODEL_PRESET_STATE, MODEL_PRESET_STATE_JSON):
11421149
return True
11431150
return any(code_uses_generated_part(member) for member in model["members"])
11441151

11451152

1146-
def render_state_preset() -> str:
1147-
return "\n".join(
1148-
(
1149-
"const FrState = Freezed(",
1150-
" copyWith: true,",
1151-
" equal: true,",
1152-
" toStringOverride: true,",
1153-
" fromJson: true,",
1154-
" toJson: true,",
1155-
");",
1156-
)
1157-
)
1158-
1159-
11601153
def render_contract_file(
11611154
page: dict[str, Any],
11621155
models: list[dict[str, Any]],
@@ -1174,9 +1167,6 @@ def render_contract_file(
11741167
lines.append(f"part '{page['file_name']}.v.dart';")
11751168
lines.append(f"part '{page['file_name']}.vm.dart';")
11761169
lines.append("")
1177-
if any(model["preset"] == MODEL_PRESET_STATE for model in models):
1178-
lines.append(render_state_preset())
1179-
lines.append("")
11801170

11811171
lines.extend(section_lines("Figma", page["figma"]))
11821172
if page["api_reference"] != API_BFF:

0 commit comments

Comments
 (0)