mindsystem-cc 3.3.2 → 3.5.0
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.
- package/README.md +65 -1
- package/agents/ms-code-simplifier.md +1 -0
- package/agents/ms-flutter-reviewer.md +210 -0
- package/agents/ms-flutter-simplifier.md +42 -101
- package/bin/install.js +8 -0
- package/commands/ms/audit-milestone.md +209 -0
- package/commands/ms/do-work.md +7 -7
- package/commands/ms/execute-phase.md +5 -5
- package/commands/ms/research-project.md +10 -4
- package/mindsystem/templates/config.json +5 -1
- package/mindsystem/workflows/do-work.md +23 -23
- package/mindsystem/workflows/execute-phase.md +20 -20
- package/mindsystem/workflows/map-codebase.md +12 -6
- package/package.json +3 -2
- package/skills/flutter-senior-review/AGENTS.md +869 -0
- package/skills/flutter-senior-review/SKILL.md +205 -0
- package/skills/flutter-senior-review/principles/dependencies-data-not-callbacks.md +75 -0
- package/skills/flutter-senior-review/principles/dependencies-provider-tree.md +85 -0
- package/skills/flutter-senior-review/principles/dependencies-temporal-coupling.md +97 -0
- package/skills/flutter-senior-review/principles/pragmatism-consistent-error-handling.md +130 -0
- package/skills/flutter-senior-review/principles/pragmatism-speculative-generality.md +91 -0
- package/skills/flutter-senior-review/principles/state-data-clumps.md +64 -0
- package/skills/flutter-senior-review/principles/state-invalid-states.md +53 -0
- package/skills/flutter-senior-review/principles/state-single-source-of-truth.md +68 -0
- package/skills/flutter-senior-review/principles/state-type-hierarchies.md +75 -0
- package/skills/flutter-senior-review/principles/structure-composition-over-config.md +105 -0
- package/skills/flutter-senior-review/principles/structure-shared-visual-patterns.md +107 -0
- package/skills/flutter-senior-review/principles/structure-wrapper-pattern.md +90 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Data Clumps to Records
|
|
3
|
+
category: state
|
|
4
|
+
impact: MEDIUM-HIGH
|
|
5
|
+
impactDescription: Reduces parameter proliferation
|
|
6
|
+
tags: record, typedef, data-modeling, parameters
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Data Clumps to Records
|
|
10
|
+
|
|
11
|
+
Group parameters that travel together into typed objects (records or classes).
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Same 3+ parameters appear in multiple function signatures
|
|
15
|
+
- Parameters are logically related (always used together)
|
|
16
|
+
- Adding a new related field requires updating many signatures
|
|
17
|
+
- Bugs from passing parameters in the wrong order
|
|
18
|
+
|
|
19
|
+
**Incorrect (repeated parameter groups):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
void showReward(int baseReward, int? boostedReward, int? multiplier) { ... }
|
|
23
|
+
void displayBadge(int baseReward, int? boostedReward, int? multiplier) { ... }
|
|
24
|
+
void logClaim(int baseReward, int? boostedReward, int? multiplier) { ... }
|
|
25
|
+
|
|
26
|
+
Widget buildReward({
|
|
27
|
+
required int baseReward,
|
|
28
|
+
int? boostedReward,
|
|
29
|
+
int? multiplier,
|
|
30
|
+
}) { ... }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct (typed record/class):**
|
|
34
|
+
|
|
35
|
+
```dart
|
|
36
|
+
// Record for simple data grouping
|
|
37
|
+
typedef RewardBonus = ({int base, int? boosted, int? multiplier});
|
|
38
|
+
|
|
39
|
+
// Or class if behavior is needed
|
|
40
|
+
class RewardCalculation {
|
|
41
|
+
final int base;
|
|
42
|
+
final int? boosted;
|
|
43
|
+
final int? multiplier;
|
|
44
|
+
|
|
45
|
+
bool get hasBonus => multiplier != null;
|
|
46
|
+
int get displayAmount => boosted ?? base;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Clean call sites
|
|
50
|
+
void showReward(RewardBonus bonus) { ... }
|
|
51
|
+
Widget buildReward({required RewardBonus bonus}) { ... }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Why it matters:**
|
|
55
|
+
- Single place to add new related fields
|
|
56
|
+
- Impossible to pass parameters in wrong order
|
|
57
|
+
- Semantic meaning is clear
|
|
58
|
+
- Reduces parameter count everywhere
|
|
59
|
+
|
|
60
|
+
**Detection questions:**
|
|
61
|
+
- Do the same 3+ parameters appear in multiple function signatures?
|
|
62
|
+
- Are parameters logically related (always used together)?
|
|
63
|
+
- Would adding a new related field require updating many signatures?
|
|
64
|
+
- Are there bugs from passing parameters in the wrong order?
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Make Invalid States Unrepresentable
|
|
3
|
+
category: state
|
|
4
|
+
impact: CRITICAL
|
|
5
|
+
impactDescription: Eliminates entire class of bugs
|
|
6
|
+
tags: sealed-class, boolean-flags, state-modeling, type-safety
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Make Invalid States Unrepresentable
|
|
10
|
+
|
|
11
|
+
Replace boolean flag combinations with sealed class hierarchies where each variant represents exactly one valid state.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- 3+ boolean parameters passed together
|
|
15
|
+
- Same boolean checks repeated in multiple places
|
|
16
|
+
- if/else chains checking flag combinations
|
|
17
|
+
- Some flag combinations would cause undefined behavior
|
|
18
|
+
|
|
19
|
+
**Incorrect (boolean flag explosion):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
Widget build() {
|
|
23
|
+
final isLoading = ...;
|
|
24
|
+
final isExpired = ...;
|
|
25
|
+
final isTutorial = ...;
|
|
26
|
+
final hasBonus = ...;
|
|
27
|
+
// What happens when isTutorial && isExpired?
|
|
28
|
+
// What about isLoading && hasBonus && isTutorial?
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Correct (sealed class hierarchy):**
|
|
33
|
+
|
|
34
|
+
```dart
|
|
35
|
+
sealed class ItemMode {
|
|
36
|
+
const ItemMode();
|
|
37
|
+
}
|
|
38
|
+
final class ItemModeNormal extends ItemMode { ... }
|
|
39
|
+
final class ItemModeTutorial extends ItemMode { ... }
|
|
40
|
+
final class ItemModeExpired extends ItemMode { ... }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Why it matters:**
|
|
44
|
+
- Compiler enforces exhaustive handling via switch expressions
|
|
45
|
+
- New states added explicitly, not as boolean combinations
|
|
46
|
+
- Impossible to create invalid state combinations
|
|
47
|
+
- Self-documenting: sealed class shows all possible states
|
|
48
|
+
|
|
49
|
+
**Detection questions:**
|
|
50
|
+
- Are there 3+ boolean parameters being passed together?
|
|
51
|
+
- Do you see the same boolean checks repeated in multiple places?
|
|
52
|
+
- Are there if/else chains checking combinations of flags?
|
|
53
|
+
- Could some flag combinations cause undefined behavior?
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Single Source of Truth
|
|
3
|
+
category: state
|
|
4
|
+
impact: HIGH
|
|
5
|
+
impactDescription: Prevents stale data bugs
|
|
6
|
+
tags: state-ownership, provider, derived-state, riverpod
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Single Source of Truth (State Ownership)
|
|
10
|
+
|
|
11
|
+
One owner per state, derive the rest via selectors. Push long-lived state up to providers; keep only ephemeral UI state local.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Same data stored in both a provider and local widget state
|
|
15
|
+
- useEffect hooks syncing local state from provider state
|
|
16
|
+
- Two sources of truth could disagree
|
|
17
|
+
- Derived data being cached instead of computed
|
|
18
|
+
|
|
19
|
+
**Incorrect (duplicated state):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
class ItemScreen extends HookConsumerWidget {
|
|
23
|
+
Widget build(context, ref) {
|
|
24
|
+
final items = ref.watch(itemsProvider);
|
|
25
|
+
// Local state duplicating what provider knows
|
|
26
|
+
final selectedItem = useState<Item?>(null);
|
|
27
|
+
final isEditing = useState(false);
|
|
28
|
+
|
|
29
|
+
// Widget caches a derived value
|
|
30
|
+
final totalPrice = useState(0);
|
|
31
|
+
useEffect(() {
|
|
32
|
+
totalPrice.value = items.fold(0, (sum, i) => sum + i.price);
|
|
33
|
+
}, [items]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct (single owner, derived values):**
|
|
39
|
+
|
|
40
|
+
```dart
|
|
41
|
+
// Provider owns the state
|
|
42
|
+
@riverpod
|
|
43
|
+
class ItemsController extends _$ItemsController {
|
|
44
|
+
Item? get selectedItem => state.value?.firstWhereOrNull((i) => i.isSelected);
|
|
45
|
+
int get totalPrice => state.value?.fold(0, (sum, i) => sum + i.price) ?? 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Widget only has ephemeral UI state
|
|
49
|
+
class ItemScreen extends HookConsumerWidget {
|
|
50
|
+
Widget build(context, ref) {
|
|
51
|
+
final items = ref.watch(itemsProvider);
|
|
52
|
+
final selectedItem = ref.watch(itemsProvider.select((s) => s.selectedItem));
|
|
53
|
+
final isTextFieldFocused = useState(false); // Truly ephemeral
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Why it matters:**
|
|
59
|
+
- No "which value is authoritative?" confusion
|
|
60
|
+
- State updates propagate automatically
|
|
61
|
+
- Easier debugging: one place to inspect
|
|
62
|
+
- Prevents stale data bugs
|
|
63
|
+
|
|
64
|
+
**Detection questions:**
|
|
65
|
+
- Is the same data stored in both a provider and local widget state?
|
|
66
|
+
- Are there useEffect hooks syncing local state from provider state?
|
|
67
|
+
- Could two sources of truth disagree? What happens then?
|
|
68
|
+
- Is there derived data being cached instead of computed?
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Explicit Type Hierarchies Over Implicit Conventions
|
|
3
|
+
category: state
|
|
4
|
+
impact: HIGH
|
|
5
|
+
impactDescription: Centralizes decision logic
|
|
6
|
+
tags: factory, sealed-class, decision-logic, type-safety
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Explicit Type Hierarchies Over Implicit Conventions
|
|
10
|
+
|
|
11
|
+
Use factories to encapsulate decision logic. Widgets receive pre-computed decisions, not raw data to interpret.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Complex if/else chains determining which widget to render
|
|
15
|
+
- Same decision logic duplicated across multiple widgets
|
|
16
|
+
- Widgets receive data they only use to make decisions (not to display)
|
|
17
|
+
- New requirement would add another boolean parameter
|
|
18
|
+
|
|
19
|
+
**Incorrect (scattered decision logic):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
Widget _buildTrailing(BuildContext context, {required bool isExpired, required bool isTutorial}) {
|
|
23
|
+
if (quest.status.isClaimable && quest.isExpired) {
|
|
24
|
+
return QuestClaimButton(quest: quest, isExpired: true);
|
|
25
|
+
}
|
|
26
|
+
if (quest.canClaim) {
|
|
27
|
+
if (isTutorial) {
|
|
28
|
+
return PulsingGlowWrapper(child: QuestClaimButton(quest: quest, isTutorial: true));
|
|
29
|
+
}
|
|
30
|
+
return QuestClaimButton(quest: quest);
|
|
31
|
+
}
|
|
32
|
+
return _buildRewardBadge(context);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Correct (factory with type hierarchy):**
|
|
37
|
+
|
|
38
|
+
```dart
|
|
39
|
+
sealed class QuestClaimMode {
|
|
40
|
+
factory QuestClaimMode.fromContext({
|
|
41
|
+
required QuestEntity quest,
|
|
42
|
+
required UserEntity? user,
|
|
43
|
+
required bool isTutorialQuest,
|
|
44
|
+
}) {
|
|
45
|
+
if (quest.status.isClaimable && quest.isExpired) {
|
|
46
|
+
return QuestClaimModeExpired(...);
|
|
47
|
+
}
|
|
48
|
+
if (quest.canClaim) {
|
|
49
|
+
return isTutorialQuest
|
|
50
|
+
? QuestClaimModeTutorial(...)
|
|
51
|
+
: QuestClaimModeNormal(...);
|
|
52
|
+
}
|
|
53
|
+
return QuestClaimModePending(...);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Widget becomes a simple switch
|
|
58
|
+
Widget _buildTrailing() => switch (claimMode) {
|
|
59
|
+
QuestClaimModePending() => QuestRewardDisplay(...),
|
|
60
|
+
QuestClaimModeTutorial() => _wrapWithTutorialEffects(QuestClaimButton(mode: claimMode)),
|
|
61
|
+
QuestClaimModeNormal() || QuestClaimModeExpired() => QuestClaimButton(mode: claimMode),
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Why it matters:**
|
|
66
|
+
- Decision logic lives in one place (the factory)
|
|
67
|
+
- Widgets receive pre-computed decisions, not raw data to interpret
|
|
68
|
+
- Adding new modes is explicit and compiler-checked
|
|
69
|
+
- Testing is clearer: test the factory, then test each mode's rendering
|
|
70
|
+
|
|
71
|
+
**Detection questions:**
|
|
72
|
+
- Are there complex if/else chains determining which widget to render?
|
|
73
|
+
- Is the same decision logic duplicated across multiple widgets?
|
|
74
|
+
- Do widgets receive data they only use to make decisions (not to display)?
|
|
75
|
+
- Would a new requirement add another boolean parameter?
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Composition Over Configuration
|
|
3
|
+
category: structure
|
|
4
|
+
impact: MEDIUM
|
|
5
|
+
impactDescription: Simplifies widget APIs
|
|
6
|
+
tags: composition, widget-api, god-widget, parameters
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Composition Over Configuration
|
|
10
|
+
|
|
11
|
+
Small focused widgets over god widgets with many flags. Use builders and slots instead of boolean parameters.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Widget has more than 6-8 parameters
|
|
15
|
+
- Boolean parameters that are mutually exclusive
|
|
16
|
+
- Widget is really 3 different widgets pretending to be 1
|
|
17
|
+
- Adding a new variant would require another boolean flag
|
|
18
|
+
|
|
19
|
+
**Incorrect (god widget):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
AppButton(
|
|
23
|
+
label: 'Submit',
|
|
24
|
+
isPrimary: true,
|
|
25
|
+
isSecondary: false,
|
|
26
|
+
isDestructive: false,
|
|
27
|
+
isLoading: false,
|
|
28
|
+
isDisabled: false,
|
|
29
|
+
showIcon: true,
|
|
30
|
+
iconPosition: IconPosition.left,
|
|
31
|
+
size: ButtonSize.medium,
|
|
32
|
+
// ... 10 more parameters
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Correct (composed widgets):**
|
|
37
|
+
|
|
38
|
+
```dart
|
|
39
|
+
// Separate widgets for distinct purposes
|
|
40
|
+
class PrimaryButton extends StatelessWidget {
|
|
41
|
+
final VoidCallback? onPressed;
|
|
42
|
+
final Widget child;
|
|
43
|
+
final bool isLoading;
|
|
44
|
+
|
|
45
|
+
const PrimaryButton({
|
|
46
|
+
super.key,
|
|
47
|
+
required this.onPressed,
|
|
48
|
+
required this.child,
|
|
49
|
+
this.isLoading = false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ...
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Composition for custom layouts
|
|
56
|
+
PrimaryButton(
|
|
57
|
+
onPressed: handleSubmit,
|
|
58
|
+
child: Row(
|
|
59
|
+
mainAxisSize: MainAxisSize.min,
|
|
60
|
+
children: [
|
|
61
|
+
Icon(Icons.check),
|
|
62
|
+
SizedBox(width: 8),
|
|
63
|
+
Text('Submit'),
|
|
64
|
+
],
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Or slot-based API for common patterns
|
|
69
|
+
PrimaryButton.icon(
|
|
70
|
+
icon: Icons.check,
|
|
71
|
+
label: 'Submit',
|
|
72
|
+
onPressed: handleSubmit,
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Alternative: Use sealed class for mutually exclusive variants:**
|
|
77
|
+
|
|
78
|
+
```dart
|
|
79
|
+
sealed class ButtonVariant {
|
|
80
|
+
const ButtonVariant();
|
|
81
|
+
}
|
|
82
|
+
class PrimaryVariant extends ButtonVariant { ... }
|
|
83
|
+
class SecondaryVariant extends ButtonVariant { ... }
|
|
84
|
+
class DestructiveVariant extends ButtonVariant { ... }
|
|
85
|
+
|
|
86
|
+
class AppButton extends StatelessWidget {
|
|
87
|
+
final ButtonVariant variant;
|
|
88
|
+
final Widget child;
|
|
89
|
+
final VoidCallback? onPressed;
|
|
90
|
+
|
|
91
|
+
// Single widget, but variants are explicit and exhaustive
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Why it matters:**
|
|
96
|
+
- Each widget has one job
|
|
97
|
+
- New variants don't bloat existing widgets
|
|
98
|
+
- Easier to understand and test
|
|
99
|
+
- Flexible: compose for custom needs, use shortcuts for common ones
|
|
100
|
+
|
|
101
|
+
**Detection questions:**
|
|
102
|
+
- Does this widget have more than 6-8 parameters?
|
|
103
|
+
- Are there boolean parameters that are mutually exclusive?
|
|
104
|
+
- Is this widget really 3 different widgets pretending to be 1?
|
|
105
|
+
- Would adding a new variant require another boolean flag?
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Extract Shared Visual Patterns
|
|
3
|
+
category: structure
|
|
4
|
+
impact: MEDIUM-HIGH
|
|
5
|
+
impactDescription: Guarantees visual consistency
|
|
6
|
+
tags: widget-extraction, style-variants, ui-deduplication, decoration
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Extract Shared Visual Patterns
|
|
10
|
+
|
|
11
|
+
Deduplicate UI with style variants. When similar visual patterns appear 2+ times, extract a shared widget with an enum or sealed class for variants.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- Similar Container/decoration patterns across multiple widgets
|
|
15
|
+
- Visual elements (colors, padding, borders) vary based on state
|
|
16
|
+
- Design change would require updating multiple files
|
|
17
|
+
- Subtle inconsistencies between similar UI elements
|
|
18
|
+
|
|
19
|
+
**Incorrect (duplicated patterns):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
// In QuestClaimButton
|
|
23
|
+
Container(
|
|
24
|
+
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
25
|
+
decoration: BoxDecoration(
|
|
26
|
+
color: isExpired ? AppColors.gray400 : AppColors.forge,
|
|
27
|
+
borderRadius: BorderRadius.circular(20),
|
|
28
|
+
),
|
|
29
|
+
child: Row(children: [Text('grape'), Text('$reward')]),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// In QuestListTile (slightly different)
|
|
33
|
+
Container(
|
|
34
|
+
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
35
|
+
decoration: BoxDecoration(
|
|
36
|
+
borderRadius: BorderRadius.circular(20),
|
|
37
|
+
border: Border.all(color: ...),
|
|
38
|
+
),
|
|
39
|
+
child: Row(children: [Text('grape'), Text('$reward')]),
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Correct (shared widget with variants):**
|
|
44
|
+
|
|
45
|
+
```dart
|
|
46
|
+
enum QuestRewardStyle { filled, outlined, disabled }
|
|
47
|
+
|
|
48
|
+
class QuestRewardDisplay extends StatelessWidget {
|
|
49
|
+
final int reward;
|
|
50
|
+
final QuestRewardStyle style;
|
|
51
|
+
|
|
52
|
+
const QuestRewardDisplay({
|
|
53
|
+
super.key,
|
|
54
|
+
required this.reward,
|
|
55
|
+
this.style = QuestRewardStyle.filled,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
Widget build(context) {
|
|
59
|
+
return Container(
|
|
60
|
+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
61
|
+
decoration: style.buildDecoration(context),
|
|
62
|
+
child: Row(
|
|
63
|
+
mainAxisSize: MainAxisSize.min,
|
|
64
|
+
children: [
|
|
65
|
+
Text('grape'),
|
|
66
|
+
const SizedBox(width: 4),
|
|
67
|
+
Text('$reward', style: style.buildTextStyle(context)),
|
|
68
|
+
],
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
extension on QuestRewardStyle {
|
|
75
|
+
BoxDecoration buildDecoration(BuildContext context) => switch (this) {
|
|
76
|
+
QuestRewardStyle.filled => BoxDecoration(
|
|
77
|
+
color: AppColors.forge,
|
|
78
|
+
borderRadius: BorderRadius.circular(20),
|
|
79
|
+
),
|
|
80
|
+
QuestRewardStyle.outlined => BoxDecoration(
|
|
81
|
+
borderRadius: BorderRadius.circular(20),
|
|
82
|
+
border: Border.all(color: AppColors.forge),
|
|
83
|
+
),
|
|
84
|
+
QuestRewardStyle.disabled => BoxDecoration(
|
|
85
|
+
color: AppColors.gray400,
|
|
86
|
+
borderRadius: BorderRadius.circular(20),
|
|
87
|
+
),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
TextStyle buildTextStyle(BuildContext context) => switch (this) {
|
|
91
|
+
QuestRewardStyle.disabled => context.textTheme.bodyMedium!.copyWith(color: AppColors.gray600),
|
|
92
|
+
_ => context.textTheme.bodyMedium!,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Why it matters:**
|
|
98
|
+
- Visual consistency is guaranteed
|
|
99
|
+
- Style changes propagate automatically
|
|
100
|
+
- New variants are added in one place
|
|
101
|
+
- Reduces widget file sizes significantly
|
|
102
|
+
|
|
103
|
+
**Detection questions:**
|
|
104
|
+
- Are there similar Container/decoration patterns across multiple widgets?
|
|
105
|
+
- Do visual elements (colors, padding, borders) vary based on state?
|
|
106
|
+
- Would a design change require updating multiple files?
|
|
107
|
+
- Are there subtle inconsistencies between similar UI elements?
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Isolate Feature Responsibility (Wrapper Pattern)
|
|
3
|
+
category: structure
|
|
4
|
+
impact: HIGH
|
|
5
|
+
impactDescription: Features become removable
|
|
6
|
+
tags: wrapper, composition, feature-isolation, widget-extraction
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Isolate Feature Responsibility (Wrapper Pattern)
|
|
10
|
+
|
|
11
|
+
Extract optional feature logic into wrapper components. The wrapper owns all feature-specific state; the core component doesn't know the feature exists.
|
|
12
|
+
|
|
13
|
+
**Detection signals:**
|
|
14
|
+
- More than 30% of a widget's code dedicated to one optional feature
|
|
15
|
+
- Removing a feature requires deleting scattered lines throughout the file
|
|
16
|
+
- Multiple `if (featureEnabled)` checks spread across the widget
|
|
17
|
+
- State variables only used by one feature
|
|
18
|
+
|
|
19
|
+
**Incorrect (scattered feature logic):**
|
|
20
|
+
|
|
21
|
+
```dart
|
|
22
|
+
class QuestListScreen extends HookConsumerWidget {
|
|
23
|
+
Widget build(context, ref) {
|
|
24
|
+
// 50 lines of tutorial-specific state management
|
|
25
|
+
final tutorialKey = useMemoized(GlobalKey.new);
|
|
26
|
+
final overlayVisible = useState(true);
|
|
27
|
+
final cutoutRect = useState<Rect?>(null);
|
|
28
|
+
|
|
29
|
+
// Tutorial measurement logic mixed with list logic
|
|
30
|
+
useEffect(() {
|
|
31
|
+
if (isTutorialActive) {
|
|
32
|
+
// 30 lines of tutorial position measurement
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Main list rendering polluted with tutorial checks
|
|
37
|
+
return Stack(
|
|
38
|
+
children: [
|
|
39
|
+
questList,
|
|
40
|
+
if (showTutorialOverlay) TutorialOverlay(...),
|
|
41
|
+
],
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Correct (wrapper pattern):**
|
|
48
|
+
|
|
49
|
+
```dart
|
|
50
|
+
// Tutorial logic fully encapsulated
|
|
51
|
+
class TutorialQuestSpotlight extends HookConsumerWidget {
|
|
52
|
+
final Widget Function(BuildContext, {GlobalKey? tutorialKey}) builder;
|
|
53
|
+
|
|
54
|
+
Widget build(context, ref) {
|
|
55
|
+
// All tutorial state lives here
|
|
56
|
+
final tutorialKey = useMemoized(GlobalKey.new);
|
|
57
|
+
final overlayVisible = useState(true);
|
|
58
|
+
final cutoutRect = useState<Rect?>(null);
|
|
59
|
+
|
|
60
|
+
// Core list doesn't know tutorials exist
|
|
61
|
+
return Stack(
|
|
62
|
+
children: [
|
|
63
|
+
builder(context, tutorialKey: tutorialKey),
|
|
64
|
+
if (overlayVisible.value) TutorialOverlay(cutoutRect: cutoutRect.value),
|
|
65
|
+
],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clean core component
|
|
71
|
+
class QuestListScreen extends HookConsumerWidget {
|
|
72
|
+
Widget build(context, ref) {
|
|
73
|
+
return TutorialQuestSpotlight(
|
|
74
|
+
builder: (context, {tutorialKey}) => _buildQuestList(tutorialKey),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Why it matters:**
|
|
81
|
+
- Feature can be disabled/removed by removing one wrapper
|
|
82
|
+
- Core component remains focused and testable
|
|
83
|
+
- Feature logic is cohesive and isolated
|
|
84
|
+
- Multiple features can compose without polluting each other
|
|
85
|
+
|
|
86
|
+
**Detection questions:**
|
|
87
|
+
- Is more than 30% of a widget's code dedicated to one optional feature?
|
|
88
|
+
- Would removing a feature require deleting scattered lines throughout the file?
|
|
89
|
+
- Are there multiple `if (featureEnabled)` checks spread across the widget?
|
|
90
|
+
- Does the widget have state variables only used by one feature?
|