@pilllesss/yorn 1.0.182 → 1.0.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,670 @@
1
+ # Dart / Flutter Code Review Guide
2
+
3
+ > Code review guidelines for Dart 3 and Flutter focusing on widget rebuilds, const constructors, null safety, isolates, async in `build`, Riverpod/Bloc state pitfalls, platform channels, keys, disposal, and testability. Not a language tutorial.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Widget Rebuilds & Const Constructors](#widget-rebuilds--const-constructors)
8
+ - [Null Safety & `late`](#null-safety--late)
9
+ - [Isolates](#isolates)
10
+ - [Async in `build`](#async-in-build)
11
+ - [State Management: Riverpod & Bloc](#state-management-riverpod--bloc)
12
+ - [Platform Channels](#platform-channels)
13
+ - [Keys](#keys)
14
+ - [Disposal & Lifecycle](#disposal--lifecycle)
15
+ - [Testability](#testability)
16
+ - [Review Checklist](#review-checklist)
17
+ - [References](#references)
18
+
19
+ ---
20
+
21
+ ## Widget Rebuilds & Const Constructors
22
+
23
+ Flutter rebuilds are cheap only when the Element tree can skip work. A review should flag widgets that allocate new subtrees, closures, or Theme lookups on every frame when a `const` constructor or an extracted widget would keep the child Element.
24
+
25
+ ### Prefer `const` constructors where the subtree is static
26
+
27
+ ```dart
28
+ // ❌ Bad: every parent rebuild allocates a new Text and Icon.
29
+ Widget build(BuildContext context) {
30
+ return Row(
31
+ children: [
32
+ Icon(Icons.star),
33
+ Text('Favorites'),
34
+ ],
35
+ );
36
+ }
37
+
38
+ // ✅ Good: const widgets can be canonicalized and skipped on rebuild.
39
+ Widget build(BuildContext context) {
40
+ return const Row(
41
+ children: [
42
+ Icon(Icons.star),
43
+ Text('Favorites'),
44
+ ],
45
+ );
46
+ }
47
+ ```
48
+
49
+ Review questions:
50
+ - Are leaf widgets (`Text`, `Icon`, `SizedBox`, `Padding` with constant insets) `const`?
51
+ - Does a missing `const` on a parent block `const` on an entire subtree?
52
+ - Is `const` omitted because a single non-const argument (color from `Theme.of`, a closure) poisons the constructor?
53
+
54
+ ### Extract widgets, not helper methods, when state should be isolated
55
+
56
+ A private method that returns a `Widget` is inlined into the caller's `build`. The returned widgets have no Element identity of their own, so they rebuild whenever the caller rebuilds.
57
+
58
+ ```dart
59
+ class ProfilePage extends StatelessWidget {
60
+ const ProfilePage({super.key, required this.user});
61
+ final User user;
62
+
63
+ // ❌ Bad: `_header()` rebuilds with ProfilePage even when `user` is unchanged.
64
+ Widget _header() => Header(title: user.name);
65
+
66
+ @override
67
+ Widget build(BuildContext context) {
68
+ return Column(children: [_header(), const Feed()]);
69
+ }
70
+ }
71
+
72
+ // ✅ Good: a separate widget gets its own Element; Feed stays const.
73
+ class ProfilePage extends StatelessWidget {
74
+ const ProfilePage({super.key, required this.user});
75
+ final User user;
76
+
77
+ @override
78
+ Widget build(BuildContext context) {
79
+ return Column(
80
+ children: [
81
+ Header(title: user.name),
82
+ const Feed(),
83
+ ],
84
+ );
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Do not call `setState` for derived values
90
+
91
+ ```dart
92
+ // ❌ Bad: derived data stored in State, forcing a rebuild to recompute a getter.
93
+ class CartBadge extends StatefulWidget { /* ... */ }
94
+
95
+ class _CartBadgeState extends State<CartBadge> {
96
+ int _count = 0;
97
+
98
+ void didUpdateWidget(CartBadge old) {
99
+ super.didUpdateWidget(old);
100
+ setState(() => _count = widget.items.length);
101
+ }
102
+ }
103
+
104
+ // ✅ Good: derive in build; setState only when the source data changes.
105
+ class CartBadge extends StatelessWidget {
106
+ const CartBadge({super.key, required this.items});
107
+ final List<Item> items;
108
+
109
+ @override
110
+ Widget build(BuildContext context) {
111
+ return Text('${items.length}');
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### Rebuild scope
117
+
118
+ - `setState` on a high `State` object rebuilds that subtree. Prefer lifting only the state that must be shared, and wrapping expensive children in `RepaintBoundary` or extracting them.
119
+ - `ListView(children: [...])` builds every child. Prefer `ListView.builder` / `SliverList` for long lists.
120
+ - Passing a newly allocated `List`/`Map` or a new callback instance into a child that is otherwise `const`-eligible defeats child skip. Capture stable callbacks (`void Function()` stored on State) or use `Widget.canUpdate` identity via `const` / extracted widgets.
121
+
122
+ For general UI performance patterns see [Performance Review Guide](performance-review-guide.md).
123
+
124
+ ---
125
+
126
+ ## Null Safety & `late`
127
+
128
+ Dart's null safety is only as strong as the holes the code leaves (`!`, `late`, `as`). Treat those as review signals, not as style.
129
+
130
+ ### Avoid `!` on values the compiler cannot prove
131
+
132
+ ```dart
133
+ // ❌ Bad: bang operator hides a possible null and crashes at runtime.
134
+ void openProfile(User? user) {
135
+ Navigator.pushNamed(context, '/user/${user!.id}');
136
+ }
137
+
138
+ // ✅ Good: promote with a local or return early.
139
+ void openProfile(User? user) {
140
+ final id = user?.id;
141
+ if (id == null) return;
142
+ Navigator.pushNamed(context, '/user/$id');
143
+ }
144
+ ```
145
+
146
+ ### `late` is a delayed crash, not a type
147
+
148
+ `late` without an initializer throws `LateInitializationError` on first read. `late final` with an initializer is lazy and is the usual legitimate case (expensive, once).
149
+
150
+ ```dart
151
+ class Session {
152
+ // ❌ Bad: `late` field is read from another method with no constructor guarantee.
153
+ late String token;
154
+
155
+ bool get isReady => token.isNotEmpty;
156
+ }
157
+
158
+ class Session {
159
+ // ✅ Good: nullable until assigned; API makes absence visible.
160
+ String? token;
161
+
162
+ bool get isReady => token != null && token!.isNotEmpty;
163
+ }
164
+
165
+ class ThemeCache {
166
+ // ✅ Good: lazy `late final` with initializer; first read computes once.
167
+ late final Map<String, Color> _colors = _loadColors();
168
+ }
169
+ ```
170
+
171
+ Review questions:
172
+ - Is `late` used because the author did not want `?` / a constructor argument?
173
+ - Is a `late` field assigned in `initState` but read from a listener that can fire earlier (animation, platform callback)?
174
+ - Are `as Foo` casts used where a pattern (`if (x case final Foo foo)`) or `is` promotion would fail loudly and locally?
175
+
176
+ ### Fields do not promote
177
+
178
+ ```dart
179
+ class ProfileTile extends StatelessWidget {
180
+ const ProfileTile({super.key, required this.user});
181
+ final User? user;
182
+
183
+ @override
184
+ Widget build(BuildContext context) {
185
+ // ❌ Bad: field promotion does not apply; this is a lint (`unchecked_use_of_nullable_value`).
186
+ // return Text(user.name);
187
+
188
+ // ✅ Good: promote a local.
189
+ final user = this.user;
190
+ if (user == null) return const SizedBox.shrink();
191
+ return Text(user.name);
192
+ }
193
+ }
194
+ ```
195
+
196
+ Null-related crashes are still crashes. See [Error Handling Guide](cross-cutting/error-handling-principles.md) for fail-fast vs. swallowing.
197
+
198
+ ---
199
+
200
+ ## Isolates
201
+
202
+ > 📖 Cross-language concurrency patterns: [Async & Concurrency Guide](cross-cutting/async-concurrency-patterns.md)
203
+
204
+ Flutter's UI isolate must stay free of heavy JSON, image, and crypto work. Dart isolates do not share memory; messages must be sendable.
205
+
206
+ ### Do not block the UI isolate
207
+
208
+ ```dart
209
+ // ❌ Bad: large JSON decode on the UI isolate janks frames.
210
+ final users = (jsonDecode(raw) as List)
211
+ .map((e) => User.fromJson(e as Map<String, dynamic>))
212
+ .toList();
213
+
214
+ // ✅ Good: `Isolate.run` (Dart 2.19+) / `compute` for one-shot work.
215
+ final users = await Isolate.run(() {
216
+ return (jsonDecode(raw) as List)
217
+ .map((e) => User.fromJson(e as Map<String, dynamic>))
218
+ .toList();
219
+ });
220
+ ```
221
+
222
+ ### Message restrictions
223
+
224
+ ```dart
225
+ // ❌ Bad: closures, ReceivePorts, and many plugin types are not sendable.
226
+ await Isolate.run(() => widget.onParsed());
227
+
228
+ // ✅ Good: send plain data; map back to UI types on the main isolate.
229
+ final dto = await Isolate.run(() => parseReport(bytes));
230
+ setState(() => report = Report.fromDto(dto));
231
+ ```
232
+
233
+ Review questions:
234
+ - Is `jsonDecode` / image decode / encryption of non-trivial payloads on the UI isolate?
235
+ - Does the isolate callback capture `BuildContext`, `State`, or plugin controllers?
236
+ - If a long-lived isolate is spawned, is `kill` / `pause` paired with widget disposal?
237
+ - Are isolate failures surfaced, or does `await Isolate.run` lack error handling?
238
+
239
+ `compute` from `flutter/foundation.dart` is a thin wrapper; prefer `Isolate.run` in Dart-only code. Neither is a thread pool — spawning per tiny call is more expensive than doing the work inline.
240
+
241
+ ---
242
+
243
+ ## Async in `build`
244
+
245
+ `build` must stay synchronous and side-effect free. Futures created in `build` restart on every rebuild.
246
+
247
+ ### Do not start I/O or create a new `Future` in `build`
248
+
249
+ ```dart
250
+ // ❌ Bad: new Future every rebuild; the previous request is abandoned.
251
+ Widget build(BuildContext context) {
252
+ return FutureBuilder<Profile>(
253
+ future: api.fetchProfile(id),
254
+ builder: (context, snap) { /* ... */ },
255
+ );
256
+ }
257
+
258
+ // ✅ Good: cache the Future in State; refetch when `id` changes.
259
+ class ProfileView extends StatefulWidget {
260
+ const ProfileView({super.key, required this.id});
261
+ final String id;
262
+ @override
263
+ State<ProfileView> createState() => _ProfileViewState();
264
+ }
265
+
266
+ class _ProfileViewState extends State<ProfileView> {
267
+ late Future<Profile> _future = api.fetchProfile(widget.id);
268
+
269
+ @override
270
+ void didUpdateWidget(ProfileView oldWidget) {
271
+ super.didUpdateWidget(oldWidget);
272
+ if (oldWidget.id != widget.id) {
273
+ _future = api.fetchProfile(widget.id);
274
+ }
275
+ }
276
+
277
+ @override
278
+ Widget build(BuildContext context) {
279
+ return FutureBuilder<Profile>(
280
+ future: _future,
281
+ builder: (context, snap) { /* ... */ },
282
+ );
283
+ }
284
+ }
285
+ ```
286
+
287
+ Keying `ProfileView` with `ValueKey(id)` also remounts State and starts a new fetch. Either approach is fine; a cached `late final` future with no `didUpdateWidget` goes stale when `id` changes.
288
+
289
+ The same rule applies to `StreamBuilder`: do not open a new stream in `build`. Create it once (`initState`, a provider, a Bloc) and cancel on dispose.
290
+
291
+ ### `setState` after `await` must check `mounted`
292
+
293
+ ```dart
294
+ Future<void> _load() async {
295
+ final data = await api.fetch();
296
+ // ❌ Bad: widget may have been disposed during the await.
297
+ setState(() => _data = data);
298
+ }
299
+
300
+ Future<void> _load() async {
301
+ final data = await api.fetch();
302
+ if (!mounted) return;
303
+ setState(() => _data = data);
304
+ }
305
+ ```
306
+
307
+ Use `context.mounted` (Flutter 3.7+) before using `BuildContext` after an async gap (`Navigator`, `ScaffoldMessenger`, `showDialog`). The `use_build_context_synchronously` lint exists because this is a crash class, not a style issue.
308
+
309
+ ### Do not mark `build` `async`
310
+
311
+ ```dart
312
+ // ❌ Bad: build cannot be async; this does not compile, or a helper is abused.
313
+ Widget build(BuildContext context) async {
314
+ final user = await repo.user();
315
+ return Text(user.name);
316
+ }
317
+
318
+ // ✅ Good: hold async state in State / a notifier; build only reads it.
319
+ ```
320
+
321
+ Fire-and-forget `unawaited(load())` inside `build` is the same bug with extra steps.
322
+
323
+ ---
324
+
325
+ ## State Management: Riverpod & Bloc
326
+
327
+ These examples use Riverpod and Bloc because they dominate Flutter reviews. The same pitfalls apply to Provider, GetX, Signals, and raw `InheritedWidget`: rebuild scope, side effects in build, and lifetime.
328
+
329
+ ### Riverpod: `watch` vs `read`, and rebuild scope
330
+
331
+ ```dart
332
+ class CartButton extends ConsumerWidget {
333
+ const CartButton({super.key});
334
+
335
+ @override
336
+ Widget build(BuildContext context, WidgetRef ref) {
337
+ // ❌ Bad: watches the whole cart; button rebuilds on every item mutation.
338
+ final cart = ref.watch(cartProvider);
339
+
340
+ // ✅ Good: watch only the field this widget needs.
341
+ final count = ref.watch(cartProvider.select((c) => c.items.length));
342
+ return Badge(label: Text('$count'));
343
+ }
344
+ }
345
+ ```
346
+
347
+ ```dart
348
+ void onPressed() {
349
+ // ❌ Bad: `watch` in a callback / listener; it is not a rebuild subscription.
350
+ // ref.watch(cartProvider).add(item);
351
+
352
+ // ✅ Good: `read` for one-shot, `watch`/`select` only in `build` / `build` of a listen widget.
353
+ ref.read(cartProvider.notifier).add(item);
354
+ }
355
+ ```
356
+
357
+ Review questions:
358
+ - Is `ref.watch` used in `initState`, a gesture handler, or a `Provider`'s constructor?
359
+ - Does a widget `watch` a large object when `select` / a derived provider would do?
360
+ - Are providers that capture `ref` after `dispose` (`ref.onDispose` missing for controllers, streams)?
361
+ - Is `autoDispose` omitted on screen-scoped providers so they leak after pop?
362
+
363
+ ### Bloc: `create` vs `value`, `buildWhen`, closed cubits
364
+
365
+ `BlocProvider(create: ...)` inside `build` is the documented pattern: `create` runs once per Element, not on every rebuild. `updateShouldNotify` does not control `create`. The bug is constructing a new bloc instance in `build` and handing it to `BlocProvider.value` (no ownership, no dispose, new object every rebuild). Because `create` captures `id` once, a parent that later passes a new `id` will not reload unless the Element remounts or you dispatch an update.
366
+
367
+ ```dart
368
+ // ❌ Bad: new ProfileBloc() every rebuild; `value` does not dispose it.
369
+ Widget build(BuildContext context) {
370
+ return BlocProvider.value(
371
+ value: ProfileBloc()..add(ProfileStarted(id)),
372
+ child: const ProfileBody(),
373
+ );
374
+ }
375
+
376
+ // ❌ Bad: UniqueKey remounts every rebuild — `create` runs again each time.
377
+ Widget build(BuildContext context) {
378
+ return BlocProvider(
379
+ key: UniqueKey(),
380
+ create: (_) => ProfileBloc()..add(ProfileStarted(id)),
381
+ child: const ProfileBody(),
382
+ );
383
+ }
384
+
385
+ // ✅ Good: `create` owns one instance; ValueKey(id) remounts only when id changes.
386
+ class ProfilePage extends StatelessWidget {
387
+ const ProfilePage({super.key, required this.id});
388
+ final String id;
389
+
390
+ @override
391
+ Widget build(BuildContext context) {
392
+ return BlocProvider(
393
+ key: ValueKey(id),
394
+ create: (_) => ProfileBloc()..add(ProfileStarted(id)),
395
+ child: const ProfileBody(),
396
+ );
397
+ }
398
+ }
399
+ ```
400
+
401
+ Alternatively, keep the same bloc and in `didUpdateWidget` dispatch `ProfileRequested(id)` when `id` changes — do not construct a replacement `ProfileBloc()` there.
402
+
403
+ ```dart
404
+ // ❌ Bad: listener work inside builder; also rebuilds on every state.
405
+ BlocBuilder<ProfileBloc, ProfileState>(
406
+ builder: (context, state) {
407
+ if (state is ProfileFailure) {
408
+ WidgetsBinding.instance.addPostFrameCallback((_) {
409
+ ScaffoldMessenger.of(context).showSnackBar(/* ... */);
410
+ });
411
+ }
412
+ return ProfileBody(state: state);
413
+ },
414
+ )
415
+
416
+ // ✅ Good: side effects in BlocListener; rebuilds filtered with buildWhen.
417
+ BlocConsumer<ProfileBloc, ProfileState>(
418
+ listenWhen: (p, c) => c is ProfileFailure && p is! ProfileFailure,
419
+ listener: (context, state) {
420
+ final message = (state as ProfileFailure).message;
421
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
422
+ },
423
+ buildWhen: (p, c) => p.user != c.user || p.loading != c.loading,
424
+ builder: (context, state) => ProfileBody(state: state),
425
+ )
426
+ ```
427
+
428
+ Review questions:
429
+ - Is a `Cubit`/`Bloc` closed (`BlocProvider` does this if `lazy`/`create` owns it; a manually constructed bloc must `close`)?
430
+ - Are events dispatched from `build`?
431
+ - Is `Equatable` / `==` missing on state so every `emit` rebuilds even when fields did not change?
432
+ - Is business logic in the widget (`context.read<FooBloc>().add` mixed with parsing, I/O) instead of the bloc?
433
+
434
+ Do not treat GetX `Obx` / `GetBuilder` as exempt: the same "who owns the controller, who rebuilds, who disposes" questions apply.
435
+
436
+ ---
437
+
438
+ ## Platform Channels
439
+
440
+ Method channels, event channels, and Pigeon are a trust and lifetime boundary. Failures look like "missing plugin" at runtime, not at compile time.
441
+
442
+ ```dart
443
+ // ❌ Bad: untyped maps, ignored errors, UI isolate blocked on a heavy native call.
444
+ final result = await MethodChannel('app.wallet').invokeMethod('sign', {'tx': raw});
445
+ final signature = result as String;
446
+
447
+ // ✅ Good: typed API (Pigeon or a wrapper), errors handled, no UI-thread assumption.
448
+ try {
449
+ final signature = await walletHost.sign(SignRequest(tx: raw));
450
+ if (!mounted) return;
451
+ onSigned(signature);
452
+ } on PlatformException catch (e) {
453
+ onSignFailed(e.code, e.message);
454
+ }
455
+ ```
456
+
457
+ Review questions:
458
+ - Is the channel name a collision-prone generic string (`app`, `native`) without a domain prefix?
459
+ - Are Android/iOS implementations of the same method covering all argument types and null?
460
+ - Does the Dart side assume the plugin is registered (tests, background isolates, and add-to-app engines often are not)?
461
+ - Is binary data passed as `List<int>` copies instead of `Uint8List` / Pigeon bytes?
462
+ - Are EventChannel subscriptions cancelled in `dispose`?
463
+ - Does native code do disk/network work on the platform main thread?
464
+
465
+ Platform data is untrusted input: validate it the same way you would a network payload. See [Security Review Guide](security-review-guide.md).
466
+
467
+ ---
468
+
469
+ ## Keys
470
+
471
+ Keys preserve `State` / `Element` identity across rebuilds. Wrong keys cause state to attach to the wrong row; missing keys cause state to reset when the list reorders.
472
+
473
+ ```dart
474
+ // ❌ Bad: no keys; after delete/reorder, TextField state sticks to the index.
475
+ ListView(
476
+ children: items.map((item) => TodoRow(item: item)).toList(),
477
+ )
478
+
479
+ // ❌ Bad: UniqueKey() in build: every rebuild is a new identity; state is always reset.
480
+ TodoRow(key: UniqueKey(), item: item)
481
+
482
+ // ✅ Good: a stable id from the model.
483
+ ListView(
484
+ children: [
485
+ for (final item in items)
486
+ TodoRow(key: ValueKey(item.id), item: item),
487
+ ],
488
+ )
489
+ ```
490
+
491
+ ```dart
492
+ // ❌ Bad: GlobalKey created in `build` — a new key every rebuild, plus GlobalKey cost.
493
+ Widget build(BuildContext context) {
494
+ final key = GlobalKey<FormState>();
495
+ return Form(key: key, child: /* ... */);
496
+ }
497
+
498
+ // ✅ Good: GlobalKey is rare; hold it on State when FormState is actually needed.
499
+ class _EditorState extends State<Editor> {
500
+ final _formKey = GlobalKey<FormState>();
501
+ @override
502
+ Widget build(BuildContext context) => Form(key: _formKey, child: /* ... */);
503
+ }
504
+ ```
505
+
506
+ Review questions:
507
+ - Do reorderable / dismissible / animated lists use `ValueKey` / `ObjectKey` from a stable model id?
508
+ - Is `GlobalKey` used to reach into a child's `State` when a callback / `ValueNotifier` would do?
509
+ - Are keys on widgets whose `runtimeType` already uniquely identifies them (usually unnecessary)?
510
+
511
+ ---
512
+
513
+ ## Disposal & Lifecycle
514
+
515
+ Anything with `addListener`, a subscription, a ticker, or a native peer needs a matching `dispose`. Flutter will not save you.
516
+
517
+ ```dart
518
+ class SearchField extends StatefulWidget {
519
+ const SearchField({super.key});
520
+ @override
521
+ State<SearchField> createState() => _SearchFieldState();
522
+ }
523
+
524
+ class _SearchFieldState extends State<SearchField> with SingleTickerProviderStateMixin {
525
+ late final TextEditingController _controller;
526
+ late final AnimationController _anim;
527
+ StreamSubscription<Query>? _sub;
528
+
529
+ @override
530
+ void initState() {
531
+ super.initState();
532
+ _controller = TextEditingController();
533
+ _anim = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
534
+ _sub = queryStream.listen((q) {
535
+ if (mounted) _controller.text = q.text;
536
+ });
537
+ }
538
+
539
+ @override
540
+ void dispose() {
541
+ _sub?.cancel();
542
+ _anim.dispose();
543
+ _controller.dispose();
544
+ super.dispose();
545
+ }
546
+
547
+ @override
548
+ Widget build(BuildContext context) => TextField(controller: _controller);
549
+ }
550
+ ```
551
+
552
+ Review questions:
553
+ - `TextEditingController`, `ScrollController`, `FocusNode`, `AnimationController`, `TabController`, `PageController` — created? disposed?
554
+ - `StreamSubscription`, `Timer`, `ChangeNotifier` listeners, `WidgetsBindingObserver` — removed?
555
+ - `AnimationController` uses a `TickerProvider` that dies with the `State` (`SingleTickerProviderStateMixin`), not a leaked vsync?
556
+ - Route-level controllers created in `BlocProvider`/`Provider` — `dispose` callback set?
557
+ - `Image.network` / video / camera controllers stopped when the route is covered, not only when popped?
558
+
559
+ `dispose` must not use `BuildContext` (the element is unmounted). Do not call `setState` there.
560
+
561
+ ---
562
+
563
+ ## Testability
564
+
565
+ Review the tests the same way as the product code. Flutter tests that pump the whole app to assert a string are a smell that logic is trapped in widgets.
566
+
567
+ ```dart
568
+ // ❌ Bad: parsing and I/O live in the widget; tests must pump and mock everything.
569
+ class PriceLabel extends StatelessWidget {
570
+ const PriceLabel({super.key, required this.raw});
571
+ final String raw;
572
+
573
+ @override
574
+ Widget build(BuildContext context) {
575
+ final n = NumberFormat.simpleCurrency().parse(raw);
576
+ return Text(n.toString());
577
+ }
578
+ }
579
+
580
+ // ✅ Good: pure function / mapper is unit-tested; widget only renders.
581
+ String formatPrice(String raw, NumberFormat format) => format.format(format.parse(raw));
582
+
583
+ class PriceLabel extends StatelessWidget {
584
+ const PriceLabel({super.key, required this.text});
585
+ final String text;
586
+
587
+ @override
588
+ Widget build(BuildContext context) => Text(text);
589
+ }
590
+ ```
591
+
592
+ ```dart
593
+ // ❌ Bad: test depends on real time and network.
594
+ testWidgets('loads profile', (tester) async {
595
+ await tester.pumpWidget(const ProfilePage(id: '1'));
596
+ await tester.pumpAndSettle();
597
+ expect(find.text('Ada'), findsOneWidget);
598
+ });
599
+
600
+ // ✅ Good: inject a fake; pump until the Future you control completes.
601
+ testWidgets('loads profile', (tester) async {
602
+ final api = FakeProfileApi()..completeWith(Profile(name: 'Ada'));
603
+ await tester.pumpWidget(ProfilePage(id: '1', api: api));
604
+ await tester.pump();
605
+ expect(find.text('Ada'), findsOneWidget);
606
+ });
607
+ ```
608
+
609
+ Review questions:
610
+ - Can the new logic be tested with `flutter test` without `IntegrationTestWidgetsFlutterBinding`?
611
+ - Are `Key`s present on interactive widgets the test (and the reviewer) needs to find, without painting `UniqueKey` in `build`?
612
+ - Are platform channels mocked (`TestDefaultBinaryMessengerBinding`)?
613
+ - Does `pumpAndSettle` hang because an infinite animation / polling stream never idles?
614
+ - Is `late` in tests hiding missing setup?
615
+
616
+ ---
617
+
618
+ ## Review Checklist
619
+
620
+ ### Widgets & rebuilds
621
+ - [ ] Static subtrees use `const` constructors
622
+ - [ ] Expensive children are extracted widgets, not `_buildFoo()` methods
623
+ - [ ] Long lists use lazy builders, not a fully-materialized `children:` list
624
+ - [ ] `setState` is not used to store values that can be derived in `build`
625
+
626
+ ### Null safety & `late`
627
+ - [ ] `!` and `as` are not used to silence the type system
628
+ - [ ] `late` is lazy-init (`late final x = ...`) or truly guaranteed before first read
629
+ - [ ] Nullable fields are promoted via locals, not assumed non-null
630
+
631
+ ### Isolates & async
632
+ - [ ] Heavy JSON / image / crypto work is off the UI isolate
633
+ - [ ] Isolate messages are sendable plain data
634
+ - [ ] `build` does not create Futures/Streams or start I/O
635
+ - [ ] `FutureBuilder`/`StreamBuilder` reuse a cached future/stream; refetch when the id/input changes
636
+ - [ ] `setState` / `BuildContext` after `await` check `mounted` / `context.mounted`
637
+
638
+ ### State management
639
+ - [ ] `ref.watch` / `context.watch` only in `build`; `read` in callbacks
640
+ - [ ] Rebuilds are narrowed (`select`, `buildWhen`)
641
+ - [ ] Do not construct a `Bloc`/`Cubit`/`ChangeNotifier` in `build` and pass it to `*.value` — use `create`; key with `ValueKey(id)` (not `UniqueKey()`) or `didUpdateWidget` so a new id reloads
642
+ - [ ] Providers/blocs that own controllers are disposed (`autoDispose`, `close`)
643
+
644
+ ### Platform, keys, disposal
645
+ - [ ] Platform channel calls handle `PlatformException` and missing plugins
646
+ - [ ] EventChannel / native subscriptions are cancelled
647
+ - [ ] List children that hold `State` have stable `ValueKey`s (not `UniqueKey()` in `build`)
648
+ - [ ] Controllers, tickers, and subscriptions are disposed; `dispose` does not use `context`
649
+
650
+ ### Tests
651
+ - [ ] Domain logic is unit-tested without pumping widgets
652
+ - [ ] Widget tests inject fakes; they do not hit network or real platform channels
653
+ - [ ] `pumpAndSettle` is not used on never-idle animations
654
+
655
+ ---
656
+
657
+ ## References
658
+
659
+ - [Dart language tour (null safety)](https://dart.dev/null-safety)
660
+ - [Effective Dart](https://dart.dev/effective-dart)
661
+ - [Flutter performance: best practices](https://docs.flutter.dev/perf/best-practices)
662
+ - [FutureBuilder class](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html)
663
+ - [Isolate.run](https://api.dart.dev/stable/dart-isolate/Isolate/run.html)
664
+ - [Riverpod: Refs (`watch` vs `read`)](https://riverpod.dev/docs/concepts2/refs)
665
+ - [Bloc: BlocListener vs BlocBuilder](https://bloclibrary.dev/bloc-concepts/)
666
+ - [Key class](https://api.flutter.dev/flutter/foundation/Key-class.html)
667
+ - [Error Handling Guide](cross-cutting/error-handling-principles.md)
668
+ - [Async & Concurrency Guide](cross-cutting/async-concurrency-patterns.md)
669
+ - [Performance Review Guide](performance-review-guide.md)
670
+ - [Security Review Guide](security-review-guide.md)