@el-j/magic-helix-plugins 4.0.0-beta.6 → 4.0.0-beta.7
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/dist/dart/templates/flutter.md +340 -0
- package/dist/dart/templates/lang-dart.md +233 -0
- package/dist/elixir/templates/lang-elixir.md +147 -0
- package/dist/index.cjs +147 -1
- package/dist/index.mjs +575 -22
- package/package.json +1 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Flutter Development Guidelines
|
|
2
|
+
|
|
3
|
+
## Architecture & Structure
|
|
4
|
+
|
|
5
|
+
### Project Organization
|
|
6
|
+
```
|
|
7
|
+
lib/
|
|
8
|
+
├── main.dart
|
|
9
|
+
├── app.dart
|
|
10
|
+
├── core/
|
|
11
|
+
│ ├── constants/
|
|
12
|
+
│ ├── theme/
|
|
13
|
+
│ └── utils/
|
|
14
|
+
├── features/
|
|
15
|
+
│ ├── auth/
|
|
16
|
+
│ │ ├── data/
|
|
17
|
+
│ │ ├── domain/
|
|
18
|
+
│ │ └── presentation/
|
|
19
|
+
│ └── home/
|
|
20
|
+
└── shared/
|
|
21
|
+
├── widgets/
|
|
22
|
+
└── models/
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Widget Best Practices
|
|
26
|
+
|
|
27
|
+
### StatelessWidget
|
|
28
|
+
```dart
|
|
29
|
+
class UserCard extends StatelessWidget {
|
|
30
|
+
const UserCard({
|
|
31
|
+
super.key,
|
|
32
|
+
required this.user,
|
|
33
|
+
this.onTap,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
final User user;
|
|
37
|
+
final VoidCallback? onTap;
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
Widget build(BuildContext context) {
|
|
41
|
+
return Card(
|
|
42
|
+
child: ListTile(
|
|
43
|
+
title: Text(user.name),
|
|
44
|
+
subtitle: Text(user.email),
|
|
45
|
+
onTap: onTap,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### StatefulWidget
|
|
53
|
+
```dart
|
|
54
|
+
class Counter extends StatefulWidget {
|
|
55
|
+
const Counter({super.key});
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
State<Counter> createState() => _CounterState();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class _CounterState extends State<Counter> {
|
|
62
|
+
int _count = 0;
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
void dispose() {
|
|
66
|
+
// Clean up resources
|
|
67
|
+
super.dispose();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
Widget build(BuildContext context) {
|
|
72
|
+
return Column(
|
|
73
|
+
children: [
|
|
74
|
+
Text('Count: $_count'),
|
|
75
|
+
ElevatedButton(
|
|
76
|
+
onPressed: () => setState(() => _count++),
|
|
77
|
+
child: const Text('Increment'),
|
|
78
|
+
),
|
|
79
|
+
],
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## State Management
|
|
86
|
+
|
|
87
|
+
### Riverpod (Recommended)
|
|
88
|
+
```dart
|
|
89
|
+
final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<User>>((ref) {
|
|
90
|
+
return UserNotifier();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
class UserNotifier extends StateNotifier<AsyncValue<User>> {
|
|
94
|
+
UserNotifier() : super(const AsyncValue.loading());
|
|
95
|
+
|
|
96
|
+
Future<void> fetchUser(String id) async {
|
|
97
|
+
state = const AsyncValue.loading();
|
|
98
|
+
state = await AsyncValue.guard(() => api.getUser(id));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Usage in widget
|
|
103
|
+
class UserProfile extends ConsumerWidget {
|
|
104
|
+
@override
|
|
105
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
106
|
+
final userState = ref.watch(userProvider);
|
|
107
|
+
|
|
108
|
+
return userState.when(
|
|
109
|
+
data: (user) => Text(user.name),
|
|
110
|
+
loading: () => CircularProgressIndicator(),
|
|
111
|
+
error: (err, stack) => Text('Error: $err'),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Bloc Pattern
|
|
118
|
+
```dart
|
|
119
|
+
class UserBloc extends Bloc<UserEvent, UserState> {
|
|
120
|
+
UserBloc() : super(UserInitial()) {
|
|
121
|
+
on<LoadUser>(_onLoadUser);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Future<void> _onLoadUser(LoadUser event, Emitter<UserState> emit) async {
|
|
125
|
+
emit(UserLoading());
|
|
126
|
+
try {
|
|
127
|
+
final user = await repository.getUser(event.id);
|
|
128
|
+
emit(UserLoaded(user));
|
|
129
|
+
} catch (e) {
|
|
130
|
+
emit(UserError(e.toString()));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Navigation
|
|
137
|
+
|
|
138
|
+
### Named Routes
|
|
139
|
+
```dart
|
|
140
|
+
// Define routes
|
|
141
|
+
final routes = {
|
|
142
|
+
'/': (context) => HomeScreen(),
|
|
143
|
+
'/profile': (context) => ProfileScreen(),
|
|
144
|
+
'/settings': (context) => SettingsScreen(),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Navigate
|
|
148
|
+
Navigator.pushNamed(context, '/profile');
|
|
149
|
+
|
|
150
|
+
// With arguments
|
|
151
|
+
Navigator.pushNamed(
|
|
152
|
+
context,
|
|
153
|
+
'/profile',
|
|
154
|
+
arguments: {'userId': '123'},
|
|
155
|
+
);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Go Router (Modern)
|
|
159
|
+
```dart
|
|
160
|
+
final router = GoRouter(
|
|
161
|
+
routes: [
|
|
162
|
+
GoRoute(
|
|
163
|
+
path: '/',
|
|
164
|
+
builder: (context, state) => HomeScreen(),
|
|
165
|
+
),
|
|
166
|
+
GoRoute(
|
|
167
|
+
path: '/profile/:id',
|
|
168
|
+
builder: (context, state) {
|
|
169
|
+
final id = state.pathParameters['id']!;
|
|
170
|
+
return ProfileScreen(userId: id);
|
|
171
|
+
},
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Navigate
|
|
177
|
+
context.go('/profile/123');
|
|
178
|
+
context.push('/settings');
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Performance Optimization
|
|
182
|
+
|
|
183
|
+
### const Constructors
|
|
184
|
+
```dart
|
|
185
|
+
// Use const wherever possible
|
|
186
|
+
const Text('Hello');
|
|
187
|
+
const Padding(padding: EdgeInsets.all(8.0));
|
|
188
|
+
const SizedBox(height: 16);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### ListView.builder
|
|
192
|
+
```dart
|
|
193
|
+
// For large lists, use builder
|
|
194
|
+
ListView.builder(
|
|
195
|
+
itemCount: items.length,
|
|
196
|
+
itemBuilder: (context, index) {
|
|
197
|
+
return ListTile(title: Text(items[index]));
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Keys
|
|
203
|
+
```dart
|
|
204
|
+
// Use keys for widgets that need to preserve state
|
|
205
|
+
ListView(
|
|
206
|
+
children: items.map((item) =>
|
|
207
|
+
ListTile(
|
|
208
|
+
key: ValueKey(item.id),
|
|
209
|
+
title: Text(item.name),
|
|
210
|
+
)
|
|
211
|
+
).toList(),
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Responsive Design
|
|
216
|
+
|
|
217
|
+
### MediaQuery
|
|
218
|
+
```dart
|
|
219
|
+
final size = MediaQuery.of(context).size;
|
|
220
|
+
final isPortrait = size.height > size.width;
|
|
221
|
+
|
|
222
|
+
if (size.width > 600) {
|
|
223
|
+
// Tablet layout
|
|
224
|
+
} else {
|
|
225
|
+
// Mobile layout
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### LayoutBuilder
|
|
230
|
+
```dart
|
|
231
|
+
LayoutBuilder(
|
|
232
|
+
builder: (context, constraints) {
|
|
233
|
+
if (constraints.maxWidth > 600) {
|
|
234
|
+
return WideLayout();
|
|
235
|
+
}
|
|
236
|
+
return NarrowLayout();
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Testing
|
|
242
|
+
|
|
243
|
+
### Widget Tests
|
|
244
|
+
```dart
|
|
245
|
+
testWidgets('Counter increments', (tester) async {
|
|
246
|
+
await tester.pumpWidget(
|
|
247
|
+
MaterialApp(home: Counter()),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(find.text('0'), findsOneWidget);
|
|
251
|
+
|
|
252
|
+
await tester.tap(find.byType(ElevatedButton));
|
|
253
|
+
await tester.pump();
|
|
254
|
+
|
|
255
|
+
expect(find.text('1'), findsOneWidget);
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Integration Tests
|
|
260
|
+
```dart
|
|
261
|
+
void main() {
|
|
262
|
+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
263
|
+
|
|
264
|
+
testWidgets('Complete user flow', (tester) async {
|
|
265
|
+
await tester.pumpWidget(MyApp());
|
|
266
|
+
|
|
267
|
+
// Login
|
|
268
|
+
await tester.enterText(find.byKey(Key('email')), 'test@test.com');
|
|
269
|
+
await tester.tap(find.byKey(Key('login')));
|
|
270
|
+
await tester.pumpAndSettle();
|
|
271
|
+
|
|
272
|
+
// Verify home screen
|
|
273
|
+
expect(find.text('Welcome'), findsOneWidget);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Best Practices
|
|
279
|
+
|
|
280
|
+
### Widget Composition
|
|
281
|
+
- Break large widgets into smaller, reusable components
|
|
282
|
+
- Extract complex logic into separate methods or classes
|
|
283
|
+
- Use const constructors to improve performance
|
|
284
|
+
|
|
285
|
+
### Error Handling
|
|
286
|
+
- Use ErrorWidget.builder for custom error widgets
|
|
287
|
+
- Wrap risky operations in try-catch
|
|
288
|
+
- Provide meaningful error messages to users
|
|
289
|
+
|
|
290
|
+
### Asset Management
|
|
291
|
+
```yaml
|
|
292
|
+
# pubspec.yaml
|
|
293
|
+
flutter:
|
|
294
|
+
assets:
|
|
295
|
+
- assets/images/
|
|
296
|
+
- assets/icons/
|
|
297
|
+
fonts:
|
|
298
|
+
- family: Roboto
|
|
299
|
+
fonts:
|
|
300
|
+
- asset: fonts/Roboto-Regular.ttf
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Platform-Specific Code
|
|
304
|
+
```dart
|
|
305
|
+
import 'dart:io' show Platform;
|
|
306
|
+
|
|
307
|
+
if (Platform.isAndroid) {
|
|
308
|
+
// Android-specific
|
|
309
|
+
} else if (Platform.isIOS) {
|
|
310
|
+
// iOS-specific
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Common Patterns
|
|
315
|
+
|
|
316
|
+
### Dependency Injection
|
|
317
|
+
```dart
|
|
318
|
+
class MyApp extends StatelessWidget {
|
|
319
|
+
final ApiService apiService;
|
|
320
|
+
final AuthService authService;
|
|
321
|
+
|
|
322
|
+
const MyApp({
|
|
323
|
+
required this.apiService,
|
|
324
|
+
required this.authService,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Service Locator
|
|
330
|
+
```dart
|
|
331
|
+
final getIt = GetIt.instance;
|
|
332
|
+
|
|
333
|
+
void setupLocator() {
|
|
334
|
+
getIt.registerSingleton<ApiService>(ApiService());
|
|
335
|
+
getIt.registerLazySingleton<AuthService>(() => AuthService());
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Usage
|
|
339
|
+
final api = getIt<ApiService>();
|
|
340
|
+
```
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Dart Development Guidelines
|
|
2
|
+
|
|
3
|
+
## Language Overview
|
|
4
|
+
Dart is a client-optimized language for fast apps on any platform. It's the programming language behind Flutter and supports strong typing with null safety.
|
|
5
|
+
|
|
6
|
+
## Code Style & Conventions
|
|
7
|
+
|
|
8
|
+
### Naming
|
|
9
|
+
- **Classes/Enums/Typedefs**: PascalCase (`UserModel`, `ColorState`)
|
|
10
|
+
- **Functions/Variables**: camelCase (`getUserData`, `userName`)
|
|
11
|
+
- **Constants**: lowerCamelCase (`defaultTimeout`, `maxRetries`)
|
|
12
|
+
- **Private members**: Prefix with underscore (`_privateMethod`, `_internalState`)
|
|
13
|
+
|
|
14
|
+
### Null Safety
|
|
15
|
+
```dart
|
|
16
|
+
// Non-nullable by default
|
|
17
|
+
String name; // Must be initialized
|
|
18
|
+
String? optionalName; // Can be null
|
|
19
|
+
|
|
20
|
+
// Null-aware operators
|
|
21
|
+
String? maybeValue;
|
|
22
|
+
final length = maybeValue?.length ?? 0; // Default value
|
|
23
|
+
final value = maybeValue!; // Null assertion (use carefully)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Collections
|
|
27
|
+
```dart
|
|
28
|
+
// Use const for immutable collections
|
|
29
|
+
const colors = ['red', 'green', 'blue'];
|
|
30
|
+
|
|
31
|
+
// Type-safe collections
|
|
32
|
+
final List<User> users = [];
|
|
33
|
+
final Map<String, int> scores = {};
|
|
34
|
+
final Set<String> uniqueNames = {};
|
|
35
|
+
|
|
36
|
+
// Collection operators
|
|
37
|
+
final adults = users.where((u) => u.age >= 18).toList();
|
|
38
|
+
final names = users.map((u) => u.name).toList();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Object-Oriented Programming
|
|
42
|
+
|
|
43
|
+
### Classes
|
|
44
|
+
```dart
|
|
45
|
+
class User {
|
|
46
|
+
final String id;
|
|
47
|
+
final String name;
|
|
48
|
+
final int age;
|
|
49
|
+
|
|
50
|
+
// Constructor with named parameters
|
|
51
|
+
const User({
|
|
52
|
+
required this.id,
|
|
53
|
+
required this.name,
|
|
54
|
+
required this.age,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Named constructor
|
|
58
|
+
User.guest() : this(id: 'guest', name: 'Guest', age: 0);
|
|
59
|
+
|
|
60
|
+
// Factory constructor
|
|
61
|
+
factory User.fromJson(Map<String, dynamic> json) {
|
|
62
|
+
return User(
|
|
63
|
+
id: json['id'],
|
|
64
|
+
name: json['name'],
|
|
65
|
+
age: json['age'],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Copyhold
|
|
70
|
+
User copyWith({String? name, int? age}) {
|
|
71
|
+
return User(
|
|
72
|
+
id: id,
|
|
73
|
+
name: name ?? this.name,
|
|
74
|
+
age: age ?? this.age,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Mixins
|
|
81
|
+
```dart
|
|
82
|
+
mixin LoggerMixin {
|
|
83
|
+
void log(String message) {
|
|
84
|
+
print('[${DateTime.now()}] $message');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class MyService with LoggerMixin {
|
|
89
|
+
void doSomething() {
|
|
90
|
+
log('Doing something');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Async Programming
|
|
96
|
+
|
|
97
|
+
### Futures
|
|
98
|
+
```dart
|
|
99
|
+
Future<User> fetchUser(String id) async {
|
|
100
|
+
final response = await http.get(Uri.parse('/users/$id'));
|
|
101
|
+
return User.fromJson(json.decode(response.body));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Error handling
|
|
105
|
+
try {
|
|
106
|
+
final user = await fetchUser('123');
|
|
107
|
+
} catch (e) {
|
|
108
|
+
print('Error: $e');
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Streams
|
|
113
|
+
```dart
|
|
114
|
+
Stream<int> countStream(int max) async* {
|
|
115
|
+
for (int i = 1; i <= max; i++) {
|
|
116
|
+
await Future.delayed(Duration(seconds: 1));
|
|
117
|
+
yield i;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Usage
|
|
122
|
+
await for (final count in countStream(5)) {
|
|
123
|
+
print(count);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Testing
|
|
128
|
+
|
|
129
|
+
### Unit Tests
|
|
130
|
+
```dart
|
|
131
|
+
import 'package:test/test.dart';
|
|
132
|
+
|
|
133
|
+
void main() {
|
|
134
|
+
group('User', () {
|
|
135
|
+
test('creates from JSON', () {
|
|
136
|
+
final json = {'id': '1', 'name': 'John', 'age': 30};
|
|
137
|
+
final user = User.fromJson(json);
|
|
138
|
+
|
|
139
|
+
expect(user.id, '1');
|
|
140
|
+
expect(user.name, 'John');
|
|
141
|
+
expect(user.age, 30);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('copyWith preserves unchanged fields', () {
|
|
145
|
+
final user = User(id: '1', name: 'John', age: 30);
|
|
146
|
+
final updated = user.copyWith(name: 'Jane');
|
|
147
|
+
|
|
148
|
+
expect(updated.id, user.id);
|
|
149
|
+
expect(updated.name, 'Jane');
|
|
150
|
+
expect(updated.age, user.age);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Best Practices
|
|
157
|
+
|
|
158
|
+
### Performance
|
|
159
|
+
- Use `const` constructors when possible
|
|
160
|
+
- Prefer `final` over `var` for immutability
|
|
161
|
+
- Use collection literals instead of constructors
|
|
162
|
+
- Avoid unnecessary rebuilds
|
|
163
|
+
|
|
164
|
+
### Code Organization
|
|
165
|
+
- One class per file (generally)
|
|
166
|
+
- Group related files in directories
|
|
167
|
+
- Use barrel files (index.dart) for clean exports
|
|
168
|
+
- Separate models, services, and utilities
|
|
169
|
+
|
|
170
|
+
### Error Handling
|
|
171
|
+
- Use exceptions for exceptional cases
|
|
172
|
+
- Return `Result<T, E>` types for expected failures
|
|
173
|
+
- Validate input early
|
|
174
|
+
- Provide meaningful error messages
|
|
175
|
+
|
|
176
|
+
## Common Patterns
|
|
177
|
+
|
|
178
|
+
### Singleton
|
|
179
|
+
```dart
|
|
180
|
+
class ApiService {
|
|
181
|
+
static final ApiService _instance = ApiService._internal();
|
|
182
|
+
factory ApiService() => _instance;
|
|
183
|
+
ApiService._internal();
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Factory Pattern
|
|
188
|
+
```dart
|
|
189
|
+
abstract class Shape {
|
|
190
|
+
factory Shape(String type) {
|
|
191
|
+
switch (type) {
|
|
192
|
+
case 'circle':
|
|
193
|
+
return Circle();
|
|
194
|
+
case 'square':
|
|
195
|
+
return Square();
|
|
196
|
+
default:
|
|
197
|
+
throw ArgumentError('Unknown shape: $type');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Repository Pattern
|
|
204
|
+
```dart
|
|
205
|
+
abstract class UserRepository {
|
|
206
|
+
Future<User> getUser(String id);
|
|
207
|
+
Future<void> saveUser(User user);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class ApiUserRepository implements UserRepository {
|
|
211
|
+
@override
|
|
212
|
+
Future<User> getUser(String id) async {
|
|
213
|
+
// Implementation
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Tools & Commands
|
|
219
|
+
```bash
|
|
220
|
+
# Format code
|
|
221
|
+
dart format .
|
|
222
|
+
|
|
223
|
+
# Analyze code
|
|
224
|
+
dart analyze
|
|
225
|
+
|
|
226
|
+
# Run tests
|
|
227
|
+
dart test
|
|
228
|
+
|
|
229
|
+
# Pub commands
|
|
230
|
+
dart pub get
|
|
231
|
+
dart pub upgrade
|
|
232
|
+
dart pub outdated
|
|
233
|
+
```
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Elixir Development Guidelines
|
|
2
|
+
|
|
3
|
+
## Language Overview
|
|
4
|
+
Elixir is a functional, concurrent programming language built on the Erlang VM (BEAM). It's designed for building scalable, maintainable applications with excellent support for distributed systems.
|
|
5
|
+
|
|
6
|
+
## Code Style & Conventions
|
|
7
|
+
|
|
8
|
+
### Naming
|
|
9
|
+
- **Modules**: PascalCase (`MyApp.UserController`)
|
|
10
|
+
- **Functions/Variables**: snake_case (`create_user`, `user_id`)
|
|
11
|
+
- **Atoms**: snake_case (`:ok`, `:error`, `:user_not_found`)
|
|
12
|
+
- **Constants**: snake_case module attributes (`@default_timeout`)
|
|
13
|
+
|
|
14
|
+
### Pattern Matching
|
|
15
|
+
```elixir
|
|
16
|
+
# Use pattern matching extensively
|
|
17
|
+
def process({:ok, result}), do: result
|
|
18
|
+
def process({:error, reason}), do: Logger.error(reason)
|
|
19
|
+
|
|
20
|
+
# Guards for additional constraints
|
|
21
|
+
def divide(a, b) when b != 0, do: {:ok, a / b}
|
|
22
|
+
def divide(_, 0), do: {:error, :division_by_zero}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Pipe Operator
|
|
26
|
+
```elixir
|
|
27
|
+
# Chain operations with |>
|
|
28
|
+
user
|
|
29
|
+
|> validate_user()
|
|
30
|
+
|> create_record()
|
|
31
|
+
|> send_welcome_email()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## OTP Patterns
|
|
35
|
+
|
|
36
|
+
### GenServer
|
|
37
|
+
```elixir
|
|
38
|
+
defmodule MyApp.Worker do
|
|
39
|
+
use GenServer
|
|
40
|
+
|
|
41
|
+
def start_link(opts) do
|
|
42
|
+
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def init(state) do
|
|
46
|
+
{:ok, state}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def handle_call({:get, key}, _from, state) do
|
|
50
|
+
{:reply, Map.get(state, key), state}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Supervision
|
|
56
|
+
```elixir
|
|
57
|
+
children = [
|
|
58
|
+
{MyApp.Repo, []},
|
|
59
|
+
{MyApp.Worker, []},
|
|
60
|
+
{Phoenix.PubSub, name: MyApp.PubSub}
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
Supervisor.start_link(children, strategy: :one_for_one)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Phoenix Framework
|
|
67
|
+
|
|
68
|
+
### Controllers
|
|
69
|
+
```elixir
|
|
70
|
+
defmodule MyAppWeb.UserController do
|
|
71
|
+
use MyAppWeb, :controller
|
|
72
|
+
|
|
73
|
+
def create(conn, %{"user" => user_params}) do
|
|
74
|
+
case Accounts.create_user(user_params) do
|
|
75
|
+
{:ok, user} ->
|
|
76
|
+
conn
|
|
77
|
+
|> put_status(:created)
|
|
78
|
+
|> render("show.json", user: user)
|
|
79
|
+
|
|
80
|
+
{:error, changeset} ->
|
|
81
|
+
conn
|
|
82
|
+
|> put_status(:unprocessable_entity)
|
|
83
|
+
|> render("error.json", changeset: changeset)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Contexts
|
|
90
|
+
- Group related functionality in contexts
|
|
91
|
+
- Keep controllers thin, business logic in contexts
|
|
92
|
+
- Use Ecto changesets for validation
|
|
93
|
+
|
|
94
|
+
## Ecto Database
|
|
95
|
+
|
|
96
|
+
### Schemas
|
|
97
|
+
```elixir
|
|
98
|
+
defmodule MyApp.User do
|
|
99
|
+
use Ecto.Schema
|
|
100
|
+
import Ecto.Changeset
|
|
101
|
+
|
|
102
|
+
schema "users" do
|
|
103
|
+
field :email, :string
|
|
104
|
+
field :name, :string
|
|
105
|
+
has_many :posts, MyApp.Post
|
|
106
|
+
|
|
107
|
+
timestamps()
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def changeset(user, attrs) do
|
|
111
|
+
user
|
|
112
|
+
|> cast(attrs, [:email, :name])
|
|
113
|
+
|> validate_required([:email])
|
|
114
|
+
|> validate_format(:email, ~r/@/)
|
|
115
|
+
|> unique_constraint(:email)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Testing
|
|
121
|
+
|
|
122
|
+
### ExUnit
|
|
123
|
+
```elixir
|
|
124
|
+
defmodule MyApp.UserTest do
|
|
125
|
+
use MyApp.DataCase
|
|
126
|
+
|
|
127
|
+
test "creates user with valid attrs" do
|
|
128
|
+
attrs = %{email: "test@example.com", name: "Test"}
|
|
129
|
+
assert {:ok, user} = Accounts.create_user(attrs)
|
|
130
|
+
assert user.email == "test@example.com"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Error Handling
|
|
136
|
+
- Use tagged tuples: `{:ok, result}`, `{:error, reason}`
|
|
137
|
+
- Pattern match on results
|
|
138
|
+
- Use `with` for complex error handling
|
|
139
|
+
- Leverage supervisor restart strategies
|
|
140
|
+
|
|
141
|
+
## Best Practices
|
|
142
|
+
- Keep functions pure when possible
|
|
143
|
+
- Use immutable data structures
|
|
144
|
+
- Leverage concurrency with Task and GenServer
|
|
145
|
+
- Follow "let it crash" philosophy
|
|
146
|
+
- Use telemetry for observability
|
|
147
|
+
- Run `mix format` before committing
|