# Top 20 questions d'entretien Flutter pour développeurs mobiles > Préparez vos entretiens Flutter avec les 20 questions les plus posées. Widgets, state management, Dart, architecture et bonnes pratiques expliqués en détail. - Published: 2026-01-24 - Updated: 2026-03-28 - Author: SharpSkill - Tags: flutter, dart, interview, mobile development, entretien technique - Reading time: 15 min --- Les entretiens Flutter évaluent la maîtrise du framework, du langage Dart et des patterns d'architecture mobile. Ce guide couvre les 20 questions les plus fréquentes, des fondamentaux aux concepts avancés, avec des réponses détaillées et des exemples de code. > **Conseil d'entretien** > > Les recruteurs apprécient les candidats qui expliquent le "pourquoi" en plus du "comment". Pour chaque concept, la compréhension des cas d'usage et des compromis techniques fait la différence. ## Questions fondamentales sur Flutter et Dart ### 1. Quelle est la différence entre StatelessWidget et StatefulWidget ? **StatelessWidget** représente un widget immuable dont l'apparence dépend uniquement de sa configuration initiale. Une fois construit, il ne change jamais. **StatefulWidget** maintient un état mutable qui peut évoluer au fil du temps, déclenchant des reconstructions du widget. ```dart // stateless_example.dart // StatelessWidget : affichage statique, pas de changement après construction class WelcomeMessage extends StatelessWidget { // Paramètre final - ne change jamais final String username; const WelcomeMessage({super.key, required this.username}); @override Widget build(BuildContext context) { // Build appelé une seule fois (sauf si parent reconstruit) return Text('Bienvenue, $username'); } } ``` ```dart // stateful_example.dart // StatefulWidget : état mutable, peut se reconstruire class LikeButton extends StatefulWidget { const LikeButton({super.key}); @override State createState() => _LikeButtonState(); } class _LikeButtonState extends State { // État local mutable int _likeCount = 0; void _incrementLike() { // setState déclenche rebuild avec nouvel état setState(() { _likeCount++; }); } @override Widget build(BuildContext context) { return ElevatedButton( onPressed: _incrementLike, child: Text('Likes: $_likeCount'), ); } } ``` La règle : utiliser StatelessWidget par défaut, StatefulWidget uniquement pour l'état local d'un widget. ### 2. Comment fonctionne le widget tree de Flutter ? Flutter organise l'interface en trois arbres interconnectés : le **Widget Tree** (déclaration immuable), l'**Element Tree** (cycle de vie et liaison), et le **Render Tree** (layout et peinture). ```dart // widget_tree_example.dart // Structure déclarative - les widgets décrivent l'UI class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // Chaque widget crée un Element dans l'Element Tree return MaterialApp( home: Scaffold( // Le Scaffold crée plusieurs RenderObjects body: Center( // Center modifie le layout de son enfant child: Column( children: [ // Chaque Text a son propre RenderParagraph Text('Premier'), Text('Deuxième'), ], ), ), ), ); } } ``` Quand `setState` est appelé, Flutter compare l'ancien et le nouveau widget tree pour ne reconstruire que les éléments modifiés. Cette différentiation s'appuie sur les clés et les types de widgets. ### 3. Qu'est-ce que la const constructor et pourquoi l'utiliser ? Les **const constructors** créent des widgets à la compilation plutôt qu'à l'exécution. Flutter peut réutiliser ces instances, améliorant les performances en évitant les reconstructions inutiles. ```dart // const_example.dart class OptimizedScreen extends StatelessWidget { const OptimizedScreen({super.key}); @override Widget build(BuildContext context) { return Column( children: [ // ✅ const : même instance réutilisée à chaque build const Icon(Icons.star, size: 48), const SizedBox(height: 16), const Text('Titre statique'), // ❌ Non-const : nouvelle instance à chaque build Icon(Icons.star, color: Theme.of(context).primaryColor), ], ); } } // Widget personnalisé avec const constructor class StaticCard extends StatelessWidget { final String title; // Tous les champs doivent être final pour const const StaticCard({super.key, required this.title}); @override Widget build(BuildContext context) { return Card(child: Text(title)); } } ``` L'analyseur Flutter signale les opportunités manquées avec le lint `prefer_const_constructors`. ### 4. Expliquez les différents types de Keys dans Flutter Les **Keys** préservent l'état des widgets lors des réorganisations. Sans clé, Flutter se base sur la position dans l'arbre, ce qui peut causer des bugs lors de réordonnancement de listes. ```dart // keys_example.dart class TodoList extends StatelessWidget { final List todos; const TodoList({super.key, required this.todos}); @override Widget build(BuildContext context) { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; // ✅ ValueKey : préserve l'état si l'ordre change return TodoTile( key: ValueKey(todo.id), todo: todo, ); }, ); } } // Différents types de clés selon le contexte class KeyExamples extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ // ValueKey : basée sur une valeur unique Container(key: ValueKey('unique-id')), // ObjectKey : basée sur l'identité de l'objet Container(key: ObjectKey(myObject)), // UniqueKey : nouvelle clé à chaque build Container(key: UniqueKey()), // GlobalKey : accès à l'état depuis l'extérieur Form(key: _formKey), ], ); } } ``` Les GlobalKeys permettent également d'accéder à l'état d'un widget depuis n'importe où, mais doivent être utilisées avec parcimonie. ## Questions sur Dart ### 5. Quelle est la différence entre final et const en Dart ? **final** définit une variable assignable une seule fois, évaluée à l'exécution. **const** crée une constante de compilation, immuable et évaluée avant l'exécution. ```dart // final_const_example.dart class DateExample { // final : valeur assignée à l'exécution final DateTime createdAt = DateTime.now(); // const : valeur connue à la compilation static const int maxItems = 100; static const String appName = 'MyApp'; // ❌ Erreur : DateTime.now() pas const (valeur runtime) // static const DateTime timestamp = DateTime.now(); } void demonstrateDifference() { // final : chaque appel peut avoir valeur différente final timestamp1 = DateTime.now(); final timestamp2 = DateTime.now(); print(timestamp1 == timestamp2); // false (différentes) // const : même instance réutilisée const list1 = [1, 2, 3]; const list2 = [1, 2, 3]; print(identical(list1, list2)); // true (même instance) } ``` En Flutter, préférer const pour les widgets statiques et final pour les valeurs calculées. ### 6. Comment fonctionnent les Futures et async/await ? **Future** représente une valeur qui sera disponible plus tard. **async/await** offre une syntaxe lisible pour gérer ces opérations asynchrones sans callbacks imbriqués. ```dart // async_example.dart class UserRepository { final ApiClient _client; UserRepository(this._client); // Future : promesse d'une valeur future Future fetchUser(String id) async { try { // await suspend l'exécution jusqu'à résolution final response = await _client.get('/users/$id'); return User.fromJson(response); } catch (e) { // Erreurs propagées normalement avec async/await throw UserNotFoundException(id); } } // Exécution parallèle avec Future.wait Future> fetchUsers(List ids) async { // Toutes les requêtes démarrent simultanément final futures = ids.map((id) => fetchUser(id)); // Attend que toutes soient terminées return Future.wait(futures); } // Traitement séquentiel Future processSequentially(List ids) async { for (final id in ids) { // Chaque requête attend la précédente await fetchUser(id); } } } ``` `Future.wait` pour le parallélisme, boucles async pour le séquentiel. > **FutureBuilder vs Riverpod** > > FutureBuilder convient aux cas simples, mais Riverpod (AsyncValue) offre une meilleure gestion du cache, des erreurs et des rafraîchissements pour les applications complexes. ### 7. Expliquez les Streams et leur utilisation Les **Streams** représentent des séquences de valeurs asynchrones, idéales pour les événements en temps réel : WebSockets, données de capteurs, ou interactions utilisateur. ```dart // stream_example.dart class MessageService { // StreamController gère la création et diffusion final _messageController = StreamController.broadcast(); // Exposer uniquement le Stream (pas le Sink) Stream get messages => _messageController.stream; void addMessage(Message message) { _messageController.sink.add(message); } void dispose() { _messageController.close(); } } // Utilisation avec StreamBuilder class MessageList extends StatelessWidget { final MessageService service; const MessageList({super.key, required this.service}); @override Widget build(BuildContext context) { return StreamBuilder( stream: service.messages, builder: (context, snapshot) { // Gestion de tous les états possibles if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } if (snapshot.hasError) { return Text('Erreur: ${snapshot.error}'); } if (!snapshot.hasData) { return const Text('Aucun message'); } return MessageCard(message: snapshot.data!); }, ); } } ``` Les Streams `.broadcast()` permettent plusieurs listeners, contrairement aux Streams simples. ## Questions sur l'architecture et le state management ### 8. Comparez les solutions de state management : Provider, Riverpod, Bloc Chaque solution répond à des besoins différents. **Provider** offre simplicité et intégration native. **Riverpod** apporte type-safety et testabilité. **Bloc** impose une architecture événementielle stricte. ```dart // riverpod_example.dart // Riverpod : approche déclarative et type-safe final userProvider = FutureProvider.autoDispose((ref) async { final repository = ref.watch(userRepositoryProvider); return repository.fetchCurrentUser(); }); class UserProfile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // AsyncValue gère loading/error/data final userAsync = ref.watch(userProvider); return userAsync.when( loading: () => const CircularProgressIndicator(), error: (error, stack) => Text('Erreur: $error'), data: (user) => Text(user.displayName), ); } } ``` ```dart // bloc_example.dart // Bloc : séparation événements/états explicite abstract class AuthEvent {} class LoginRequested extends AuthEvent { final String email; final String password; LoginRequested(this.email, this.password); } abstract class AuthState {} class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} class AuthSuccess extends AuthState { final User user; AuthSuccess(this.user); } class AuthFailure extends AuthState { final String error; AuthFailure(this.error); } class AuthBloc extends Bloc { AuthBloc() : super(AuthInitial()) { on(_onLoginRequested); } Future _onLoginRequested( LoginRequested event, Emitter emit, ) async { emit(AuthLoading()); try { final user = await _repository.login(event.email, event.password); emit(AuthSuccess(user)); } catch (e) { emit(AuthFailure(e.toString())); } } } ``` Riverpod est recommandé pour les nouveaux projets grâce à son API moderne et sa facilité de test. ### 9. Qu'est-ce que l'architecture Clean Architecture en Flutter ? **Clean Architecture** sépare le code en couches indépendantes : Domain (logique métier), Data (sources de données), et Presentation (UI). Cette séparation facilite les tests et la maintenance. ```dart // domain/entities/user.dart // Entité : objet métier pur, sans dépendance framework class User { final String id; final String email; final String name; const User({ required this.id, required this.email, required this.name, }); } // domain/repositories/user_repository.dart // Interface : contrat abstrait, implémentation dans Data abstract class UserRepository { Future getUser(String id); Future updateUser(User user); } // domain/usecases/get_user_usecase.dart // Use case : logique métier isolée class GetUserUseCase { final UserRepository _repository; GetUserUseCase(this._repository); Future call(String userId) { return _repository.getUser(userId); } } ``` ```dart // data/repositories/user_repository_impl.dart // Implémentation concrète avec sources de données class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource _remoteDataSource; final UserLocalDataSource _localDataSource; UserRepositoryImpl(this._remoteDataSource, this._localDataSource); @override Future getUser(String id) async { try { // Tente d'abord le cache local final cachedUser = await _localDataSource.getUser(id); if (cachedUser != null) return cachedUser; } catch (_) {} // Fallback sur l'API final user = await _remoteDataSource.fetchUser(id); await _localDataSource.cacheUser(user); return user; } } ``` La couche Domain ne dépend de rien, Data dépend de Domain, Presentation dépend des deux. ### 10. Comment implémenter l'injection de dépendances ? L'**injection de dépendances** découple les composants en fournissant leurs dépendances de l'extérieur. Riverpod excelle dans ce rôle avec ses providers. ```dart // di_example.dart // Définition des providers (dépendances) final apiClientProvider = Provider((ref) { return ApiClient(baseUrl: Environment.apiUrl); }); final userRepositoryProvider = Provider((ref) { // Injecte automatiquement ApiClient final client = ref.watch(apiClientProvider); return UserRepositoryImpl(client); }); final getUserUseCaseProvider = Provider((ref) { final repository = ref.watch(userRepositoryProvider); return GetUserUseCase(repository); }); // Utilisation dans un widget class UserScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final getUserUseCase = ref.watch(getUserUseCaseProvider); // Utilisation du use case... } } // Tests : override facile des dépendances void main() { testWidgets('affiche utilisateur', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ // Remplace par un mock pour les tests userRepositoryProvider.overrideWithValue(MockUserRepository()), ], child: const MyApp(), ), ); }); } ``` L'avantage majeur : les tests peuvent remplacer n'importe quelle dépendance par un mock. ## Questions sur les performances ### 11. Comment optimiser les performances de rebuild ? Minimiser les rebuilds améliore les performances. Les techniques clés : widgets const, découpage granulaire, et sélecteurs Riverpod. ```dart // rebuild_optimization.dart // ❌ Mauvais : tout reconstruit à chaque changement class BadExample extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userProvider); return Column( children: [ Text(user.name), Text(user.email), const ExpensiveWidget(), // Reconstruit inutilement ], ); } } // ✅ Bon : sélecteur pour ne reconstruire que si name change class GoodExample extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // select : reconstruit uniquement si name change final name = ref.watch(userProvider.select((u) => u.name)); return Text(name); } } // ✅ Bon : découpage en widgets plus petits class OptimizedScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ const Header(), // Statique, jamais reconstruit const UserNameWidget(), // Reconstruit si name change const UserEmailWidget(), // Reconstruit si email change const Footer(), // Statique ], ); } } ``` Utiliser le DevTools Performance pour identifier les rebuilds excessifs. ### 12. Comment optimiser les listes longues ? Les listes longues nécessitent le **lazy loading** avec ListView.builder. Éviter ListView avec children directs pour plus de 20 éléments. ```dart // list_optimization.dart class OptimizedList extends StatelessWidget { final List items; const OptimizedList({super.key, required this.items}); @override Widget build(BuildContext context) { // ✅ ListView.builder : construit à la demande return ListView.builder( // Hauteur fixe améliore le scrolling itemExtent: 72, itemCount: items.length, itemBuilder: (context, index) { return ItemTile( // Key pour préserver l'état lors du scroll key: ValueKey(items[index].id), item: items[index], ); }, ); } } // Widget de liste avec chargement infini class InfiniteList extends ConsumerStatefulWidget { @override ConsumerState createState() => _InfiniteListState(); } class _InfiniteListState extends ConsumerState { final _scrollController = ScrollController(); @override void initState() { super.initState(); _scrollController.addListener(_onScroll); } void _onScroll() { // Charge plus de données proche de la fin if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { ref.read(itemsProvider.notifier).loadMore(); } } @override Widget build(BuildContext context) { final items = ref.watch(itemsProvider); return ListView.builder( controller: _scrollController, itemCount: items.length, itemBuilder: (context, index) => ItemTile(item: items[index]), ); } } ``` Pour les listes très longues (1000+ éléments), considérer `ListView.separated` ou des packages spécialisés comme `scrollable_positioned_list`. > **Image caching** > > Pour les listes avec images, utiliser `cached_network_image` pour éviter les rechargements. Les images non cachées causent des stutters au scroll. ### 13. Expliquez le fonctionnement du moteur de rendu Impeller **Impeller** remplace Skia comme moteur de rendu par défaut (Flutter 3.16+). Il précompile les shaders pour éliminer le "jank" du premier affichage. ```dart // impeller_benefits.dart // Les animations complexes bénéficient d'Impeller class SmoothAnimation extends StatefulWidget { @override State createState() => _SmoothAnimationState(); } class _SmoothAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { // Impeller : pas de compilation shader au runtime return Transform.rotate( angle: _controller.value * 2 * 3.14159, child: child, ); }, child: const FlutterLogo(size: 100), ); } } ``` Impeller est activé par défaut sur iOS. Sur Android, vérifier la compatibilité avec `flutter run --enable-impeller`. ## Questions sur la navigation et les formulaires ### 14. Comment gérer la navigation avec deep linking ? Le **deep linking** permet d'ouvrir l'app sur un écran spécifique via URL. GoRouter gère nativement cette fonctionnalité. ```dart // deep_linking.dart final router = GoRouter( routes: [ GoRoute( path: '/products/:productId', builder: (context, state) { // Extraction du paramètre d'URL final productId = state.pathParameters['productId']!; return ProductScreen(productId: productId); }, ), GoRoute( path: '/search', builder: (context, state) { // Query parameters (?query=flutter) final query = state.uri.queryParameters['query'] ?? ''; return SearchScreen(initialQuery: query); }, ), ], ); // Navigation programmatique void navigateToProduct(BuildContext context, String id) { // go : remplace la pile de navigation context.go('/products/$id'); // push : ajoute à la pile (permet back) context.push('/products/$id'); // pushNamed avec extra pour données complexes context.pushNamed( 'productDetail', pathParameters: {'productId': id}, extra: ProductData(id: id), ); } ``` Configurer les fichiers natifs (AndroidManifest.xml, Info.plist) pour activer le deep linking système. ### 15. Comment valider les formulaires efficacement ? La validation combine Form, TextFormField et validateurs personnalisés. Zod-like packages comme `formz` ajoutent de la structure. ```dart // form_validation.dart class RegistrationForm extends StatefulWidget { @override State createState() => _RegistrationFormState(); } class _RegistrationFormState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmController = TextEditingController(); @override Widget build(BuildContext context) { return Form( key: _formKey, // Validation en temps réel autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, validator: _validateEmail, ), TextFormField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Mot de passe'), obscureText: true, validator: _validatePassword, ), TextFormField( controller: _confirmController, decoration: const InputDecoration(labelText: 'Confirmer'), obscureText: true, validator: _validateConfirmPassword, ), ElevatedButton( onPressed: _submit, child: const Text('Créer compte'), ), ], ), ); } String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email requis'; } final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(value)) { return 'Format email invalide'; } return null; } String? _validatePassword(String? value) { if (value == null || value.length < 8) { return 'Minimum 8 caractères'; } if (!value.contains(RegExp(r'[A-Z]'))) { return 'Au moins une majuscule requise'; } if (!value.contains(RegExp(r'[0-9]'))) { return 'Au moins un chiffre requis'; } return null; } String? _validateConfirmPassword(String? value) { if (value != _passwordController.text) { return 'Les mots de passe ne correspondent pas'; } return null; } void _submit() { if (_formKey.currentState!.validate()) { // Formulaire valide, procéder } } } ``` Le mode `AutovalidateMode.onUserInteraction` offre le meilleur UX : erreurs affichées après interaction. ## Questions sur les tests ### 16. Comment tester les widgets Flutter ? Les tests de widgets vérifient l'UI sans dépendre du device. Le package `flutter_test` fournit les utilitaires nécessaires. ```dart // widget_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() { testWidgets('affiche message de bienvenue', (tester) async { // Arrange : construit le widget await tester.pumpWidget( const ProviderScope( child: MaterialApp( home: WelcomeScreen(username: 'Alice'), ), ), ); // Assert : vérifie le contenu expect(find.text('Bienvenue, Alice'), findsOneWidget); }); testWidgets('bouton incrémente compteur', (tester) async { await tester.pumpWidget( const MaterialApp(home: CounterScreen()), ); // État initial expect(find.text('0'), findsOneWidget); // Act : tape sur le bouton await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Reconstruit après setState // Assert : compteur incrémenté expect(find.text('1'), findsOneWidget); }); testWidgets('formulaire valide email', (tester) async { await tester.pumpWidget( const MaterialApp(home: LoginForm()), ); // Saisie email invalide await tester.enterText( find.byKey(const Key('email-field')), 'invalid-email', ); await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Vérifie message d'erreur expect(find.text('Format email invalide'), findsOneWidget); }); } ``` `pump()` avance d'un frame, `pumpAndSettle()` attend la fin de toutes les animations. ### 17. Comment tester les providers Riverpod ? Riverpod facilite les tests grâce à `ProviderContainer` et aux overrides. ```dart // provider_test.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; // Mock du repository class MockUserRepository extends Mock implements UserRepository {} void main() { late MockUserRepository mockRepository; late ProviderContainer container; setUp(() { mockRepository = MockUserRepository(); container = ProviderContainer( overrides: [ userRepositoryProvider.overrideWithValue(mockRepository), ], ); }); tearDown(() { container.dispose(); }); test('charge utilisateur depuis repository', () async { // Arrange final expectedUser = User(id: '1', name: 'Test'); when(() => mockRepository.getUser('1')) .thenAnswer((_) async => expectedUser); // Act final user = await container.read(userProvider('1').future); // Assert expect(user, expectedUser); verify(() => mockRepository.getUser('1')).called(1); }); test('gère erreur repository', () async { when(() => mockRepository.getUser(any())) .thenThrow(Exception('Erreur réseau')); expect( () => container.read(userProvider('1').future), throwsException, ); }); } ``` Les tests de providers sont rapides car ils ne nécessitent pas de rendu d'UI. ## Questions sur le déploiement ### 18. Comment gérer les différents environnements (dev, staging, prod) ? Les environnements se configurent via des fichiers `.env` ou des constantes compilées avec `--dart-define`. ```dart // environment.dart enum Environment { dev, staging, prod } class AppConfig { final Environment environment; final String apiUrl; final bool enableAnalytics; const AppConfig._({ required this.environment, required this.apiUrl, required this.enableAnalytics, }); // Configurations prédéfinies static const dev = AppConfig._( environment: Environment.dev, apiUrl: 'https://api-dev.example.com', enableAnalytics: false, ); static const staging = AppConfig._( environment: Environment.staging, apiUrl: 'https://api-staging.example.com', enableAnalytics: true, ); static const prod = AppConfig._( environment: Environment.prod, apiUrl: 'https://api.example.com', enableAnalytics: true, ); // Lecture depuis --dart-define static AppConfig fromEnvironment() { const env = String.fromEnvironment('ENV', defaultValue: 'dev'); switch (env) { case 'prod': return prod; case 'staging': return staging; default: return dev; } } } ``` ```bash # terminal # Lancement avec environnement spécifique flutter run --dart-define=ENV=staging # Build production flutter build apk --dart-define=ENV=prod --release ``` ### 19. Comment implémenter le flavoring pour plusieurs variantes d'app ? Le **flavoring** crée plusieurs variantes (client1, client2) avec configurations distinctes (icône, nom, API). ```dart // main_client1.dart import 'package:flutter/material.dart'; import 'config/flavor_config.dart'; import 'app.dart'; void main() { FlavorConfig( flavor: Flavor.client1, name: 'App Client 1', apiUrl: 'https://api.client1.com', primaryColor: Colors.blue, ); runApp(const App()); } // config/flavor_config.dart enum Flavor { client1, client2, internal } class FlavorConfig { final Flavor flavor; final String name; final String apiUrl; final Color primaryColor; // Singleton pour accès global static FlavorConfig? _instance; static FlavorConfig get instance => _instance!; FlavorConfig({ required this.flavor, required this.name, required this.apiUrl, required this.primaryColor, }) { _instance = this; } bool get isProduction => flavor != Flavor.internal; } ``` Configurer les fichiers natifs Android (build.gradle) et iOS (xcconfig) pour chaque flavor. ### 20. Quelles sont les bonnes pratiques de sécurité Flutter ? La sécurité couvre le stockage des données sensibles, la validation des entrées, et la protection contre le reverse engineering. ```dart // security_example.dart import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SecureStorage { // Stockage chiffré pour données sensibles final _storage = const FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), ); Future saveToken(String token) async { await _storage.write(key: 'auth_token', value: token); } Future getToken() async { return _storage.read(key: 'auth_token'); } Future deleteToken() async { await _storage.delete(key: 'auth_token'); } } // Validation des entrées class InputValidator { // Prévention injection static String sanitize(String input) { return input .replaceAll(RegExp(r'[<>"\']'), '') .trim(); } // Validation longueur static bool isValidLength(String input, int min, int max) { return input.length >= min && input.length <= max; } } // Protection SSL pinning class SecureApiClient { Dio createSecureClient() { final dio = Dio(); // Ajout du certificate pinning (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { final client = HttpClient(); client.badCertificateCallback = (cert, host, port) { // Vérifier le fingerprint du certificat return _isValidCertificate(cert); }; return client; }; return dio; } } ``` Ne jamais stocker de secrets dans le code. Utiliser `flutter_secure_storage` pour les tokens et `--obfuscate` pour la production. ## Conclusion Ces 20 questions couvrent les aspects essentiels des entretiens Flutter : fondamentaux du framework, maîtrise de Dart, patterns d'architecture, et bonnes pratiques de production. La clé du succès réside dans la compréhension profonde des mécanismes sous-jacents, pas seulement la syntaxe. ### Checklist de préparation - ✅ Maîtriser la différence StatelessWidget/StatefulWidget et les cas d'usage - ✅ Comprendre le widget tree et l'optimisation des rebuilds - ✅ Pratiquer async/await, Futures et Streams avec des exercices - ✅ Implémenter un projet avec Riverpod pour le state management - ✅ Connaître GoRouter et le deep linking - ✅ Écrire des tests unitaires et widget tests - ✅ Comprendre la configuration multi-environnements La pratique régulière sur des projets personnels reste le meilleur moyen de consolider ces connaissances. Chaque question abordée ici mérite d'être approfondie avec du code réel pour maîtriser les subtilités du framework Flutter. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/flutter/flutter-questions-entretien