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.

Flutter révolutionne le développement mobile en permettant de créer des applications iOS et Android avec une seule base de code. Ce framework développé par Google combine performance native et productivité exceptionnelle grâce à son moteur de rendu Skia et son système de widgets déclaratifs. Ce guide couvre la création complète d'une application, de l'installation aux bonnes pratiques de production.
Flutter 3.27 apporte des améliorations significatives : support natif de Material 3 par défaut, nouvelles animations Impeller optimisées, et intégration Dart 3.6 avec les macros expérimentales. Le framework compile désormais en natif ARM64 pour des performances optimales.
Installation et configuration de l'environnement
La mise en place de Flutter nécessite quelques étapes de configuration. Le SDK Flutter inclut tout le nécessaire : le framework, les outils de compilation et le gestionnaire de dépendances Dart.
# terminal
# Téléchargement du SDK Flutter (macOS/Linux)
git clone https://github.com/flutter/flutter.git -b stable
export PATH="$PATH:`pwd`/flutter/bin"
# Vérification de l'installation et des dépendances
flutter doctor
# Création d'un nouveau projet
flutter create --org com.example mon_app
cd mon_app
# Lancement en mode développement
flutter runLa commande flutter doctor vérifie que toutes les dépendances sont installées : Android Studio, Xcode (macOS), et les émulateurs configurés.
# pubspec.yaml
name: mon_app
description: Application Flutter cross-platform
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.6.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# UI et design
cupertino_icons: ^1.0.8
google_fonts: ^6.2.1
# State management
flutter_riverpod: ^2.6.1
# Navigation
go_router: ^14.6.2
# HTTP et API
dio: ^5.7.0
# Stockage local
shared_preferences: ^2.3.4
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: trueCe fichier pubspec.yaml configure les dépendances essentielles pour une application Flutter moderne.
Architecture d'un projet Flutter
Une structure de projet claire facilite la maintenance et l'évolution de l'application. Cette organisation sépare clairement les responsabilités.
lib/
├── main.dart # Point d'entrée
├── app.dart # Configuration de l'app
├── core/
│ ├── constants/ # Couleurs, dimensions, strings
│ ├── theme/ # Thème Material 3
│ └── utils/ # Fonctions utilitaires
├── features/
│ ├── auth/ # Fonctionnalité authentification
│ │ ├── data/ # Repositories, data sources
│ │ ├── domain/ # Modèles, use cases
│ │ └── presentation/ # Screens, widgets, providers
│ └── home/ # Fonctionnalité accueil
│ ├── data/
│ ├── domain/
│ └── presentation/
├── shared/
│ ├── widgets/ # Widgets réutilisables
│ └── providers/ # Providers partagés
└── routing/
└── app_router.dart # Configuration des routesCette architecture "feature-first" regroupe tout le code relatif à une fonctionnalité dans un même dossier, facilitant la navigation et les refactorings.
L'approche feature-first organise le code par fonctionnalité métier plutôt que par type technique. Chaque feature contient ses propres modèles, widgets et logique, rendant le code plus modulaire et testable.
Comprendre les widgets Flutter
Les widgets constituent les briques fondamentales de Flutter. Tout est widget : les boutons, les textes, les layouts, même l'application elle-même. Flutter propose deux types de widgets : StatelessWidget (sans état) et StatefulWidget (avec état local).
import 'package:flutter/material.dart';
/// Widget bouton personnalisé réutilisable dans toute l'application.
/// Gère automatiquement les états loading et disabled.
class CustomButton extends StatelessWidget {
// Paramètres requis et optionnels du widget
final String label;
final VoidCallback? onPressed;
final bool isLoading;
final bool isOutlined;
// Constructeur avec paramètres nommés pour plus de clarté
const CustomButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.isOutlined = false,
});
Widget build(BuildContext context) {
// Accès au thème pour des styles cohérents
final theme = Theme.of(context);
// Construction conditionnelle selon le type de bouton
if (isOutlined) {
return OutlinedButton(
// Désactive le bouton pendant le chargement
onPressed: isLoading ? null : onPressed,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _buildChild(theme),
);
}
return FilledButton(
onPressed: isLoading ? null : onPressed,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _buildChild(theme),
);
}
/// Construit le contenu du bouton avec gestion du loading.
Widget _buildChild(ThemeData theme) {
if (isLoading) {
return const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
);
}
return Text(label);
}
}Ce widget encapsule la logique d'affichage d'un bouton avec gestion automatique des états de chargement.
import 'package:flutter/material.dart';
import '../../features/auth/domain/models/user.dart';
/// Carte affichant les informations d'un utilisateur.
/// Utilise Material 3 avec elevation et shape cohérentes.
class UserCard extends StatelessWidget {
final User user;
final VoidCallback? onTap;
const UserCard({
super.key,
required this.user,
this.onTap,
});
Widget build(BuildContext context) {
final theme = Theme.of(context);
// Card Material 3 avec InkWell pour effet ripple
return Card(
// Elevation adaptative selon le thème
elevation: 2,
// Forme arrondie cohérente
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
// Clippage pour que le ripple respecte les bords
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Avatar avec image ou initiales
CircleAvatar(
radius: 28,
backgroundImage: user.avatarUrl != null
? NetworkImage(user.avatarUrl!)
: null,
child: user.avatarUrl == null
? Text(user.initials)
: null,
),
const SizedBox(width: 16),
// Informations utilisateur
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.displayName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
user.email,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
// Icône de navigation
Icon(
Icons.chevron_right,
color: theme.colorScheme.onSurfaceVariant,
),
],
),
),
),
);
}
}La composition de widgets permet de construire des interfaces complexes à partir de blocs simples et réutilisables.
State management avec Riverpod
Riverpod représente la solution moderne pour la gestion d'état en Flutter. Cette bibliothèque offre une approche déclarative, type-safe et testable pour partager l'état entre les widgets.
/// Modèle utilisateur immutable avec factory fromJson.
class User {
final String id;
final String email;
final String displayName;
final String? avatarUrl;
final DateTime createdAt;
const User({
required this.id,
required this.email,
required this.displayName,
this.avatarUrl,
required this.createdAt,
});
/// Génère les initiales à partir du nom d'affichage.
String get initials {
final parts = displayName.split(' ');
if (parts.length >= 2) {
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
}
return displayName.substring(0, 2).toUpperCase();
}
/// Crée une instance depuis un JSON (réponse API).
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as String,
email: json['email'] as String,
displayName: json['display_name'] as String,
avatarUrl: json['avatar_url'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
/// Convertit en JSON pour envoi API.
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'display_name': displayName,
'avatar_url': avatarUrl,
'created_at': createdAt.toIso8601String(),
};
}
}import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/repositories/auth_repository.dart';
import '../../domain/models/user.dart';
/// État d'authentification représentant les différents cas possibles.
sealed class AuthState {
const AuthState();
}
class AuthInitial extends AuthState {
const AuthInitial();
}
class AuthLoading extends AuthState {
const AuthLoading();
}
class AuthAuthenticated extends AuthState {
final User user;
const AuthAuthenticated(this.user);
}
class AuthUnauthenticated extends AuthState {
const AuthUnauthenticated();
}
class AuthError extends AuthState {
final String message;
const AuthError(this.message);
}
/// Provider pour le repository d'authentification.
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepository();
});
/// Provider principal gérant l'état d'authentification.
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
final repository = ref.watch(authRepositoryProvider);
return AuthNotifier(repository);
});
/// Notifier gérant la logique d'authentification.
class AuthNotifier extends StateNotifier<AuthState> {
final AuthRepository _repository;
AuthNotifier(this._repository) : super(const AuthInitial()) {
// Vérifie l'état initial au démarrage
checkAuthStatus();
}
/// Vérifie si un utilisateur est déjà connecté.
Future<void> checkAuthStatus() async {
state = const AuthLoading();
try {
final user = await _repository.getCurrentUser();
if (user != null) {
state = AuthAuthenticated(user);
} else {
state = const AuthUnauthenticated();
}
} catch (e) {
state = const AuthUnauthenticated();
}
}
/// Connecte un utilisateur avec email et mot de passe.
Future<void> signIn(String email, String password) async {
state = const AuthLoading();
try {
final user = await _repository.signIn(email, password);
state = AuthAuthenticated(user);
} catch (e) {
state = AuthError(e.toString());
}
}
/// Crée un nouveau compte utilisateur.
Future<void> signUp(String email, String password, String displayName) async {
state = const AuthLoading();
try {
final user = await _repository.signUp(email, password, displayName);
state = AuthAuthenticated(user);
} catch (e) {
state = AuthError(e.toString());
}
}
/// Déconnecte l'utilisateur.
Future<void> signOut() async {
state = const AuthLoading();
await _repository.signOut();
state = const AuthUnauthenticated();
}
}Le pattern sealed class (Dart 3.0+) garantit que tous les cas d'état sont traités lors de l'utilisation avec switch.
Prêt à réussir tes entretiens Flutter ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Navigation avec GoRouter
GoRouter simplifie la navigation déclarative en Flutter. Cette bibliothèque gère les routes, les paramètres, les redirections et la navigation imbriquée.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../features/auth/presentation/providers/auth_provider.dart';
import '../features/auth/presentation/screens/login_screen.dart';
import '../features/auth/presentation/screens/register_screen.dart';
import '../features/home/presentation/screens/home_screen.dart';
import '../features/home/presentation/screens/profile_screen.dart';
import '../shared/widgets/shell_scaffold.dart';
/// Provider pour le routeur avec gestion de l'authentification.
final routerProvider = Provider<GoRouter>((ref) {
// Écoute les changements d'état d'authentification
final authState = ref.watch(authProvider);
return GoRouter(
// Route initiale
initialLocation: '/',
// Rafraîchit le routeur quand l'auth change
refreshListenable: GoRouterRefreshStream(ref, authProvider),
// Gestion de la redirection selon l'authentification
redirect: (context, state) {
final isAuthenticated = authState is AuthAuthenticated;
final isAuthRoute = state.matchedLocation.startsWith('/auth');
// Non authentifié sur route protégée → login
if (!isAuthenticated && !isAuthRoute) {
return '/auth/login';
}
// Authentifié sur route auth → home
if (isAuthenticated && isAuthRoute) {
return '/';
}
return null; // Pas de redirection
},
routes: [
// Routes d'authentification (sans shell)
GoRoute(
path: '/auth/login',
name: 'login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/auth/register',
name: 'register',
builder: (context, state) => const RegisterScreen(),
),
// Routes protégées avec shell (bottom navigation)
ShellRoute(
builder: (context, state, child) {
return ShellScaffold(child: child);
},
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/profile',
name: 'profile',
builder: (context, state) => const ProfileScreen(),
),
GoRoute(
path: '/profile/:userId',
name: 'userProfile',
builder: (context, state) {
// Extraction du paramètre de route
final userId = state.pathParameters['userId']!;
return ProfileScreen(userId: userId);
},
),
],
),
],
// Page d'erreur personnalisée
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text('Page non trouvée: ${state.error}'),
),
),
);
});
/// Stream pour déclencher le refresh du routeur.
class GoRouterRefreshStream extends ChangeNotifier {
GoRouterRefreshStream(Ref ref, StateNotifierProvider provider) {
ref.listen(provider, (previous, next) {
notifyListeners();
});
}
}La redirection automatique selon l'état d'authentification garantit que les routes protégées restent inaccessibles aux utilisateurs non connectés.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Scaffold avec navigation bottom bar pour les routes protégées.
class ShellScaffold extends StatelessWidget {
final Widget child;
const ShellScaffold({
super.key,
required this.child,
});
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
// Détermine l'index actif selon la route
selectedIndex: _calculateSelectedIndex(context),
onDestinationSelected: (index) => _onItemTapped(index, context),
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Accueil',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: 'Profil',
),
],
),
);
}
/// Calcule l'index de navigation selon la route actuelle.
int _calculateSelectedIndex(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
if (location.startsWith('/profile')) return 1;
return 0;
}
/// Navigue vers la route correspondant à l'index.
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0:
context.goNamed('home');
case 1:
context.goNamed('profile');
}
}
}Le NavigationBar Material 3 s'adapte automatiquement aux thèmes clair et sombre.
Écrans et formulaires
La création d'écrans interactifs combine les widgets, le state management et la validation des formulaires.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../providers/auth_provider.dart';
import '../../../../shared/widgets/custom_button.dart';
/// Écran de connexion avec formulaire validé.
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
// Clé globale pour le formulaire
final _formKey = GlobalKey<FormState>();
// Contrôleurs pour les champs de texte
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
// État local pour la visibilité du mot de passe
bool _obscurePassword = true;
void dispose() {
// Libération des ressources
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
/// Soumet le formulaire de connexion.
Future<void> _submit() async {
// Valide tous les champs
if (_formKey.currentState!.validate()) {
await ref.read(authProvider.notifier).signIn(
_emailController.text.trim(),
_passwordController.text,
);
}
}
Widget build(BuildContext context) {
final theme = Theme.of(context);
final authState = ref.watch(authProvider);
final isLoading = authState is AuthLoading;
// Écoute les erreurs pour afficher un snackbar
ref.listen<AuthState>(authProvider, (previous, next) {
if (next is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(next.message),
backgroundColor: theme.colorScheme.error,
),
);
}
});
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo ou titre
Icon(
Icons.flutter_dash,
size: 80,
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Connexion',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Champ email avec validation
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'exemple@email.com',
prefixIcon: Icon(Icons.email_outlined),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'L\'email est requis';
}
// Validation basique du format email
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
return 'Format d\'email invalide';
}
return null;
},
),
const SizedBox(height: 16),
// Champ mot de passe avec toggle visibilité
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
decoration: InputDecoration(
labelText: 'Mot de passe',
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Le mot de passe est requis';
}
if (value.length < 8) {
return 'Minimum 8 caractères';
}
return null;
},
),
const SizedBox(height: 24),
// Bouton de connexion
CustomButton(
label: 'Se connecter',
isLoading: isLoading,
onPressed: _submit,
),
const SizedBox(height: 16),
// Lien vers inscription
TextButton(
onPressed: () => context.goNamed('register'),
child: const Text('Pas de compte ? Créer un compte'),
),
],
),
),
),
),
),
);
}
}L'utilisation de ConsumerStatefulWidget combine l'état local (contrôleurs de formulaire) avec l'état global (Riverpod).
Les TextEditingController doivent impérativement être disposed dans la méthode dispose() pour éviter les fuites mémoire. Cette règle s'applique à tous les contrôleurs et listeners créés manuellement.
Communication HTTP avec Dio
Dio offre une API HTTP puissante avec intercepteurs, gestion des erreurs et transformation automatique des réponses.
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constants/api_constants.dart';
/// Provider pour le client HTTP Dio.
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient();
});
/// Client HTTP configuré avec intercepteurs.
class ApiClient {
late final Dio _dio;
ApiClient() {
_dio = Dio(
BaseOptions(
baseUrl: ApiConstants.baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
// Ajout des intercepteurs
_dio.interceptors.addAll([
_AuthInterceptor(),
_LoggingInterceptor(),
]);
}
/// Requête GET générique.
Future<T> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
T Function(dynamic)? fromJson,
}) async {
try {
final response = await _dio.get(
path,
queryParameters: queryParameters,
);
if (fromJson != null) {
return fromJson(response.data);
}
return response.data as T;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Requête POST générique.
Future<T> post<T>(
String path, {
dynamic data,
T Function(dynamic)? fromJson,
}) async {
try {
final response = await _dio.post(path, data: data);
if (fromJson != null) {
return fromJson(response.data);
}
return response.data as T;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Définit le token d'authentification.
void setAuthToken(String token) {
_dio.options.headers['Authorization'] = 'Bearer $token';
}
/// Supprime le token d'authentification.
void clearAuthToken() {
_dio.options.headers.remove('Authorization');
}
/// Convertit les erreurs Dio en exceptions lisibles.
Exception _handleError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return Exception('Connexion timeout. Vérifiez votre connexion.');
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode;
final message = error.response?.data['message'] ?? 'Erreur serveur';
return Exception('Erreur $statusCode: $message');
case DioExceptionType.cancel:
return Exception('Requête annulée');
default:
return Exception('Erreur réseau: ${error.message}');
}
}
}
/// Intercepteur pour ajouter automatiquement le token.
class _AuthInterceptor extends Interceptor {
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
// Le token est ajouté via setAuthToken()
handler.next(options);
}
void onError(
DioException err,
ErrorInterceptorHandler handler,
) {
// Gestion du 401 pour refresh token
if (err.response?.statusCode == 401) {
// TODO: Implémenter le refresh token
}
handler.next(err);
}
}
/// Intercepteur de logging pour le développement.
class _LoggingInterceptor extends Interceptor {
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
print('→ ${options.method} ${options.path}');
handler.next(options);
}
void onResponse(
Response response,
ResponseInterceptorHandler handler,
) {
print('← ${response.statusCode} ${response.requestOptions.path}');
handler.next(response);
}
void onError(
DioException err,
ErrorInterceptorHandler handler,
) {
print('✗ ${err.response?.statusCode} ${err.requestOptions.path}');
handler.next(err);
}
}Les intercepteurs permettent de centraliser la logique d'authentification et de logging sans polluer chaque requête.
Thème Material 3 personnalisé
Un thème cohérent garantit une expérience utilisateur uniforme. Material 3 (Material You) s'adapte aux préférences système.
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// Configuration du thème de l'application.
class AppTheme {
// Couleur primaire de la marque
static const _primaryColor = Color(0xFF6750A4);
/// Thème clair Material 3.
static ThemeData get lightTheme {
// Génération du color scheme depuis la couleur primaire
final colorScheme = ColorScheme.fromSeed(
seedColor: _primaryColor,
brightness: Brightness.light,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
// Typographie personnalisée
textTheme: GoogleFonts.interTextTheme(),
// Configuration de l'AppBar
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
),
// Configuration des champs de formulaire
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.error),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
// Configuration des boutons
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
// Configuration des cartes
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
);
}
/// Thème sombre Material 3.
static ThemeData get darkTheme {
final colorScheme = ColorScheme.fromSeed(
seedColor: _primaryColor,
brightness: Brightness.dark,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
textTheme: GoogleFonts.interTextTheme(
ThemeData.dark().textTheme,
),
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.error),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
);
}
}ColorScheme.fromSeed génère automatiquement une palette complète et accessible à partir d'une seule couleur.
Configuration de l'application
Le point d'entrée de l'application initialise Riverpod et configure les thèmes.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app.dart';
void main() {
// Garantit l'initialisation des bindings Flutter
WidgetsFlutterBinding.ensureInitialized();
// Lance l'application avec le scope Riverpod
runApp(
const ProviderScope(
child: App(),
),
);
}import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/theme/app_theme.dart';
import 'routing/app_router.dart';
/// Widget racine de l'application.
class App extends ConsumerWidget {
const App({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// Récupère le routeur configuré
final router = ref.watch(routerProvider);
return MaterialApp.router(
title: 'Mon App Flutter',
debugShowCheckedModeBanner: false,
// Configuration des thèmes
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
// Configuration du routeur
routerConfig: router,
);
}
}ThemeMode.system adapte automatiquement le thème aux préférences de l'utilisateur.
Conclusion
Flutter offre un écosystème complet pour créer des applications cross-platform performantes. La combinaison des widgets déclaratifs, de Riverpod pour l'état et de GoRouter pour la navigation permet de construire des applications maintenables et évolutives.
Checklist pour démarrer avec Flutter
- ✅ Installer Flutter SDK et configurer l'environnement avec
flutter doctor - ✅ Structurer le projet en features pour une meilleure organisation
- ✅ Utiliser Riverpod pour la gestion d'état type-safe
- ✅ Configurer GoRouter avec redirections d'authentification
- ✅ Créer des widgets réutilisables et composables
- ✅ Implémenter un thème Material 3 cohérent
- ✅ Centraliser les appels HTTP avec Dio et intercepteurs
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
L'approche déclarative de Flutter encourage la composition et la réutilisation. Chaque widget peut être testé indépendamment, et la hot-reload accélère considérablement le cycle de développement. Cette base solide permet d'étendre l'application vers des fonctionnalités avancées comme les animations complexes, les tests automatisés et le déploiement sur les stores.
Tags
Partager
Articles similaires

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.

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.

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.