# 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. - Published: 2026-01-20 - Updated: 2026-03-28 - Author: SharpSkill - Tags: flutter, dart, mobile development, cross-platform, android ios - Reading time: 18 min --- 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 - Janvier 2026** > > 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. ```bash # 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 run ``` La commande `flutter doctor` vérifie que toutes les dépendances sont installées : Android Studio, Xcode (macOS), et les émulateurs configurés. ```yaml # 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: true ``` Ce 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 routes ``` Cette architecture "feature-first" regroupe tout le code relatif à une fonctionnalité dans un même dossier, facilitant la navigation et les refactorings. > **Architecture Feature-First** > > 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). ```dart // lib/shared/widgets/custom_button.dart 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, }); @override 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. ```dart // lib/shared/widgets/user_card.dart 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, }); @override 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. ```dart // lib/features/auth/domain/models/user.dart /// 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 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 toJson() { return { 'id': id, 'email': email, 'display_name': displayName, 'avatar_url': avatarUrl, 'created_at': createdAt.toIso8601String(), }; } } ``` ```dart // lib/features/auth/presentation/providers/auth_provider.dart 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((ref) { return AuthRepository(); }); /// Provider principal gérant l'état d'authentification. final authProvider = StateNotifierProvider((ref) { final repository = ref.watch(authRepositoryProvider); return AuthNotifier(repository); }); /// Notifier gérant la logique d'authentification. class AuthNotifier extends StateNotifier { 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 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 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 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 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. ## 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. ```dart // lib/routing/app_router.dart 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((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. ```dart // lib/shared/widgets/shell_scaffold.dart 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, }); @override 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. ```dart // lib/features/auth/presentation/screens/login_screen.dart 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}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { // Clé globale pour le formulaire final _formKey = GlobalKey(); // 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; @override void dispose() { // Libération des ressources _emailController.dispose(); _passwordController.dispose(); super.dispose(); } /// Soumet le formulaire de connexion. Future _submit() async { // Valide tous les champs if (_formKey.currentState!.validate()) { await ref.read(authProvider.notifier).signIn( _emailController.text.trim(), _passwordController.text, ); } } @override 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(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). > **Gestion des contrôleurs** > > 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. ```dart // lib/core/network/api_client.dart 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((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 get( String path, { Map? 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 post( 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 { @override void onRequest( RequestOptions options, RequestInterceptorHandler handler, ) { // Le token est ajouté via setAuthToken() handler.next(options); } @override 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 { @override void onRequest( RequestOptions options, RequestInterceptorHandler handler, ) { print('→ ${options.method} ${options.path}'); handler.next(options); } @override void onResponse( Response response, ResponseInterceptorHandler handler, ) { print('← ${response.statusCode} ${response.requestOptions.path}'); handler.next(response); } @override 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. ```dart // lib/core/theme/app_theme.dart 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. ```dart // lib/main.dart 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(), ), ); } ``` ```dart // lib/app.dart 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}); @override 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 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/flutter/flutter-creer-premiere-app-cross-platform