You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: skills/flutter-architecture-expert/SKILL.md
+190-3Lines changed: 190 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -412,16 +412,203 @@ class MyService {
412
412
// Test: MyService(api: MockApiClient())
413
413
```
414
414
415
+
## Manager init() vs Commands
416
+
417
+
Manager `init()` loads initial data via **direct API calls**, not through commands. Commands are the **UI-facing reactive interface** — widgets watch their `isRunning`, `errors`, and `results`. Don't route init through commands:
418
+
419
+
```dart
420
+
class MyManager {
421
+
final items = ValueNotifier<List<Item>>([]);
422
+
423
+
// Command for UI-triggered refresh (widget watches isRunning)
424
+
late final loadCommand = Command.createAsyncNoParam<List<Item>>(
425
+
() async {
426
+
final result = await di<ApiClient>().getItems();
427
+
items.value = result;
428
+
return result;
429
+
},
430
+
initialValue: [],
431
+
);
432
+
433
+
// init() calls API directly — no command needed
434
+
Future<MyManager> init() async {
435
+
items.value = await di<ApiClient>().getItems();
436
+
return this;
437
+
}
438
+
}
439
+
```
440
+
441
+
**Don't nest commands**: If a command needs to reload data after mutation, call the API directly inside the command body — don't call another command's `run()`:
442
+
443
+
```dart
444
+
// ✅ Direct API call inside command
445
+
late final deleteCommand = Command.createAsync<int, bool>((id) async {
**Outside widgets** (managers, services): Use listen_it `listen()` instead of raw `addListener` — it returns a `ListenableSubscription` for easy cancellation:
`allReady()` belongs in the **UI** (WatchingWidget), not in imperative code. The root widget's `allReady()` shows a loading indicator until all async singletons (including newly pushed scopes) are ready:
A sync singleton registered before async services. Abstracts user-facing feedback (toasts, future dialogs). Receives a `BuildContext` via a connector widget so it can show context-dependent UI without threading context through managers:
A static method on your app coordinator (e.g. `TheApp`), assigned to `Command.globalExceptionHandler` in `main()`. Catches any command error that has no local `.errors` listener (default `ErrorReaction.firstLocalThenGlobalHandler`):
For commands where you want a user-friendly message instead of the raw exception, add `.errors.listen()` (listen_it) in the manager's `init()`. These suppress the global handler:
581
+
582
+
```dart
583
+
Future<MyManager> init() async {
584
+
final interaction = di<InteractionManager>();
585
+
startSessionCommand.errors.listen((error, _) {
586
+
interaction.showToast('Could not start session', isError: true);
587
+
});
588
+
submitOutcomeCommand.errors.listen((error, _) {
589
+
interaction.showToast('Could not submit outcome', isError: true);
590
+
});
591
+
// ... load initial data
592
+
return this;
593
+
}
594
+
```
595
+
596
+
**Flow**: Command fails → ErrorFilter (default: `firstLocalThenGlobalHandler`) → if local `.errors` has listeners, only they fire → if no local listeners, global handler fires → toast shown.
597
+
415
598
## Best Practices
416
599
417
600
- Register all services before `runApp()`
418
-
- Use `allReady()`with watch_it or FutureBuilder for async services
601
+
- Use `allReady()`in WatchingWidgets for async service loading — not in imperative code
419
602
- Break UI into small WatchingWidgets (only watch what you need)
420
603
- Use managers (ChangeNotifier/ValueNotifier subclasses) for state
421
-
- Use commands for async operations with loading/error states
604
+
- Use commands for UI-triggered async operations with loading/error states
605
+
- Manager `init()` calls APIs directly, commands are for UI interaction
606
+
- Don't nest commands — use direct API calls for internal logic
422
607
- Use scopes for user sessions and resettable services
423
608
- Use `createOnce()` for widget-local disposable objects
424
-
- Use `registerHandler()` for side effects (dialogs, navigation, snackbars)
609
+
- Use `registerHandler()` for side effects in widgets (dialogs, navigation, snackbars)
610
+
- Use listen_it `listen()` for side effects outside widgets (managers, services)
611
+
- Never use raw `addListener` — use `registerHandler` (widgets) or `listen()` (non-widgets)
425
612
- Use `run()` not `execute()` on commands
426
613
- Use proxies to wrap DTOs with reactive behavior (commands, computed properties, change notification)
427
614
- Use DataRepository with reference counting when same entity appears in multiple places
0 commit comments