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.

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.
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.
// 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');
}
}// 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).
// 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.
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.
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.
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.
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 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.
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 : 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 : 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.
// 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);
}
}// 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.
// 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.
// ❌ 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.
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.
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.
// 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é.
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.
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.
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.
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.
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;
}
}
}# terminal
# Lancement avec environnement spécifique
flutter run --dart-define=ENV=staging
# Build production
flutter build apk --dart-define=ENV=prod --release19. 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).
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.
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
Partager
Articles similaires

Flutter : Créer votre première app cross-platform
Guide complet pour créer une application mobile cross-platform avec Flutter et Dart. Widgets, state management, navigation et bonnes pratiques pour débutants.

Dart 3 en pratique : Records, Pattern Matching et Sealed Classes pour Flutter
Guide complet sur les records, le pattern matching et les sealed classes de Dart 3 pour Flutter. Exemples concrets, bonnes pratiques et questions d'entretien technique avancees.

State Management Flutter : Riverpod vs BLoC - Guide Comparatif Complet
Comparaison approfondie entre Riverpod et BLoC pour la gestion d'état Flutter. Architecture, performances, testabilité et cas d'usage pour choisir la meilleure solution.