| name | flutter-helper |
| description | Aide au développement Flutter/Dart avec bonnes pratiques, patterns, snippets copiables et diagnostics d'erreurs. Se déclenche avec "Flutter", "Dart", "Widget", "BLoC", "Riverpod", "Provider", "pub.dev", "MaterialApp", "flutter build". Also triggers on "Flutter app", "Dart widget", "Flutter state management". |
Flutter Helper
1. Diagnostic initial
Avant tout, collecter :
flutter --version → version SDK (cible : Flutter 3.22+, Dart 3.4+)
flutter doctor -v → chaîne outils (Xcode, Android SDK, etc.)
- Message d'erreur complet (stack trace si disponible)
- Plateforme cible (Android / iOS / Web / Desktop)
2. Structure projet
Feature-first (projets moyens/grands) :
lib/
features/
auth/
data/ # repositories, data sources, models
domain/ # entities, use cases
presentation/ # pages, widgets, controllers
core/ # DI, router, theme, utils
main.dart
Layer-first (petits projets ou prototypes) :
lib/
data/ domain/ presentation/
main.dart
Critère de choix : si > 3 features ou > 2 devs → feature-first obligatoire.
3. State management — décision
| Taille projet | Solution recommandée | Quand éviter |
|---|
| Prototype / très petit | Provider | > 3 features |
| Moyen | Riverpod 2.x | jamais — convient toujours |
| Grande logique métier | BLoC/Cubit | sur-architecture sur petits cas |
| Full-stack léger | GetX | maintainability sacrifiée |
Riverpod — pattern minimal fonctionnel :
// counter_provider.dart
final counterProvider = StateNotifierProvider<CounterNotifier, int>(
(ref) => CounterNotifier(),
);
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
// widget
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
Cubit — pattern minimal :
class AuthCubit extends Cubit<AuthState> {
AuthCubit(this._repo) : super(AuthInitial());
final AuthRepository _repo;
Future<void> login(String email, String password) async {
emit(AuthLoading());
try {
final user = await _repo.login(email, password);
emit(AuthSuccess(user));
} catch (e) {
emit(AuthError(e.toString()));
}
}
}
4. Widgets — règles de performance
// ✅ Bon : const constructor évite le rebuild
const MyCard({super.key, required this.title});
// ✅ Isoler le Consumer au plus proche de la donnée
Consumer<CartModel>(
builder: (_, cart, __) => Text('${cart.itemCount}'),
);
// ❌ Mauvais : Consumer sur tout l'arbre
Consumer<CartModel>(
builder: (_, cart, __) => Scaffold(...), // rebuild = toute la page
);
// ✅ ListView.builder pour listes longues
ListView.builder(
itemCount: items.length,
itemBuilder: (_, i) => ItemTile(item: items[i]),
);
Règle : jamais de logique métier dans build() — uniquement présentation.
5. Navigation — GoRouter (recommandé 2025+)
dependencies:
go_router: ^14.0.0
final router = GoRouter(
initialLocation: '/home',
redirect: (context, state) {
final loggedIn = ref.read(authProvider).isLoggedIn;
if (!loggedIn && state.matchedLocation != '/login') return '/login';
return null;
},
routes: [
GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
GoRoute(
path: '/home',
builder: (_, __) => const HomePage(),
routes: [
GoRoute(path: 'detail/:id', builder: (_, s) => DetailPage(id: s.pathParameters['id']!)),
],
),
],
);
// Navigation
context.go('/home/detail/42');
context.push('/home/detail/42'); // ajoute au back stack
6. Networking + modèles
dependencies:
dio: ^5.4.0
retrofit: ^4.1.0
freezed_annotation: ^2.4.1
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
freezed: ^2.4.7
json_serializable: ^6.7.1
retrofit_generator: ^8.1.0
// model généré via freezed
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
String? avatar,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// génération : flutter pub run build_runner build --delete-conflicting-outputs
Intercepteur Dio pour token :
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
));
7. Testing
// Widget test
testWidgets('affiche le titre', (tester) async {
await tester.pumpWidget(const MaterialApp(home: MyWidget(title: 'Test')));
expect(find.text('Test'), findsOneWidget);
});
// BLoC/Cubit test
blocTest<AuthCubit, AuthState>(
'émet AuthSuccess après login réussi',
build: () => AuthCubit(mockRepo),
act: (c) => c.login('u@test.com', 'pass'),
expect: () => [isA<AuthLoading>(), isA<AuthSuccess>()],
);
Commandes utiles :
flutter test
flutter test --coverage
genhtml coverage/lcov.info -o cov_html
flutter test integration_test/
8. Performance — checklist
// compute() = Isolate simplifié
final result = await compute(parseJsonHeavy, rawString);
9. Build & déploiement
flutter run --dart-define=ENV=staging
flutter build apk --dart-define=ENV=prod --release
flutter build apk --split-per-abi
flutter build ipa --release
flutter pub version
Flavors dans le code :
const env = String.fromEnvironment('ENV', defaultValue: 'dev');
10. Anti-patterns / pièges à éviter
| Piège | Solution |
|---|
setState dans un StatelessWidget | Passer à StatefulWidget ou state manager |
BuildContext utilisé après await | Vérifier mounted avant toute opération post-await |
Appels API dans build() | Déplacer dans initState ou un provider |
Image.network() sans cache | Utiliser CachedNetworkImage |
| Packages abandonnés (vérifier pub.dev) | Filtrer par "Likes > 100" et "Maintained" |
print() en production | Remplacer par debugPrint() ou logger package |
Nested Scaffold | Un seul Scaffold par route |
// ✅ Vérification mounted après async
Future<void> fetchData() async {
final data = await api.get();
if (!mounted) return; // évite memory leak / crash
setState(() => _data = data);
}