Skip to content

Commit 66ec6ba

Browse files
committed
Update skill files with refined content
1 parent 15bfa22 commit 66ec6ba

File tree

3 files changed

+305
-13
lines changed

3 files changed

+305
-13
lines changed

skills/flutter-architecture-expert/SKILL.md

Lines changed: 190 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,16 +412,203 @@ class MyService {
412412
// Test: MyService(api: MockApiClient())
413413
```
414414

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 {
446+
final result = await di<ApiClient>().delete(id);
447+
items.value = await di<ApiClient>().getItems(); // reload directly
448+
return result;
449+
}, initialValue: false);
450+
451+
// ❌ Don't call another command from inside a command
452+
late final deleteCommand = Command.createAsync<int, bool>((id) async {
453+
final result = await di<ApiClient>().delete(id);
454+
loadCommand.run(); // WRONG — nesting commands
455+
return result;
456+
}, initialValue: false);
457+
```
458+
459+
## Reacting to Command Results
460+
461+
**In WatchingWidgets**: Use `registerHandler` on command results for side effects (navigation, dialogs). Never use `addListener` or `runAsync()`:
462+
463+
```dart
464+
class MyPage extends WatchingWidget {
465+
@override
466+
Widget build(BuildContext context) {
467+
final isRunning = watchValue((MyManager m) => m.createCommand.isRunning);
468+
469+
// React to result — navigate on success
470+
registerHandler(
471+
select: (MyManager m) => m.createCommand.results,
472+
handler: (context, result, cancel) {
473+
if (result.hasData && result.data != null) {
474+
appPath.push(DetailRoute(id: result.data!.id));
475+
}
476+
},
477+
);
478+
479+
return ElevatedButton(
480+
onPressed: isRunning ? null : () => di<MyManager>().createCommand.run(params),
481+
child: isRunning ? CircularProgressIndicator() : Text('Create'),
482+
);
483+
}
484+
}
485+
```
486+
487+
**Outside widgets** (managers, services): Use listen_it `listen()` instead of raw `addListener` — it returns a `ListenableSubscription` for easy cancellation:
488+
489+
```dart
490+
_subscription = someCommand.results.listen((result, subscription) {
491+
if (result.hasData) doSomething(result.data);
492+
});
493+
// later: _subscription.cancel();
494+
```
495+
496+
## Where allReady() Belongs
497+
498+
`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:
499+
500+
```dart
501+
// ✅ UI handles loading state
502+
class MyApp extends WatchingWidget {
503+
@override
504+
Widget build(BuildContext context) {
505+
if (!allReady()) return LoadingScreen();
506+
return MainApp();
507+
}
508+
}
509+
510+
// ✅ Push scope, let UI react
511+
Future<void> onAuthenticated(Client client) async {
512+
di.pushNewScope(scopeName: 'auth', init: (scope) {
513+
scope.registerSingleton<Client>(client);
514+
scope.registerSingletonAsync<MyManager>(() => MyManager().init(), dependsOn: [Client]);
515+
});
516+
// No await di.allReady() here — UI handles it
517+
}
518+
```
519+
520+
## Error Handling
521+
522+
Three layers: **InteractionManager** (toast abstraction), **global handler** (catch-all), **local listeners** (custom messages).
523+
524+
### InteractionManager
525+
526+
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:
527+
528+
```dart
529+
class InteractionManager {
530+
BuildContext? _context;
531+
532+
void setContext(BuildContext context) => _context = context;
533+
534+
BuildContext? get stableContext {
535+
final ctx = _context;
536+
if (ctx != null && ctx.mounted) return ctx;
537+
return null;
538+
}
539+
540+
void showToast(String message, {bool isError = false}) {
541+
Fluttertoast.showToast(msg: message, ...);
542+
}
543+
}
544+
545+
// Connector widget — wrap around app content inside MaterialApp
546+
class InteractionConnector extends StatefulWidget { ... }
547+
class _InteractionConnectorState extends State<InteractionConnector> {
548+
@override
549+
void didChangeDependencies() {
550+
super.didChangeDependencies();
551+
di<InteractionManager>().setContext(context);
552+
}
553+
@override
554+
Widget build(BuildContext context) => widget.child;
555+
}
556+
```
557+
558+
Register sync in base scope (before async singletons):
559+
```dart
560+
di.registerSingleton<InteractionManager>(InteractionManager());
561+
```
562+
563+
### Global Exception Handler
564+
565+
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`):
566+
567+
```dart
568+
// In TheApp
569+
static void globalErrorHandler(CommandError error, StackTrace stackTrace) {
570+
debugPrint('Command error [${error.commandName}]: ${error.error}');
571+
di<InteractionManager>().showToast(error.error.toString(), isError: true);
572+
}
573+
574+
// In main()
575+
Command.globalExceptionHandler = TheApp.globalErrorHandler;
576+
```
577+
578+
### Local Error Listeners
579+
580+
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+
415598
## Best Practices
416599

417600
- 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
419602
- Break UI into small WatchingWidgets (only watch what you need)
420603
- 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
422607
- Use scopes for user sessions and resettable services
423608
- 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)
425612
- Use `run()` not `execute()` on commands
426613
- Use proxies to wrap DTOs with reactive behavior (commands, computed properties, change notification)
427614
- Use DataRepository with reference counting when same entity appears in multiple places

skills/get-it-expert/SKILL.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,19 @@ Future<void> setupThrowableScope() async {
243243
await di.popScopesTill('throwableScope', inclusive: true);
244244
await setupThrowableScope();
245245
```
246+
247+
**Logout / scope cleanup** — use `popScopesTill` to pop multiple scopes at once instead of manually checking and popping each one:
248+
```dart
249+
// ❌ Manual scope-by-scope popping
250+
void onLogout() {
251+
if (di.hasScope('chat')) di.popScope();
252+
if (di.hasScope('auth')) di.popScope();
253+
}
254+
255+
// ✅ Use popScopesTill to pop everything above (and including) the auth scope
256+
Future<void> onLogout() async {
257+
if (di.hasScope('auth')) {
258+
await di.popScopesTill('auth', inclusive: true);
259+
}
260+
}
261+
```

0 commit comments

Comments
 (0)