oh-my-customcode 0.30.7 → 0.30.9
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 +15 -13
- package/package.json +1 -1
- package/templates/.claude/agents/fe-flutter-agent.md +46 -0
- package/templates/.claude/ontology/agents.yaml +14 -2
- package/templates/.claude/ontology/graphs/agent-skill.json +2 -0
- package/templates/.claude/ontology/graphs/full-graph.json +13 -1
- package/templates/.claude/ontology/graphs/routing.json +2 -0
- package/templates/.claude/ontology/graphs/skill-rule.json +1 -0
- package/templates/.claude/ontology/skills.yaml +69 -59
- package/templates/.claude/skills/analysis/SKILL.md +1 -0
- package/templates/.claude/skills/dev-lead-routing/SKILL.md +4 -2
- package/templates/.claude/skills/flutter-best-practices/SKILL.md +431 -0
- package/templates/.claude/skills/intent-detection/patterns/agent-triggers.yaml +8 -0
- package/templates/guides/flutter/architecture.md +141 -0
- package/templates/guides/flutter/fundamentals.md +119 -0
- package/templates/guides/flutter/index.yaml +44 -0
- package/templates/guides/flutter/performance.md +119 -0
- package/templates/guides/flutter/state-management.md +144 -0
- package/templates/guides/flutter/testing.md +155 -0
- package/templates/guides/index.yaml +8 -0
- package/templates/manifest.json +4 -4
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter-best-practices
|
|
3
|
+
description: Flutter/Dart development best practices for widget composition, state management, and performance
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
Apply Flutter and Dart best practices from official documentation and community standards. Covers widget patterns, state management (Riverpod/BLoC), performance optimization, testing, security, and Dart 3.x language patterns.
|
|
10
|
+
|
|
11
|
+
## Core Principles
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Widget composition over inheritance
|
|
15
|
+
Unidirectional data flow
|
|
16
|
+
Immutable state
|
|
17
|
+
const by default
|
|
18
|
+
Platform-adaptive design
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Rules
|
|
22
|
+
|
|
23
|
+
### 1. Widget Patterns
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
composition:
|
|
27
|
+
prefer: "Small, focused StatelessWidget classes"
|
|
28
|
+
avoid: "Helper functions returning Widget (no Element identity)"
|
|
29
|
+
reason: "Flutter diffing depends on widget type identity"
|
|
30
|
+
|
|
31
|
+
const_constructors:
|
|
32
|
+
rule: "Mark all static widgets const"
|
|
33
|
+
pattern: "const Text('Hello'), const SizedBox(height: 8)"
|
|
34
|
+
impact: "Zero rebuild cost — compile-time constant"
|
|
35
|
+
|
|
36
|
+
sizing_widgets:
|
|
37
|
+
prefer: "SizedBox for spacing/sizing"
|
|
38
|
+
avoid: "Container when only size is needed"
|
|
39
|
+
reason: "SizedBox is lighter, no decoration overhead"
|
|
40
|
+
|
|
41
|
+
state_choice:
|
|
42
|
+
StatelessWidget: "No mutable state, pure rendering"
|
|
43
|
+
StatefulWidget: "Local ephemeral state (animations, form input)"
|
|
44
|
+
InheritedWidget: "Data propagation down tree (base of Provider)"
|
|
45
|
+
|
|
46
|
+
build_context:
|
|
47
|
+
rule: "Never store BuildContext across async gaps"
|
|
48
|
+
pattern: |
|
|
49
|
+
// BAD
|
|
50
|
+
final ctx = context;
|
|
51
|
+
await Future.delayed(Duration(seconds: 1));
|
|
52
|
+
Navigator.of(ctx).push(...); // context may be invalid
|
|
53
|
+
|
|
54
|
+
// GOOD
|
|
55
|
+
if (!mounted) return;
|
|
56
|
+
Navigator.of(context).push(...);
|
|
57
|
+
|
|
58
|
+
keys:
|
|
59
|
+
ValueKey: "When items have unique business identity"
|
|
60
|
+
ObjectKey: "When items are objects without natural key"
|
|
61
|
+
UniqueKey: "Force rebuild on every build (rare)"
|
|
62
|
+
GlobalKey: "Cross-widget state access (use sparingly)"
|
|
63
|
+
|
|
64
|
+
lists:
|
|
65
|
+
prefer: "ListView.builder for >10 items (lazy construction)"
|
|
66
|
+
avoid: "ListView(children: [...]) for large lists"
|
|
67
|
+
optimization: "Set itemExtent to skip intrinsic layout passes"
|
|
68
|
+
|
|
69
|
+
layout:
|
|
70
|
+
rule: "Constraints flow down, sizes flow up"
|
|
71
|
+
common_error: "Unbounded constraints in Column/Row children"
|
|
72
|
+
fix: "Wrap with Expanded/Flexible or constrain explicitly"
|
|
73
|
+
|
|
74
|
+
repaint_boundary:
|
|
75
|
+
when: "Frequently repainting subtrees (animations, video, maps)"
|
|
76
|
+
effect: "Isolates paint scope, prevents cascade repaints"
|
|
77
|
+
detect: "DevTools → highlight repaints toggle"
|
|
78
|
+
|
|
79
|
+
slivers:
|
|
80
|
+
prefer: "CustomScrollView + SliverList.builder for complex scrolling"
|
|
81
|
+
use_for: "Floating headers, parallax, mixed scroll content"
|
|
82
|
+
avoid: "Nested ListView in ListView"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. State Management
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
default_choice:
|
|
89
|
+
new_projects: "Riverpod 3.0"
|
|
90
|
+
enterprise: "BLoC 9.0"
|
|
91
|
+
simple_prototypes: "setState or Provider"
|
|
92
|
+
avoid: "GetX (maintenance crisis, runtime crashes)"
|
|
93
|
+
|
|
94
|
+
riverpod_patterns:
|
|
95
|
+
reactive_read: "ref.watch(provider) — in build methods only"
|
|
96
|
+
one_time_read: "ref.read(provider) — in callbacks, onPressed"
|
|
97
|
+
never: "ref.watch inside non-build methods"
|
|
98
|
+
async_state: "AsyncNotifier + AsyncValue (loading/data/error)"
|
|
99
|
+
family: "family modifier for parameterized providers"
|
|
100
|
+
keep_alive: "Only when justified (expensive computations)"
|
|
101
|
+
invalidate_vs_refresh: |
|
|
102
|
+
ref.invalidate(provider) // reset to loading, lazy re-fetch
|
|
103
|
+
ref.refresh(provider) // immediate re-fetch, return new value
|
|
104
|
+
|
|
105
|
+
bloc_patterns:
|
|
106
|
+
one_event_per_action: "One UI action = one event class"
|
|
107
|
+
cubit_vs_bloc: |
|
|
108
|
+
Cubit: direct emit(state) — for simple state changes
|
|
109
|
+
Bloc: event → state mapping — when audit trail needed
|
|
110
|
+
never: "Emit state in constructor body"
|
|
111
|
+
listener_vs_consumer: |
|
|
112
|
+
BlocListener: side effects (navigation, snackbar)
|
|
113
|
+
BlocConsumer: rebuild UI + side effects
|
|
114
|
+
BlocBuilder: rebuild UI only (most common)
|
|
115
|
+
stream_management: "Cancel subscriptions in close()"
|
|
116
|
+
|
|
117
|
+
state_immutability:
|
|
118
|
+
rule: "All state objects must be immutable"
|
|
119
|
+
tool: "freezed package for copyWith/==/hashCode generation"
|
|
120
|
+
pattern: |
|
|
121
|
+
@freezed
|
|
122
|
+
class UserState with _$UserState {
|
|
123
|
+
const factory UserState({
|
|
124
|
+
required String name,
|
|
125
|
+
required int age,
|
|
126
|
+
@Default(false) bool isLoading,
|
|
127
|
+
}) = _UserState;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
result_type:
|
|
131
|
+
rule: "Return Result<T> from repositories, never throw"
|
|
132
|
+
pattern: |
|
|
133
|
+
sealed class Result<T> {
|
|
134
|
+
const Result();
|
|
135
|
+
}
|
|
136
|
+
final class Ok<T> extends Result<T> {
|
|
137
|
+
const Ok(this.value);
|
|
138
|
+
final T value;
|
|
139
|
+
}
|
|
140
|
+
final class Error<T> extends Result<T> {
|
|
141
|
+
const Error(this.error);
|
|
142
|
+
final Exception error;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 3. Performance
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
build_optimization:
|
|
150
|
+
const_widgets: "Mark immutable widgets const — zero rebuild"
|
|
151
|
+
localize_setState: "Call setState on smallest possible subtree"
|
|
152
|
+
extract_widgets: "StatelessWidget class > helper method"
|
|
153
|
+
child_parameter: |
|
|
154
|
+
// GOOD: static child passed through
|
|
155
|
+
AnimatedBuilder(
|
|
156
|
+
animation: controller,
|
|
157
|
+
builder: (context, child) => Transform.rotate(
|
|
158
|
+
angle: controller.value,
|
|
159
|
+
child: child, // not rebuilt
|
|
160
|
+
),
|
|
161
|
+
child: const ExpensiveWidget(), // built once
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
rebuild_avoidance:
|
|
165
|
+
consumer_placement: "Place Consumer/ListenableBuilder as deep as possible"
|
|
166
|
+
read_in_callbacks: "context.read<T>() not context.watch<T>() in handlers"
|
|
167
|
+
selector: "Use BlocSelector/Selector for partial state rebuilds"
|
|
168
|
+
|
|
169
|
+
rendering:
|
|
170
|
+
avoid_opacity: "Use color.withValues(alpha:) (Flutter 3.27+) or AnimatedOpacity instead; color.withOpacity() is deprecated"
|
|
171
|
+
avoid_clip: "Pre-clip static content; avoid ClipRRect in animations"
|
|
172
|
+
minimize_saveLayer: "ShaderMask, ColorFilter, Chip trigger saveLayer"
|
|
173
|
+
|
|
174
|
+
compute_offloading:
|
|
175
|
+
rule: "Isolate.run() for operations >16ms (one frame budget)"
|
|
176
|
+
web_compatible: "Use compute() for web-compatible apps"
|
|
177
|
+
use_for: "JSON parsing, image processing, complex filtering"
|
|
178
|
+
|
|
179
|
+
frame_budget:
|
|
180
|
+
target: "<8ms build + <8ms render = 16.67ms (60fps)"
|
|
181
|
+
profiling: "flutter run --profile, not debug mode"
|
|
182
|
+
tool: "DevTools Performance view for jank detection"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 4. Testing
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
test_pyramid:
|
|
189
|
+
unit: "Single class/function — fast, low confidence"
|
|
190
|
+
widget: "Single widget tree — fast, medium confidence"
|
|
191
|
+
integration: "Full app on device — slow, high confidence"
|
|
192
|
+
golden: "Visual regression via matchesGoldenFile()"
|
|
193
|
+
|
|
194
|
+
widget_test_pattern: |
|
|
195
|
+
testWidgets('shows loading then content', (tester) async {
|
|
196
|
+
await tester.pumpWidget(
|
|
197
|
+
ProviderScope(
|
|
198
|
+
overrides: [productsProvider.overrideWith((_) => fakeProducts)],
|
|
199
|
+
child: const MaterialApp(home: ProductListScreen()),
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
203
|
+
await tester.pumpAndSettle();
|
|
204
|
+
expect(find.byType(ProductCard), findsNWidgets(3));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
mocking:
|
|
208
|
+
prefer: "mocktail (null-safe, no codegen)"
|
|
209
|
+
avoid: "Legacy mockito with build_runner"
|
|
210
|
+
fakes: "Use Fake implementations for deterministic tests"
|
|
211
|
+
pattern: |
|
|
212
|
+
class FakeProductRepository extends Fake implements ProductRepository {
|
|
213
|
+
@override
|
|
214
|
+
Future<Result<List<Product>>> getAll() async => Ok(testProducts);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
bloc_testing: |
|
|
218
|
+
blocTest<AuthBloc, AuthState>(
|
|
219
|
+
'emits [loading, success] when login succeeds',
|
|
220
|
+
build: () => AuthBloc(FakeAuthRepository()),
|
|
221
|
+
act: (bloc) => bloc.add(LoginRequested('user', 'pass')),
|
|
222
|
+
expect: () => [AuthLoading(), isA<AuthSuccess>()],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
coverage_target:
|
|
226
|
+
widget_tests: "80%+ for UI logic"
|
|
227
|
+
unit_tests: "90%+ for business logic"
|
|
228
|
+
integration: "Critical user flows only"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 5. Security
|
|
232
|
+
|
|
233
|
+
```yaml
|
|
234
|
+
secrets:
|
|
235
|
+
never: "Hardcode API keys, tokens, or credentials in source"
|
|
236
|
+
best: "Backend proxy for all sensitive API calls"
|
|
237
|
+
use: "--dart-define-from-file=.env for NON-SECRET build config only (feature flags, environment URLs)"
|
|
238
|
+
warning: "dart-define values are embedded in compiled binary and extractable via static analysis. Use only for non-secret build configuration (feature flags, environment URLs)."
|
|
239
|
+
|
|
240
|
+
storage:
|
|
241
|
+
sensitive_data: "flutter_secure_storage v10+ (Keychain/Keystore)"
|
|
242
|
+
never: "SharedPreferences for tokens, PII, or credentials"
|
|
243
|
+
ios: "AppleOptions(useSecureEnclave: true) for high-value"
|
|
244
|
+
android: "AndroidOptions(encryptedSharedPreferences: true)"
|
|
245
|
+
web_warning: "flutter_secure_storage on Web uses localStorage by default, which is accessible to any JavaScript on the page (XSS vulnerable). For Web targets, use HttpOnly cookies managed by backend or in-memory-only storage for sensitive data."
|
|
246
|
+
|
|
247
|
+
network:
|
|
248
|
+
tls: "Certificate pinning (SPKI) for financial/health apps"
|
|
249
|
+
cleartext: "cleartextTrafficPermitted=false in network_security_config"
|
|
250
|
+
ios_ats: "NSAllowsArbitraryLoads=false (default, never override)"
|
|
251
|
+
|
|
252
|
+
release_builds:
|
|
253
|
+
obfuscate: "--obfuscate --split-debug-info=<path>"
|
|
254
|
+
proguard: "Configure android/app/proguard-rules.pro"
|
|
255
|
+
debug_check: "Remove all kDebugMode unguarded print() calls"
|
|
256
|
+
rule: "Never ship debug APK to production"
|
|
257
|
+
|
|
258
|
+
deep_links:
|
|
259
|
+
validate: "Allow-list all URI parameters with RegExp"
|
|
260
|
+
reject: "Arbitrary schemes and unvalidated paths"
|
|
261
|
+
prefer: "Universal Links (iOS) and App Links (Android) only"
|
|
262
|
+
|
|
263
|
+
logging:
|
|
264
|
+
rule: "Guard print() with kDebugMode"
|
|
265
|
+
prefer: "dart:developer log() for debug output"
|
|
266
|
+
never: "Log PII, tokens, or credentials"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 6. Dart Language Patterns
|
|
270
|
+
|
|
271
|
+
```yaml
|
|
272
|
+
naming:
|
|
273
|
+
types: "UpperCamelCase for classes, enums, typedefs, extensions, mixins (e.g., HttpClient, JsonParser)"
|
|
274
|
+
variables: "lowerCamelCase for variables, parameters, named constants (e.g., itemCount, defaultTimeout)"
|
|
275
|
+
libraries: "lowercase_with_underscores for libraries, packages, directories, source files (e.g., my_package, slider_menu.dart)"
|
|
276
|
+
constants: "lowerCamelCase for const (e.g., const defaultTimeout = 30), NOT SCREAMING_CAPS"
|
|
277
|
+
private: "Prefix with underscore for library-private (e.g., _internalCache, _helper())"
|
|
278
|
+
boolean: "Prefix with is/has/can/should for booleans (e.g., isEnabled, hasData, canScroll)"
|
|
279
|
+
avoid: "Hungarian notation, type prefixes (strName, lstItems), abbreviations unless universally known (e.g., ok: http, url, id; avoid: mgr, ctx, btn)"
|
|
280
|
+
|
|
281
|
+
null_safety:
|
|
282
|
+
default: "Non-nullable types — use ? only when null is meaningful"
|
|
283
|
+
avoid_bang: "Minimize ! operator — use only when null is logically impossible"
|
|
284
|
+
late: "Only when initialization is guaranteed before use"
|
|
285
|
+
pattern: |
|
|
286
|
+
// GOOD
|
|
287
|
+
final name = user?.name ?? 'Anonymous';
|
|
288
|
+
|
|
289
|
+
// AVOID
|
|
290
|
+
final name = user!.name; // crashes if null
|
|
291
|
+
|
|
292
|
+
sealed_classes:
|
|
293
|
+
use_for: "Exhaustive pattern matching on state/result types"
|
|
294
|
+
pattern: |
|
|
295
|
+
sealed class AuthState {}
|
|
296
|
+
class AuthInitial extends AuthState {}
|
|
297
|
+
class AuthLoading extends AuthState {}
|
|
298
|
+
class AuthSuccess extends AuthState { final User user; AuthSuccess(this.user); }
|
|
299
|
+
class AuthError extends AuthState { final String message; AuthError(this.message); }
|
|
300
|
+
|
|
301
|
+
// Exhaustive switch — compiler enforces all cases
|
|
302
|
+
return switch (state) {
|
|
303
|
+
AuthInitial() => LoginScreen(),
|
|
304
|
+
AuthLoading() => CircularProgressIndicator(),
|
|
305
|
+
AuthSuccess(:final user) => HomeScreen(user: user),
|
|
306
|
+
AuthError(:final message) => ErrorWidget(message),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
records:
|
|
310
|
+
use_for: "Lightweight multi-value returns without class boilerplate"
|
|
311
|
+
pattern: "(String name, int age) getUserInfo() => ('Alice', 30);"
|
|
312
|
+
avoid: "Records for complex data — use freezed classes instead"
|
|
313
|
+
|
|
314
|
+
extension_types:
|
|
315
|
+
use_for: "Zero-cost type wrappers for primitive IDs"
|
|
316
|
+
pattern: "extension type UserId(int id) implements int {}"
|
|
317
|
+
|
|
318
|
+
immutability:
|
|
319
|
+
prefer: "final variables, const constructors"
|
|
320
|
+
collections: "UnmodifiableListView for exposed lists"
|
|
321
|
+
models: "freezed package for data classes"
|
|
322
|
+
|
|
323
|
+
async:
|
|
324
|
+
streams: "async* yield for reactive data pipelines"
|
|
325
|
+
futures: "async/await for sequential async operations"
|
|
326
|
+
isolates: "Isolate.run() for CPU-intensive work >16ms"
|
|
327
|
+
|
|
328
|
+
dynamic:
|
|
329
|
+
avoid: "dynamic type — use generics or Object? instead"
|
|
330
|
+
reason: "No compile-time type checking, reduces IDE support"
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 7. Architecture & Project Structure
|
|
334
|
+
|
|
335
|
+
```yaml
|
|
336
|
+
default_structure:
|
|
337
|
+
small_app: |
|
|
338
|
+
lib/
|
|
339
|
+
models/
|
|
340
|
+
services/
|
|
341
|
+
screens/
|
|
342
|
+
widgets/
|
|
343
|
+
medium_app: |
|
|
344
|
+
lib/
|
|
345
|
+
ui/
|
|
346
|
+
core/themes/, core/widgets/
|
|
347
|
+
<feature>/<feature>_screen.dart
|
|
348
|
+
<feature>/<feature>_viewmodel.dart
|
|
349
|
+
data/
|
|
350
|
+
repositories/<entity>_repository.dart
|
|
351
|
+
services/<source>_service.dart
|
|
352
|
+
domain/ (optional)
|
|
353
|
+
use_cases/
|
|
354
|
+
large_app: |
|
|
355
|
+
lib/
|
|
356
|
+
core/ (shared)
|
|
357
|
+
features/<feature>/
|
|
358
|
+
data/datasources/, data/repositories/
|
|
359
|
+
domain/entities/, domain/usecases/
|
|
360
|
+
presentation/bloc/, presentation/pages/
|
|
361
|
+
|
|
362
|
+
navigation:
|
|
363
|
+
default: "go_router (official recommendation)"
|
|
364
|
+
pattern: |
|
|
365
|
+
GoRoute(
|
|
366
|
+
path: '/product/:id',
|
|
367
|
+
builder: (context, state) {
|
|
368
|
+
final id = state.pathParameters['id']!;
|
|
369
|
+
return ProductDetailScreen(id: id);
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
go_vs_push: |
|
|
373
|
+
context.go('/path') // replaces stack (navigation reset)
|
|
374
|
+
context.push('/path') // adds to stack (back button works)
|
|
375
|
+
|
|
376
|
+
dependency_injection:
|
|
377
|
+
riverpod: "Built-in — providers as DI (default)"
|
|
378
|
+
getit: "GetIt + injectable — for non-Riverpod projects"
|
|
379
|
+
rule: "Dependency direction always inward (UI → ViewModel → Repository → Service)"
|
|
380
|
+
|
|
381
|
+
environments:
|
|
382
|
+
pattern: "Flavors + --dart-define for multi-environment builds"
|
|
383
|
+
command: "flutter run --flavor development --target lib/main/main_development.dart"
|
|
384
|
+
rule: "Separate bundle IDs, API URLs, and Firebase config per flavor"
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Default Stack
|
|
388
|
+
|
|
389
|
+
When starting a new Flutter project, recommend this stack:
|
|
390
|
+
|
|
391
|
+
```yaml
|
|
392
|
+
state_management: Riverpod 3.0
|
|
393
|
+
navigation: go_router
|
|
394
|
+
models: freezed + json_serializable
|
|
395
|
+
di: Riverpod (built-in)
|
|
396
|
+
http: dio
|
|
397
|
+
linting: very_good_analysis
|
|
398
|
+
testing: flutter_test + mocktail
|
|
399
|
+
structure: Official MVVM (lib/{ui,data}/)
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Enterprise Stack
|
|
403
|
+
|
|
404
|
+
For regulated or large-team projects:
|
|
405
|
+
|
|
406
|
+
```yaml
|
|
407
|
+
state_management: BLoC 9.0 + Cubit
|
|
408
|
+
navigation: go_router or auto_route
|
|
409
|
+
models: freezed + json_serializable
|
|
410
|
+
di: GetIt + injectable (or Riverpod)
|
|
411
|
+
http: dio with interceptors
|
|
412
|
+
testing: flutter_test + bloc_test + mocktail (80%+ coverage)
|
|
413
|
+
structure: Clean Architecture (features/{feature}/{presentation,domain,data}/)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Application
|
|
417
|
+
|
|
418
|
+
When writing or reviewing Flutter/Dart code:
|
|
419
|
+
|
|
420
|
+
1. **Always** use const constructors for static widgets
|
|
421
|
+
2. **Always** return Result<T> from repositories, never throw
|
|
422
|
+
3. **Always** use flutter_secure_storage for sensitive data
|
|
423
|
+
4. **Prefer** Riverpod 3.0 for new projects, BLoC for enterprise
|
|
424
|
+
5. **Prefer** StatelessWidget classes over helper functions
|
|
425
|
+
6. **Prefer** sealed classes for state/result types (exhaustive matching)
|
|
426
|
+
7. **Use** freezed for all data model classes
|
|
427
|
+
8. **Use** go_router for navigation with deep linking
|
|
428
|
+
9. **Guard** all print() with kDebugMode
|
|
429
|
+
10. **Never** use GetX for new projects (maintenance risk)
|
|
430
|
+
11. **Never** store sensitive data in SharedPreferences
|
|
431
|
+
12. **Never** hardcode API keys in source code
|
|
@@ -102,6 +102,14 @@ agents:
|
|
|
102
102
|
supported_actions: [review, create, fix, refactor]
|
|
103
103
|
base_confidence: 40
|
|
104
104
|
|
|
105
|
+
fe-flutter-agent:
|
|
106
|
+
keywords:
|
|
107
|
+
korean: [플러터, 다트, 모바일앱, 위젯]
|
|
108
|
+
english: [flutter, dart, riverpod, bloc, widget, pubspec, "flutter app"]
|
|
109
|
+
file_patterns: ["*.dart", "pubspec.yaml", "pubspec.lock", "analysis_options.yaml"]
|
|
110
|
+
supported_actions: [review, create, fix, refactor, test]
|
|
111
|
+
base_confidence: 40
|
|
112
|
+
|
|
105
113
|
# SW Engineers - Backend
|
|
106
114
|
be-fastapi-expert:
|
|
107
115
|
keywords:
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Flutter Architecture
|
|
2
|
+
|
|
3
|
+
> Reference: docs.flutter.dev/app-architecture
|
|
4
|
+
|
|
5
|
+
## Official MVVM Pattern
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
UI Layer
|
|
9
|
+
├─ View (Widget) — display only, no business logic
|
|
10
|
+
└─ ViewModel (ChangeNotifier / Notifier) — state + commands
|
|
11
|
+
|
|
12
|
+
Data Layer
|
|
13
|
+
├─ Repository — single source of truth per domain
|
|
14
|
+
└─ Service — stateless external API wrapper
|
|
15
|
+
|
|
16
|
+
Domain Layer (optional)
|
|
17
|
+
└─ UseCase — cross-repository business logic
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Dependency Rules
|
|
21
|
+
|
|
22
|
+
- View knows only its ViewModel
|
|
23
|
+
- ViewModel knows Repositories (private)
|
|
24
|
+
- Repository knows Services (private)
|
|
25
|
+
- **Direction always inward** — UI depends on data, never reverse
|
|
26
|
+
|
|
27
|
+
### ViewModel with Commands
|
|
28
|
+
|
|
29
|
+
```dart
|
|
30
|
+
class HomeViewModel extends ChangeNotifier {
|
|
31
|
+
HomeViewModel({required BookingRepository bookingRepository})
|
|
32
|
+
: _bookingRepository = bookingRepository {
|
|
33
|
+
load = Command0(_load)..execute();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
final BookingRepository _bookingRepository;
|
|
37
|
+
late final Command0 load;
|
|
38
|
+
|
|
39
|
+
List<BookingSummary> _bookings = [];
|
|
40
|
+
UnmodifiableListView<BookingSummary> get bookings =>
|
|
41
|
+
UnmodifiableListView(_bookings);
|
|
42
|
+
|
|
43
|
+
Future<Result> _load() async {
|
|
44
|
+
final result = await _bookingRepository.getBookingsList();
|
|
45
|
+
if (result case Ok(:final value)) _bookings = value;
|
|
46
|
+
notifyListeners();
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Project Structure
|
|
53
|
+
|
|
54
|
+
### Medium Apps (Official MVVM)
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
lib/
|
|
58
|
+
├── ui/
|
|
59
|
+
│ ├── core/
|
|
60
|
+
│ │ ├── themes/app_theme.dart
|
|
61
|
+
│ │ └── widgets/loading_indicator.dart
|
|
62
|
+
│ └── home/
|
|
63
|
+
│ ├── home_screen.dart
|
|
64
|
+
│ └── home_viewmodel.dart
|
|
65
|
+
├── data/
|
|
66
|
+
│ ├── repositories/booking_repository.dart
|
|
67
|
+
│ └── services/api_service.dart
|
|
68
|
+
└── domain/ (optional)
|
|
69
|
+
└── use_cases/get_bookings_usecase.dart
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Large Apps (Clean Architecture)
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
lib/
|
|
76
|
+
├── core/
|
|
77
|
+
│ ├── error/failures.dart
|
|
78
|
+
│ ├── network/api_client.dart
|
|
79
|
+
│ └── utils/extensions.dart
|
|
80
|
+
└── features/
|
|
81
|
+
└── auth/
|
|
82
|
+
├── data/
|
|
83
|
+
│ ├── datasources/auth_remote_datasource.dart
|
|
84
|
+
│ ├── models/user_model.dart
|
|
85
|
+
│ └── repositories/auth_repository_impl.dart
|
|
86
|
+
├── domain/
|
|
87
|
+
│ ├── entities/user.dart
|
|
88
|
+
│ ├── repositories/auth_repository.dart
|
|
89
|
+
│ └── use_cases/login_usecase.dart
|
|
90
|
+
└── presentation/
|
|
91
|
+
├── bloc/auth_bloc.dart
|
|
92
|
+
├── pages/login_page.dart
|
|
93
|
+
└── widgets/login_form.dart
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Navigation (go_router)
|
|
97
|
+
|
|
98
|
+
```dart
|
|
99
|
+
final router = GoRouter(
|
|
100
|
+
initialLocation: '/',
|
|
101
|
+
routes: [
|
|
102
|
+
GoRoute(
|
|
103
|
+
path: '/',
|
|
104
|
+
builder: (context, state) => const HomeScreen(),
|
|
105
|
+
),
|
|
106
|
+
GoRoute(
|
|
107
|
+
path: '/product/:id',
|
|
108
|
+
builder: (context, state) {
|
|
109
|
+
final id = state.pathParameters['id']!;
|
|
110
|
+
return ProductDetailScreen(id: id);
|
|
111
|
+
},
|
|
112
|
+
),
|
|
113
|
+
ShellRoute(
|
|
114
|
+
builder: (context, state, child) => ScaffoldWithNavBar(child: child),
|
|
115
|
+
routes: [/* nested tab routes */],
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
redirect: (context, state) {
|
|
119
|
+
final isLoggedIn = /* check auth */;
|
|
120
|
+
if (!isLoggedIn && state.matchedLocation != '/login') return '/login';
|
|
121
|
+
return null;
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Data Models (freezed)
|
|
127
|
+
|
|
128
|
+
```dart
|
|
129
|
+
@freezed
|
|
130
|
+
class Product with _$Product {
|
|
131
|
+
const factory Product({
|
|
132
|
+
required int id,
|
|
133
|
+
required String name,
|
|
134
|
+
required double price,
|
|
135
|
+
@Default('') String description,
|
|
136
|
+
}) = _Product;
|
|
137
|
+
|
|
138
|
+
factory Product.fromJson(Map<String, dynamic> json) =>
|
|
139
|
+
_$ProductFromJson(json);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Flutter Fundamentals
|
|
2
|
+
|
|
3
|
+
> Reference: Flutter Official Documentation (docs.flutter.dev)
|
|
4
|
+
|
|
5
|
+
## Widget System
|
|
6
|
+
|
|
7
|
+
Flutter's UI is built with a composition of widgets. Everything is a widget — including padding, alignment, and decoration.
|
|
8
|
+
|
|
9
|
+
### Widget Types
|
|
10
|
+
|
|
11
|
+
| Type | Use Case | State |
|
|
12
|
+
|------|----------|-------|
|
|
13
|
+
| `StatelessWidget` | Pure rendering, no mutable state | Immutable |
|
|
14
|
+
| `StatefulWidget` | Local ephemeral state (animations, forms) | Mutable via `State<T>` |
|
|
15
|
+
| `InheritedWidget` | Data propagation down widget tree | Foundation of Provider |
|
|
16
|
+
|
|
17
|
+
### Widget Lifecycle
|
|
18
|
+
|
|
19
|
+
```dart
|
|
20
|
+
// StatefulWidget lifecycle
|
|
21
|
+
class MyWidget extends StatefulWidget {
|
|
22
|
+
@override
|
|
23
|
+
State<MyWidget> createState() => _MyWidgetState();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class _MyWidgetState extends State<MyWidget> {
|
|
27
|
+
@override
|
|
28
|
+
void initState() { super.initState(); /* one-time setup */ }
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
void didChangeDependencies() { super.didChangeDependencies(); /* InheritedWidget changed */ }
|
|
32
|
+
|
|
33
|
+
@override
|
|
34
|
+
void didUpdateWidget(MyWidget oldWidget) { super.didUpdateWidget(oldWidget); /* parent rebuilt */ }
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
Widget build(BuildContext context) { return Container(); /* called on every rebuild */ }
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
void dispose() { /* cleanup controllers, subscriptions */ super.dispose(); }
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Three-Tree Architecture
|
|
45
|
+
|
|
46
|
+
Flutter maintains three parallel trees:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Widget Tree (immutable descriptions)
|
|
50
|
+
↓ createElement()
|
|
51
|
+
Element Tree (persistent, mutable handles)
|
|
52
|
+
↓ createRenderObject()
|
|
53
|
+
RenderObject Tree (layout + paint primitives)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- **Widget**: Lightweight, immutable configuration. Rebuilt frequently.
|
|
57
|
+
- **Element**: Persistent handle that manages widget lifecycle. Enables efficient diffing.
|
|
58
|
+
- **RenderObject**: Expensive layout/paint primitives. Only updated when properties change.
|
|
59
|
+
|
|
60
|
+
### Layout Algorithm
|
|
61
|
+
|
|
62
|
+
**Constraints go down, sizes go up, parent decides position.**
|
|
63
|
+
|
|
64
|
+
```dart
|
|
65
|
+
// Parent passes BoxConstraints to child
|
|
66
|
+
// Child returns its chosen Size within those constraints
|
|
67
|
+
// Parent positions child at an Offset
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Common layout errors:
|
|
71
|
+
- Unbounded height/width in `Column`/`Row` → wrap with `Expanded` or `Flexible`
|
|
72
|
+
- `Viewport was given unbounded height` → provide explicit height or use `SliverList`
|
|
73
|
+
|
|
74
|
+
## BuildContext
|
|
75
|
+
|
|
76
|
+
`BuildContext` is a handle to the widget's location in the Element tree.
|
|
77
|
+
|
|
78
|
+
```dart
|
|
79
|
+
// Access inherited data
|
|
80
|
+
final theme = Theme.of(context);
|
|
81
|
+
final mediaQuery = MediaQuery.of(context);
|
|
82
|
+
|
|
83
|
+
// NEVER store context across async gaps
|
|
84
|
+
// ALWAYS check mounted before using context after await
|
|
85
|
+
if (!mounted) return;
|
|
86
|
+
Navigator.of(context).push(...);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Keys
|
|
90
|
+
|
|
91
|
+
| Key Type | When to Use |
|
|
92
|
+
|----------|-------------|
|
|
93
|
+
| `ValueKey` | Items with unique business identity (user ID, product SKU) |
|
|
94
|
+
| `ObjectKey` | Items without natural key (use the object itself) |
|
|
95
|
+
| `UniqueKey` | Force new Element every build (rare, expensive) |
|
|
96
|
+
| `GlobalKey` | Cross-widget state access (use sparingly — breaks encapsulation) |
|
|
97
|
+
|
|
98
|
+
```dart
|
|
99
|
+
// Reorderable list MUST use keys
|
|
100
|
+
ListView(
|
|
101
|
+
children: items.map((item) => ListTile(
|
|
102
|
+
key: ValueKey(item.id),
|
|
103
|
+
title: Text(item.name),
|
|
104
|
+
)).toList(),
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Package Recommendations
|
|
109
|
+
|
|
110
|
+
| Category | Package | Notes |
|
|
111
|
+
|----------|---------|-------|
|
|
112
|
+
| State (default) | `flutter_riverpod` | Compile-time safe, built-in DI |
|
|
113
|
+
| State (enterprise) | `flutter_bloc` | Event-driven, audit trails |
|
|
114
|
+
| Navigation | `go_router` | Official, deep linking, web |
|
|
115
|
+
| Models | `freezed` + `json_serializable` | Immutable, code-gen |
|
|
116
|
+
| HTTP | `dio` | Interceptors, cancellation |
|
|
117
|
+
| Linting | `very_good_analysis` | Community standard rules |
|
|
118
|
+
| Testing | `mocktail` | Null-safe mocking, no codegen |
|
|
119
|
+
| Secure Storage | `flutter_secure_storage` | Keychain/Keystore |
|