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.

Questions d'entretien Flutter pour développeurs mobiles

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.

stateless_example.dartdart
// 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});

  
  Widget build(BuildContext context) {
    // Build appelé une seule fois (sauf si parent reconstruit)
    return Text('Bienvenue, $username');
  }
}
stateful_example.dartdart
// StatefulWidget : état mutable, peut se reconstruire
class LikeButton extends StatefulWidget {
  const LikeButton({super.key});

  
  State<LikeButton> createState() => _LikeButtonState();
}

class _LikeButtonState extends State<LikeButton> {
  // État local mutable
  int _likeCount = 0;

  void _incrementLike() {
    // setState déclenche rebuild avec nouvel état
    setState(() {
      _likeCount++;
    });
  }

  
  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).

widget_tree_example.dartdart
// Structure déclarative - les widgets décrivent l'UI
class MyApp extends StatelessWidget {
  
  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.

const_example.dartdart
class OptimizedScreen extends StatelessWidget {
  const OptimizedScreen({super.key});

  
  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});

  
  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.

keys_example.dartdart
class TodoList extends StatelessWidget {
  final List<Todo> todos;

  const TodoList({super.key, required this.todos});

  
  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 {
  
  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.

final_const_example.dartdart
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.

async_example.dartdart
class UserRepository {
  final ApiClient _client;

  UserRepository(this._client);

  // Future : promesse d'une valeur future
  Future<User> 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<List<User>> fetchUsers(List<String> 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<void> processSequentially(List<String> 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.

stream_example.dartdart
class MessageService {
  // StreamController gère la création et diffusion
  final _messageController = StreamController<Message>.broadcast();

  // Exposer uniquement le Stream (pas le Sink)
  Stream<Message> 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});

  
  Widget build(BuildContext context) {
    return StreamBuilder<Message>(
      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.

Prêt à réussir tes entretiens Flutter ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

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.

riverpod_example.dartdart
// Riverpod : approche déclarative et type-safe
final userProvider = FutureProvider.autoDispose<User>((ref) async {
  final repository = ref.watch(userRepositoryProvider);
  return repository.fetchCurrentUser();
});

class UserProfile extends ConsumerWidget {
  
  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),
    );
  }
}
bloc_example.dartdart
// 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<AuthEvent, AuthState> {
  AuthBloc() : super(AuthInitial()) {
    on<LoginRequested>(_onLoginRequested);
  }

  Future<void> _onLoginRequested(
    LoginRequested event,
    Emitter<AuthState> 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.

domain/entities/user.dartdart
// 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<User> getUser(String id);
  Future<void> 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<User> call(String userId) {
    return _repository.getUser(userId);
  }
}
data/repositories/user_repository_impl.dartdart
// Implémentation concrète avec sources de données
class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource _remoteDataSource;
  final UserLocalDataSource _localDataSource;

  UserRepositoryImpl(this._remoteDataSource, this._localDataSource);

  
  Future<User> 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.

di_example.dartdart
// Définition des providers (dépendances)
final apiClientProvider = Provider<ApiClient>((ref) {
  return ApiClient(baseUrl: Environment.apiUrl);
});

final userRepositoryProvider = Provider<UserRepository>((ref) {
  // Injecte automatiquement ApiClient
  final client = ref.watch(apiClientProvider);
  return UserRepositoryImpl(client);
});

final getUserUseCaseProvider = Provider<GetUserUseCase>((ref) {
  final repository = ref.watch(userRepositoryProvider);
  return GetUserUseCase(repository);
});

// Utilisation dans un widget
class UserScreen extends ConsumerWidget {
  
  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.

rebuild_optimization.dartdart
// ❌ Mauvais : tout reconstruit à chaque changement
class BadExample extends ConsumerWidget {
  
  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 {
  
  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 {
  
  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.

list_optimization.dartdart
class OptimizedList extends StatelessWidget {
  final List<Item> items;

  const OptimizedList({super.key, required this.items});

  
  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 {
  
  ConsumerState<InfiniteList> createState() => _InfiniteListState();
}

class _InfiniteListState extends ConsumerState<InfiniteList> {
  final _scrollController = ScrollController();

  
  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();
    }
  }

  
  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.

impeller_benefits.dartdart
// Les animations complexes bénéficient d'Impeller
class SmoothAnimation extends StatefulWidget {
  
  State<SmoothAnimation> createState() => _SmoothAnimationState();
}

class _SmoothAnimationState extends State<SmoothAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..repeat();
  }

  
  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é.

deep_linking.dartdart
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.

form_validation.dartdart
class RegistrationForm extends StatefulWidget {
  
  State<RegistrationForm> createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _confirmController = TextEditingController();

  
  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.

widget_test.dartdart
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.

provider_test.dartdart
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.

environment.dartdart
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).

main_client1.dartdart
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.

security_example.dartdart
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<void> saveToken(String token) async {
    await _storage.write(key: 'auth_token', value: token);
  }

  Future<String?> getToken() async {
    return _storage.read(key: 'auth_token');
  }

  Future<void> 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

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

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.

Tags

#flutter
#dart
#interview
#mobile development
#entretien technique

Partager

Articles similaires