Objectif
- À la fin de ce tutoriel, vous serez capable de :
- Créer une base de données MySQL simple pour l’authentification et la gestion des clients.
- Écrire un backend PHP pour interagir avec MySQL (CRUD et connexion).
- Échanger des données entre Flutter et PHP via HTTP en utilisant le format JSON.
- Créer une interface Flutter fonctionnelle pour se connecter, afficher, ajouter, modifier et supprimer des clients avec vérification côté serveur.
- À la fin de ce tutoriel, vous serez capable de :
Présentation
- Flutter ne peut pas se connecter directement à une base de données MySQL. Pour cela, on utilise PHP comme intermédiaire :
- PHP interagit avec MySQL et expose des APIs REST (GET, POST, UPDATE, DELETE).
- Flutter envoie des requêtes HTTP vers ces APIs et reçoit des réponses JSON.
- Cette architecture permet de sécuriser l’accès à la base de données tout en gardant la logique Flutter indépendante du backend.
- Flutter ne peut pas se connecter directement à une base de données MySQL. Pour cela, on utilise PHP comme intermédiaire :
Règles et bonnes pratiques pour Flutter ↔ PHP MySQL
-
Côté PHP :
- Toujours définir les
en-têtes CORSpour autoriser les requêtes Flutter :header("Access-Control-Allow-Origin: *"); header("Content-Type: application/json"); - Utiliser
json_decode(file_get_contents("php://input"))pour lire les données envoyées par Flutter. - Renvoyer systématiquement des réponses JSON avec un indicateur de succès et un message :
echo json_encode(['success' => true, 'message' => 'Opération réussie']); - Toujours utiliser des requêtes préparées pour sécuriser contre les injections SQL.
- Gérer la requête OPTIONS pour le pré-vol CORS sur les requêtes POST/PUT/DELETE.
- Toujours définir les
-
Côté Flutter :
- Créer une classe modèle pour représenter les données (ex : Client).
- Utiliser le package http pour envoyer des requêtes HTTP.
- Convertir les réponses JSON en objets Flutter.
- Gérer les erreurs réseau et les statuts HTTP.
- Séparer la logique UI et la logique API via un service dédié (ex : ClientService).
-
Architecture CRUD Flutter ↔ PHP MySQL
Action PHP API Flutter Lire get_client.php → renvoie JSON FutureBuilder pour afficher la liste Ajouter insert_client.php (POST JSON) Formulaire → ClientService.insertClient() Modifier update_client.php (POST JSON) Formulaire pré-rempli → ClientService.updateClient() Supprimer delete_client.php (POST JSON) Bouton suppression → ClientService.deleteClient()
-
Configuration du projet
Étape 1 : Créer la base de données MySQL
- Créer la base de données :
- Créer la table
utulisateurs: - Créer la table
Ajouter des utulisateurs: - Ajouter un utilisateur de test en exécutant le fichier add_user.php:
CREATE DATABASE base_api_php;CREATE TABLE utilisateurs ( id int NOT NULL AUTO_INCREMENT, email varchar(180) COLLATE utf8mb4_unicode_ci NOT NULL, roles json NOT NULL, password varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (id), UNIQUE KEY UNIQ_IDENTIFIER_EMAIL (email) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;Fichier add_user.php:
<?php require_once('dbconnect.php'); $email = 'test@example.com'; $password = password_hash('123456', PASSWORD_DEFAULT); $roles = json_encode(['ROLE_USER']); $stmt = $pdo->prepare("INSERT INTO utilisateurs (email, password, roles) VALUES (?, ?, ?)"); $stmt->execute([$email, $password, $roles]); echo "Utilisateur ajouté."; ?>Étape 2 : Créer les scripts PHP
Fichier config.php:
<?php define('DB_HOST', '127.0.0.1'); // l'hôte de la base de données MySQL define('DB_NAME', 'base_api_php'); // le nom de la base de données MySQL define('DB_USER', 'root'); // le nom d'utilisateur MySQL define('DB_PASSWORD', ''); // le mot de passe MySQLhriadh define('DB_CHARSET', 'utf8'); // le jeu de caractères pour la base de données MySQL ?>Fichier dbconnect.php:
<?php require_once('config.php'); // Créer une nouvelle connexion PDO try { $pdo = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET, DB_USER, DB_PASSWORD); // Activer le mode d'erreur PDO $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // Activer le mode d'émulation PDO pour désactiver l'utilisation de 'prepared statements' $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); } catch(PDOException $e) { // En cas d'erreur de connexion à la base de données, afficher un message d'erreur et arrêter le script die("Erreur de connexion à la base de données: ".$e->getMessage()); } ?>Fichier login.php:
<?php // Cela permet à tous les domaines (origines) d’accéder à cette // ressource via une requête HTTP (notamment via AJAX ou fetch() en // JavaScript/Flutter). // Le * (joker) autorise tout le monde. En production, on remplace souvent par un domaine spécifique : header("Access-Control-Allow-Origin: *"); // Cela autorise les requêtes à inclure certains en-têtes personnalisés (headers), ici Content-Type. header("Access-Control-Allow-Headers: Content-Type"); // Cela indique que la réponse du serveur est en JSON. header("Content-Type: application/json"); require_once('dbconnect.php'); // Récupérer les données JSON envoyées par Flutter $data = json_decode(file_get_contents("php://input")); $email = $data->email ?? ''; $password = $data->password ?? ''; if (empty($email) || empty($password)) { echo json_encode(['success' => false, 'message' => 'Email ou mot de passe manquant.']); exit; } // Requête pour trouver l'utilisateur $stmt = $pdo->prepare("SELECT * FROM utilisateurs WHERE email = ?"); $stmt->execute([$email]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if ($user && password_verify($password, $user['password'])) { echo json_encode(['success' => true, 'user' => [ 'id' => $user['id'], 'email' => $user['email'], 'roles' => json_decode($user['roles']) ]]); } else { echo json_encode(['success' => false, 'message' => 'Identifiants invalides']); } ?>Fichier index.php:
<?php /** * CONFIGURATION DES ENTÊTES (HEADERS) * Indispensable pour transformer PHP en une API accessible et compréhensible. */ // Autorise les requêtes provenant de n'importe quel domaine (CORS) header("Access-Control-Allow-Origin: *"); // Autorise spécifiquement l'envoi du header "Content-Type" par le client header("Access-Control-Allow-Headers: Content-Type"); // Indique au navigateur/client que la réponse sera au format JSON (et non HTML) header("Content-Type: application/json"); /** * LOGIQUE DU ROUTEUR */ // 1. On récupère l'URL demandée, on la nettoie et on retire les slashs inutiles // Exemple : "/mon_api_php/login/" devient "mon_api_php/login" $route = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'); // 2. Système de routage : on vérifie si l'URL correspond à une page précise if ($route === 'mon_api_php/login') { // Si oui, on délègue le travail au fichier login.php require __DIR__ . '/login.php'; // On arrête l'exécution ici pour ne pas envoyer le message "API OK" par erreur exit; } /** * RÉPONSE PAR DÉFAUT * Si aucune route n'a matché au-dessus, on renvoie un statut de santé de l'API. */ echo json_encode([ 'success' => true, 'message' => 'API OK' ]); ?>Fichier logout.php:
<?php // mon_api_php/logout.php header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: POST, GET, OPTIONS"); header("Access-Control-Allow-Headers: Content-Type, Authorization"); header("Content-Type: application/json; charset=UTF-8"); // Gérer OPTIONS if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { http_response_code(200); exit(); } // Pour logout, on accepte simplement la requête // En production, vous pourriez blacklister le token echo json_encode([ 'success' => true, 'message' => 'Déconnexion réussie' ]); ?>Fichier register.php:
<?php // Headers CORS et JSON - DOIVENT être en premier header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST, GET, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Accept, Authorization'); header('Content-Type: application/json; charset=UTF-8'); // Désactiver l'affichage des erreurs HTML ini_set('display_errors', 0); error_reporting(0); // Inclure la connexion à la base de données require_once('dbconnect.php'); // Fonction pour retourner une réponse JSON function jsonResponse($data, $code = 200) { http_response_code($code); echo json_encode($data); exit; } // Gérer les requêtes OPTIONS (preflight CORS) if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { jsonResponse(['status' => 'OK'], 200); } // Test GET pour vérifier que le fichier fonctionne if ($_SERVER['REQUEST_METHOD'] === 'GET') { jsonResponse([ 'status' => 'OK', 'message' => 'API register accessible' ]); } // Traitement POST pour l'inscription if ($_SERVER['REQUEST_METHOD'] === 'POST') { try { // Lire les données JSON $json = file_get_contents('php://input'); $data = json_decode($json, true); // Vérifier que le JSON est valide if (json_last_error() !== JSON_ERROR_NONE) { jsonResponse([ 'success' => false, 'message' => 'JSON invalide' ], 400); } // Vérifier les champs requis if (empty($data['email']) || empty($data['password'])) { jsonResponse([ 'success' => false, 'message' => 'Email et mot de passe requis' ], 400); } $email = trim($data['email']); $password = trim($data['password']); $nomComplet = isset($data['nom_complet']) ? trim($data['nom_complet']) : null; $pays = isset($data['pays']) ? trim($data['pays']) : null; $numTel = isset($data['num_tel']) ? trim($data['num_tel']) : null; $urlPhotoProfil = isset($data['url_photo_profil']) ? trim($data['url_photo_profil']) : null; // Gérer les rôles (par défaut ROLE_USER) $roles = isset($data['roles']) && is_array($data['roles']) ? $data['roles'] : ['ROLE_USER']; // Validation de l'email if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { jsonResponse([ 'success' => false, 'message' => 'Email invalide' ], 400); } // Validation du mot de passe if (strlen($password) < 6) { jsonResponse([ 'success' => false, 'message' => 'Le mot de passe doit contenir au moins 6 caractères' ], 400); } // Validation des rôles $rolesValides = ['ROLE_ADMIN', 'ROLE_USER', 'ROLE_CLIENT', 'ROLE_VENDEUR']; foreach ($roles as $role) { if (!in_array($role, $rolesValides)) { jsonResponse([ 'success' => false, 'message' => 'Rôle invalide: ' . $role ], 400); } } // Vérifier si l'email existe déjà $stmt = $pdo->prepare("SELECT id FROM utilisateurs WHERE email = ?"); $stmt->execute([$email]); if ($stmt->rowCount() > 0) { jsonResponse([ 'success' => false, 'message' => 'Cet email est déjà utilisé' ], 409); } // Hasher le mot de passe $hashedPassword = password_hash($password, PASSWORD_DEFAULT); // Créer le JSON pour les rôles $rolesJson = json_encode($roles); // Insérer le nouvel utilisateur avec les bons noms de colonnes $stmt = $pdo->prepare( "INSERT INTO utilisateurs (email, mot_de_passe_hache, nom_complet, pays, num_tel, url_photo_profil, roles) VALUES (?, ?, ?, ?, ?, ?, ?)" ); $stmt->execute([$email, $hashedPassword, $nomComplet, $pays, $numTel, $urlPhotoProfil, $rolesJson]); // Récupérer l'ID du nouvel utilisateur $userId = $pdo->lastInsertId(); // Générer un token (optionnel) $token = bin2hex(random_bytes(32)); // Réponse de succès jsonResponse([ 'success' => true, 'message' => 'Inscription réussie', 'token' => $token, 'user' => [ 'id' => (int)$userId, 'email' => $email, 'nom_complet' => $nomComplet, 'pays' => $pays, 'num_tel' => $numTel, 'url_photo_profil' => $urlPhotoProfil, 'roles' => $roles ] ], 201); } catch (PDOException $e) { jsonResponse([ 'success' => false, 'message' => 'Erreur de base de données: ' . $e->getMessage() ], 500); } catch (Exception $e) { jsonResponse([ 'success' => false, 'message' => 'Erreur serveur: ' . $e->getMessage() ], 500); } } // Méthode non autorisée jsonResponse([ 'success' => false, 'message' => 'Méthode non autorisée' ], 405); ?>Étape 3 : Créer le projet Flutter
- Créer le projet :
- Ajouter la dépendance HTTP dans
pubspec.yaml:
flutter create exemple01_phpdependencies: flutter: sdk: flutter http: ^0.13.5Architecture du projet (Dart / Flutter)
-
lib/
│
├── controllers/
│ ├── auth_controller.dart
│ └── product_controller.dart
│
├── models/
│ ├── product_model.dart
│ └── user_model.dart
│
├── services/
│ ├── api_base.dart
│ ├── auth_api.dart
│ └── product_api.dart
│
├── views/
│ ├── admin_dashboard.dart
│ ├── client_dashboard.dart
│ ├── vendeur_dashboard.dart
│ ├── dashboard_router.dart
│ ├── home_page.dart
│ ├── login_page.dart
│ ├── register_page.dart
│ └── products_page.dart
│
└── main.dart
Étape 4 : Les fichiers Controllers/
- Description : Ce fichier est le point d’entrée de votre application Flutter. Il lance le widget
MainAppqui affiche directement la page de connexionLoginPage.
Fichier : main.dart
import 'package:exemple01/controllers/product_controller.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'controllers/auth_controller.dart'; import 'views/login_page.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => AuthController()), ], child: MaterialApp( debugShowCheckedModeBanner: false, home: LoginPage(), ), ); } }- Description : Ce fichier est le point d’entrée de votre application Flutter. Il lance le widget
-
Dossier 'Controllers'
Fichier : auth_controller.dart
import 'package:exemple01/services/api_base.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/auth_api.dart'; import '../models/user_model.dart'; class AuthController with ChangeNotifier { User? _user; String? _token; bool _isLoading = false; String? _errorMessage; User? get user => _user; String? get token => _token; bool get isLoading => _isLoading; String? get errorMessage => _errorMessage; bool get isAuthenticated => _user != null && _token != null; // Méthode de connexion Future<bool> login(String email, String password) async { _isLoading = true; _errorMessage = null; notifyListeners(); try { final result = await AuthApi.login(email, password); if (result['success'] == true) { _user = User.fromJson(result['user']); _token = result['token'] ?? ''; _errorMessage = null; // Sauvegarder le token et les données utilisateur await _saveToken(_token!); await _saveUser(_user!); _isLoading = false; notifyListeners(); return true; } else { _errorMessage = result['message']; _isLoading = false; notifyListeners(); return false; } } catch (e) { _errorMessage = 'Erreur de connexion: $e'; _isLoading = false; notifyListeners(); return false; } } // Méthode d'inscription MODIFIÉE Future<bool> register({ required String email, required String password, String? nomComplet, String? pays, String? numTel, String? urlPhotoProfil, List<String>? roles, }) async { _isLoading = true; _errorMessage = null; notifyListeners(); try { // ✅ Forcer le rôle sélectionné ou ROLE_USER par défaut final List<String> finalRoles = (roles != null && roles.isNotEmpty) ? roles : ['ROLE_USER']; final data = await AuthApi.register( email: email, password: password, nomComplet: nomComplet, pays: pays, numTel: numTel, urlPhotoProfil: urlPhotoProfil, roles: finalRoles, // ✅ rôle garanti ); if (data['success'] == true) { _user = User.fromJson(data['user']); _token = data['token']; await _saveToken(_token!); await _saveUser(_user!); _isLoading = false; notifyListeners(); return true; } else { _errorMessage = data['message'] ?? "Erreur d'inscription"; _isLoading = false; notifyListeners(); return false; } } catch (e) { _errorMessage = e.toString(); _isLoading = false; notifyListeners(); return false; } } // Méthode de déconnexion Future<void> logout() async { _isLoading = true; notifyListeners(); try { if (_token != null && _token!.isNotEmpty) { try { await AuthApi.logout(_token!); print('✅ Déconnexion API réussie'); } catch (e) { print('⚠️ Erreur lors de la déconnexion API (ignorée): $e'); } } await _clearLocalData(); } catch (e) { print('⚠️ Erreur dans logout (nettoyage local): $e'); await _clearLocalData(); } finally { _isLoading = false; notifyListeners(); } } // Sauvegarder le token dans SharedPreferences Future<void> _saveToken(String token) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('auth_token', token); print('✅ Token sauvegardé'); } catch (e) { print('⚠️ Erreur lors de la sauvegarde du token: $e'); } } // Sauvegarder les données utilisateur (NOUVEAU) Future<void> _saveUser(User user) async { try { final prefs = await SharedPreferences.getInstance(); await prefs.setString('user_email', user.email); await prefs.setString('user_roles', user.roles.join(',')); if (user.nomComplet != null) { await prefs.setString('user_nom_complet', user.nomComplet!); } if (user.id != null) { await prefs.setInt('user_id', user.id!); } print('✅ Données utilisateur sauvegardées'); } catch (e) { print('⚠️ Erreur lors de la sauvegarde des données utilisateur: $e'); } } // Charger le token et l'utilisateur depuis SharedPreferences (MODIFIÉ) Future<void> loadToken() async { try { final prefs = await SharedPreferences.getInstance(); _token = prefs.getString('auth_token'); if (_token != null) { // Charger aussi les données utilisateur final userId = prefs.getInt('user_id'); final userEmail = prefs.getString('user_email'); final userRolesString = prefs.getString('user_roles'); final userNomComplet = prefs.getString('user_nom_complet'); if (userEmail != null && userRolesString != null) { _user = User( id: userId, email: userEmail, nomComplet: userNomComplet, roles: userRolesString.split(','), ); print('✅ Token et utilisateur chargés depuis le stockage'); } else { print('✅ Token chargé depuis le stockage'); } notifyListeners(); } } catch (e) { print('⚠️ Erreur lors du chargement du token: $e'); } } // Nettoyer les données locales (MODIFIÉ) Future<void> _clearLocalData() async { _user = null; _token = null; _errorMessage = null; try { final prefs = await SharedPreferences.getInstance(); await prefs.remove('auth_token'); await prefs.remove('user_id'); await prefs.remove('user_email'); await prefs.remove('user_roles'); await prefs.remove('user_nom_complet'); print('✅ Données locales nettoyées'); } catch (e) { print('⚠️ Erreur lors du nettoyage des données: $e'); } } // Forcer la déconnexion sans appeler l'API Future<void> forceLogout() async { await _clearLocalData(); notifyListeners(); } // Effacer le message d'erreur void clearError() { _errorMessage = null; notifyListeners(); } // Méthode pour simuler une connexion (pour tests) void mockLogin() { _user = User( id: 1, email: 'test@test.com', nomComplet: 'Test User', roles: ['ROLE_USER'], ); _token = 'mock_token_123456'; notifyListeners(); } // Vérifier si l'utilisateur a un rôle spécifique (NOUVEAU) bool hasRole(String role) { return _user?.hasRole(role) ?? false; } // Getters pour les rôles (NOUVEAU) bool get isAdmin => _user?.isAdmin ?? false; bool get isVendeur => _user?.isVendeur ?? false; bool get isClient => _user?.isClient ?? false; }Fichier : product_controller.dart
import 'package:flutter/material.dart'; import '../models/product_model.dart'; import '../services/product_api.dart'; class ProductController extends ChangeNotifier { final int idUtilisateur; ProductController(this.idUtilisateur); List<Product> _products = []; bool _isLoading = false; String? _errorMessage; Product? _selectedProduct; List<Product> get products => _products; bool get isLoading => _isLoading; String? get errorMessage => _errorMessage; Product? get selectedProduct => _selectedProduct; // Charger tous les produits Future<void> loadProductsByUtilisateur(int idUtilisateur) async { _isLoading = true; notifyListeners(); try { _products = await ProductApi.getProductsByUtilisateur(idUtilisateur); _isLoading = false; notifyListeners(); } catch (e) { _errorMessage = e.toString(); _isLoading = false; notifyListeners(); } } // Charger un produit par ID Future<void> loadProductById(int id) async { _isLoading = true; _errorMessage = null; notifyListeners(); try { _selectedProduct = await ProductApi.getProductById(id); _isLoading = false; notifyListeners(); } catch (e) { _errorMessage = e.toString(); _isLoading = false; notifyListeners(); } } // Ajouter un produit Future<bool> addProduct(Product product) async { _isLoading = true; _errorMessage = null; notifyListeners(); try { await ProductApi.addProduct(product); await loadProductsByUtilisateur(idUtilisateur); // Recharger la liste _isLoading = false; notifyListeners(); return true; } catch (e) { _errorMessage = e.toString(); _isLoading = false; notifyListeners(); return false; } } // Modifier un produit Future<bool> updateProduct(Product product) async { _isLoading = true; _errorMessage = null; notifyListeners(); try { await ProductApi.updateProduct(product); await loadProductsByUtilisateur(idUtilisateur); // ✅ _isLoading = false; notifyListeners(); return true; } catch (e) { _errorMessage = e.toString(); _isLoading = false; notifyListeners(); return false; } } // Supprimer un produit Future<bool> deleteProduct(int id) async { _isLoading = true; _errorMessage = null; notifyListeners(); try { await ProductApi.deleteProduct(id); await loadProductsByUtilisateur(idUtilisateur); // ✅ _isLoading = false; notifyListeners(); return true; } catch (e) { _errorMessage = e.toString(); _isLoading = false; notifyListeners(); return false; } } // Rechercher des produits List<Product> searchProducts(String query) { if (query.isEmpty) return _products; return _products.where((product) { return product.nom.toLowerCase().contains(query.toLowerCase()) || product.description.toLowerCase().contains(query.toLowerCase()); }).toList(); } // Filtrer par prix List<Product> filterByPrice(double minPrice, double maxPrice) { return _products.where((product) { return product.prix >= minPrice && product.prix <= maxPrice; }).toList(); } // Trier les produits void sortProducts(String sortBy) { switch (sortBy) { case 'nom_asc': _products.sort((a, b) => a.nom.compareTo(b.nom)); break; case 'nom_desc': _products.sort((a, b) => b.nom.compareTo(a.nom)); break; case 'prix_asc': _products.sort((a, b) => a.prix.compareTo(b.prix)); break; case 'prix_desc': _products.sort((a, b) => b.prix.compareTo(a.prix)); break; case 'date_asc': _products.sort((a, b) => (a.createdAt ?? DateTime.now()) .compareTo(b.createdAt ?? DateTime.now())); break; case 'date_desc': _products.sort((a, b) => (b.createdAt ?? DateTime.now()) .compareTo(a.createdAt ?? DateTime.now())); break; } notifyListeners(); } } -
Dossier 'Models'
Fichier : product_model.dart
class Product { final int? id; final String nom; final String description; final double prix; final int quantite; final String? imageUrl; int idUtilisateur; // vendeur final DateTime? createdAt; final DateTime? updatedAt; Product({ this.id, required this.nom, required this.description, required this.prix, this.quantite = 0, this.imageUrl, required this.idUtilisateur, this.createdAt, this.updatedAt, }); // Convertir JSON en objet Product factory Product.fromJson(Map<String, dynamic> json) { return Product( id: json['id'] != null ? int.parse(json['id'].toString()) : null, nom: json['nom'] ?? '', description: json['description'] ?? '', prix: json['prix'] != null ? double.parse(json['prix'].toString()) : 0.0, quantite: json['quantite'] != null ? int.parse(json['quantite'].toString()) : 0, imageUrl: json['image_url'], idUtilisateur: json['id_utilisateur'] ?? json['idUtilisateur'] ?? 0, createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null, updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']) : null, ); } // Convertir objet Product en JSON Map<String, dynamic> toJson() { return { 'id': id, 'nom': nom, 'description': description, 'prix': prix, 'quantite': quantite, 'image_url': imageUrl, 'id_utilisateur': idUtilisateur, // correspond à la colonne MySQL 'created_at': createdAt?.toIso8601String(), 'updated_at': updatedAt?.toIso8601String(), }; } // Copier avec modification Product copyWith({ int? id, String? nom, String? description, double? prix, int? quantite, String? imageUrl, int? idUtilisateur, DateTime? createdAt, DateTime? updatedAt, }) { return Product( id: id ?? this.id, nom: nom ?? this.nom, description: description ?? this.description, prix: prix ?? this.prix, quantite: quantite ?? this.quantite, imageUrl: imageUrl ?? this.imageUrl, idUtilisateur: idUtilisateur ?? this.idUtilisateur, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, ); } }Fichier : user_model.dart
// models/user_model.dart import 'dart:convert'; class User { final int? id; final String email; final String? nomComplet; final String? pays; final String? numTel; final String? urlPhotoProfil; final List<String> roles; // Liste des rôles final DateTime? creeA; final DateTime? misAJourA; User({ this.id, required this.email, this.nomComplet, this.pays, this.numTel, this.urlPhotoProfil, required this.roles, this.creeA, this.misAJourA, }); factory User.fromJson(Map<String, dynamic> json) { // Parser les rôles JSON List<String> rolesList = []; if (json['roles'] != null) { if (json['roles'] is String) { // Si c'est une string JSON try { rolesList = List<String>.from(jsonDecode(json['roles'])); } catch (e) { rolesList = ['ROLE_USER']; } } else if (json['roles'] is List) { // Si c'est déjà une liste rolesList = List<String>.from(json['roles']); } } return User( id: json['id'] != null ? int.parse(json['id'].toString()) : null, email: json['email'] ?? '', nomComplet: json['nom_complet'], pays: json['pays'], numTel: json['num_tel'], urlPhotoProfil: json['url_photo_profil'], roles: rolesList.isEmpty ? ['ROLE_USER'] : rolesList, creeA: json['cree_a'] != null ? DateTime.parse(json['cree_a']) : null, misAJourA: json['mis_a_jour_a'] != null ? DateTime.parse(json['mis_a_jour_a']) : null, ); } Map<String, dynamic> toJson() { return { 'id': id, 'email': email, 'nom_complet': nomComplet, 'pays': pays, 'num_tel': numTel, 'url_photo_profil': urlPhotoProfil, 'roles': roles, }; } // Méthodes pour vérifier les rôles bool hasRole(String role) => roles.contains(role); bool get isAdmin => hasRole('ROLE_ADMIN'); bool get isVendeur => hasRole('ROLE_VENDEUR'); bool get isClient => hasRole('ROLE_USER') || hasRole('ROLE_CLIENT'); // Obtenir le rôle principal (priorité) String get mainRole { if (isAdmin) return 'ROLE_ADMIN'; if (isVendeur) return 'ROLE_VENDEUR'; return 'ROLE_USER'; } } -
Dossier 'Services'
Fichier : api_base.dart
import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; class ApiBase { // URL de base corrigée static String baseUrl = 'http://10.0.2.2/flutter/mon_api_php'; // Méthode principale pour toutes les requêtes static Future<Map<String, dynamic>> request({ required String method, required String endpoint, Map<String, dynamic>? body, String? token, }) async { try { // Nettoyer l'endpoint (enlever les slashes en trop) String cleanEndpoint = endpoint.trim(); if (cleanEndpoint.startsWith('/')) { cleanEndpoint = cleanEndpoint.substring(1); } // Ajouter .php si pas présent et pas d'autre extension if (!cleanEndpoint.contains('.')) { cleanEndpoint = '$cleanEndpoint.php'; print('? Extension ajoutée: $cleanEndpoint'); } // Construire l'URL final url = Uri.parse('$baseUrl/$cleanEndpoint'); print('? Requête API: $method $url'); // Headers final headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }; // Log du body pour debug if (body != null) { print('? Body: ${json.encode(body)}'); } // Faire la requête avec timeout http.Response response; switch (method.toUpperCase()) { case 'GET': response = await http .get(url, headers: headers) .timeout(const Duration(seconds: 15)); break; case 'POST': response = await http .post( url, headers: headers, body: body != null ? json.encode(body) : null, ) .timeout(const Duration(seconds: 15)); break; case 'PUT': response = await http .put( url, headers: headers, body: body != null ? json.encode(body) : null, ) .timeout(const Duration(seconds: 15)); break; case 'DELETE': response = await http .delete(url, headers: headers) .timeout(const Duration(seconds: 15)); break; default: throw Exception('Méthode non supportée: $method'); } print('? Réponse: ${response.statusCode}'); print('? Body: ${response.body}'); // Parser la réponse return _parseResponse(response); } on SocketException { throw Exception('Pas de connexion internet. Vérifiez votre réseau.'); } on TimeoutException { throw Exception('Le serveur ne répond pas. Vérifiez que XAMPP est démarré.'); } on http.ClientException catch (e) { throw Exception('Erreur réseau: ${e.message}'); } on FormatException catch (e) { throw Exception('Format JSON invalide: $e'); } catch (e) { throw Exception('Erreur: $e'); } } // Parser la réponse static Map<String, dynamic> _parseResponse(http.Response response) { // Vérifier si c'est du HTML (erreur commune) if (response.body.trim().startsWith('<!DOCTYPE') || response.body.trim().startsWith('<html') || response.body.trim().startsWith('<?xml')) { throw Exception( 'Le serveur retourne du HTML au lieu de JSON. ' 'Vérifiez l\'URL et que le fichier PHP existe.' ); } // Vérifier si la réponse est vide if (response.body.isEmpty) { throw Exception('Réponse vide du serveur'); } try { final data = json.decode(response.body); // Gérer les différents codes de statut if (response.statusCode >= 200 && response.statusCode < 300) { return data; } else if (response.statusCode == 400) { throw Exception(data['message'] ?? 'Requête invalide'); } else if (response.statusCode == 401) { throw Exception(data['message'] ?? 'Non autorisé'); } else if (response.statusCode == 404) { throw Exception('API introuvable. Vérifiez l\'URL: $baseUrl'); } else if (response.statusCode == 406) { throw Exception( 'Erreur 406: Le serveur n\'accepte pas le format de la requête. ' 'Vérifiez les headers dans login.php' ); } else if (response.statusCode == 500) { throw Exception(data['message'] ?? 'Erreur serveur'); } else { throw Exception(data['message'] ?? 'Erreur ${response.statusCode}'); } } on FormatException { // Si le JSON est invalide, afficher un extrait du corps final preview = response.body.length > 200 ? '${response.body.substring(0, 200)}...' : response.body; throw Exception('Réponse invalide (JSON attendu): $preview'); } } // Tester la connexion à l'API static Future<bool> testConnection() async { print('? Test de connexion à $baseUrl'); try { // Créer un endpoint de test final testUrl = Uri.parse('$baseUrl/test.php'); print('? Test: GET $testUrl'); final response = await http .get(testUrl) .timeout(const Duration(seconds: 10)); print('? Réponse test: ${response.statusCode}'); print('? Body: ${response.body}'); if (response.statusCode == 200) { print('✅ Serveur accessible'); // Vérifier le contenu if (response.body.contains('success') || response.body.contains('status') || response.body.startsWith('{') || response.body.startsWith('[')) { print('✅ API semble fonctionner correctement'); return true; } else { print('⚠️ Contenu inattendu'); return false; } } else if (response.statusCode == 404) { print('❌ Fichier test.php introuvable'); print('? Créez un fichier test.php dans $baseUrl'); return false; } else { print('❌ Erreur HTTP: ${response.statusCode}'); return false; } } on SocketException { print('❌ Impossible de se connecter au serveur'); print('? Vérifications:'); print(' 1. XAMPP est démarré?'); print(' 2. Apache est en cours d\'exécution?'); print(' 3. Le dossier existe: C:\\xampp\\htdocs\\flutter\\mon_api_php\\'); return false; } on TimeoutException { print('❌ Timeout - Le serveur ne répond pas'); return false; } catch (e) { print('❌ Erreur: $e'); return false; } } // Méthodes pratiques pour chaque type de requête static Future<Map<String, dynamic>> get( String endpoint, { String? token, }) async { return request( method: 'GET', endpoint: endpoint, token: token, ); } static Future<Map<String, dynamic>> post( String endpoint, { Map<String, dynamic>? body, String? token, }) async { return request( method: 'POST', endpoint: endpoint, body: body, token: token, ); } static Future<Map<String, dynamic>> put( String endpoint, { Map<String, dynamic>? body, String? token, }) async { return request( method: 'PUT', endpoint: endpoint, body: body, token: token, ); } static Future<Map<String, dynamic>> delete( String endpoint, { String? token, }) async { return request( method: 'DELETE', endpoint: endpoint, token: token, ); } }Fichier : auth_api.dart
import 'api_base.dart'; class AuthApi { // Login static Future<Map<String, dynamic>> login( String email, String password) async { try { final response = await ApiBase.request( method: 'POST', endpoint: 'login', body: { 'email': email, 'password': password, }, ); return response; } catch (e) { return { 'success': false, 'message': 'Erreur de connexion: $e', }; } } // Register avec tous les champs de ta table static Future<Map<String, dynamic>> register({ required String email, required String password, String? nomComplet, String? pays, String? numTel, String? urlPhotoProfil, List<String>? roles, }) async { try { final response = await ApiBase.request( method: 'POST', endpoint: 'register', body: { 'email': email, 'password': password, if (nomComplet != null) 'nom_complet': nomComplet, if (pays != null) 'pays': pays, if (numTel != null) 'num_tel': numTel, if (urlPhotoProfil != null) 'url_photo_profil': urlPhotoProfil, 'roles': roles ?? ['ROLE_USER'], // Par défaut ROLE_USER }, ); return response; } catch (e) { return { 'success': false, 'message': 'Erreur d\'inscription: $e', }; } } // Logout static Future<Map<String, dynamic>> logout(String token) async { try { final response = await ApiBase.request( method: 'POST', endpoint: 'logout', token: token, ); return response; } catch (e) { // Pour logout, on ne throw pas - on retourne un succès simulé if (e.toString().contains('404') || e.toString().contains('Endpoint non trouvé')) { return { 'success': true, 'message': 'Déconnexion locale (endpoint non disponible)', }; } // Autres erreurs return { 'success': true, // On considère quand même que c'est un succès 'message': 'Déconnexion effectuée (erreur ignorée: $e)', }; } } // Méthode de test pour vérifier quels endpoints sont disponibles static Future<void> testEndpoints() async { print('? Test des endpoints d\'authentification'); final endpoints = ['login', 'logout', 'register']; for (var endpoint in endpoints) { print('\nTest endpoint: $endpoint'); try { final response = await ApiBase.request( method: endpoint == 'login' || endpoint == 'register' ? 'POST' : 'GET', endpoint: endpoint, body: endpoint == 'login' || endpoint == 'register' ? {'test': 'data'} : null, ); print(' ✅ Disponible: ${response['success']}'); } catch (e) { print(' ❌ Non disponible: $e'); } } } }Fichier : product_api.dart
import 'dart:convert'; import 'package:http/http.dart' as http; import '../models/product_model.dart'; import 'api_base.dart'; class ProductApi { // Récupérer tous les produits static Future<List<Product>> getAllProducts() async { try { final data = await ApiBase.get('produits/get_products.php'); if (data['success'] == true) { List<Product> products = (data['produits'] as List) .map((product) => Product.fromJson(product)) .toList(); return products; } else { throw Exception(data['error'] ?? 'Erreur lors de la récupération des produits'); } } catch (e) { throw Exception('Erreur: $e'); } } // Récupérer un produit par ID static Future<Product> getProductById(int id) async { try { final data = await ApiBase.get('produits/get_product.php?id=$id'); if (data['success'] == true) { return Product.fromJson(data['produit']); } else { throw Exception(data['error'] ?? 'Produit non trouvé'); } } catch (e) { throw Exception('Erreur: $e'); } } // Ajouter un produit static Future<Map<String, dynamic>> addProduct(Product product) async { try { final data = await ApiBase.post( 'produits/add_product.php', body: product.toJson(), ); if (data['success'] == true) { return data; } else { throw Exception(data['error'] ?? 'Erreur lors de l\'ajout du produit'); } } catch (e) { throw Exception('Erreur: $e'); } } // Modifier un produit static Future<Map<String, dynamic>> updateProduct(Product product) async { try { final data = await ApiBase.put( 'produits/update_product.php', body: product.toJson(), ); if (data['success'] == true) { return data; } else { throw Exception(data['error'] ?? 'Erreur lors de la modification du produit'); } } catch (e) { throw Exception('Erreur: $e'); } } // Supprimer un produit static Future<Map<String, dynamic>> deleteProduct(int id) async { try { final data = await ApiBase.delete('produits/delete_product.php?id=$id'); if (data['success'] == true) { return data; } else { throw Exception(data['error'] ?? 'Erreur lors de la suppression du produit'); } } catch (e) { throw Exception('Erreur: $e'); } } // Récupérer les produits d'un utilisateur static Future<List<Product>> getProductsByUtilisateur(int idUtilisateur) async { try { final data = await ApiBase.get( 'produits/get_products.php?id_utilisateur=$idUtilisateur', ); if (data['success'] == true) { return (data['produits'] as List) .map((p) => Product.fromJson(p)) .toList(); } else { throw Exception(data['error'] ?? 'Erreur lors de la récupération des produits'); } } catch (e) { throw Exception('Erreur: $e'); } } } -
Dossier 'Views'
Fichier : login_page.dart
Fichier : admin_dashboard.dart
import 'package:flutter/material.dart'; import '../models/user_model.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; import 'login_page.dart'; class AdminDashboard extends StatelessWidget { final User user; const AdminDashboard({super.key, required this.user}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Dashboard Admin'), backgroundColor: Colors.red, foregroundColor: Colors.white, actions: [ IconButton( icon: const Icon(Icons.logout), onPressed: () async { await context.read<AuthController>().logout(); if (context.mounted) { Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => const LoginPage()), (route) => false, ); } }, ), ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.red.shade700, Colors.red.shade500], ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.red.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Row( children: [ const CircleAvatar( radius: 30, backgroundColor: Colors.white, child: Icon(Icons.admin_panel_settings, size: 35, color: Colors.red), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.nomComplet ?? user.email, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 4), Text( 'Administrateur', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.9), ), ), ], ), ), ], ), ), const SizedBox(height: 32), // Grille de cartes Expanded( child: GridView.count( crossAxisCount: 2, crossAxisSpacing: 16, mainAxisSpacing: 16, children: [ _DashboardCard( title: 'Gestion\nUtilisateurs', icon: Icons.people, color: Colors.blue, onTap: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Gestion Utilisateurs - À venir')), ); }, ), _DashboardCard( title: 'Gestion\nProduits', icon: Icons.inventory, color: Colors.green, onTap: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Gestion Produits - À venir')), ); }, ), _DashboardCard( title: 'Statistiques', icon: Icons.bar_chart, color: Colors.orange, onTap: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Statistiques - À venir')), ); }, ), _DashboardCard( title: 'Validation\nInscriptions', icon: Icons.check_circle, color: Colors.purple, onTap: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Validation - À venir')), ); }, ), ], ), ), ], ), ), ); } } class _DashboardCard extends StatelessWidget { final String title; final IconData icon; final Color color; final VoidCallback onTap; const _DashboardCard({ required this.title, required this.icon, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ color.withOpacity(0.1), color.withOpacity(0.05), ], ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 48, color: color), const SizedBox(height: 12), Text( title, textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: color, ), ), ], ), ), ), ); } }Fichier : client_dashboard.dart
import 'package:flutter/material.dart'; import '../models/user_model.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; import 'login_page.dart'; class ClientDashboard extends StatelessWidget { final User user; const ClientDashboard({super.key, required this.user}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Catalogue Produits'), backgroundColor: Colors.blue, foregroundColor: Colors.white, actions: [ IconButton( icon: const Icon(Icons.shopping_cart), onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Panier - À venir')), ); }, ), IconButton( icon: const Icon(Icons.logout), onPressed: () async { await context.read<AuthController>().logout(); if (context.mounted) { Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => const LoginPage()), (route) => false, ); } }, ), ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue.shade700, Colors.blue.shade500], ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.blue.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Row( children: [ const CircleAvatar( radius: 30, backgroundColor: Colors.white, child: Icon(Icons.shopping_bag, size: 35, color: Colors.blue), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.nomComplet ?? user.email, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 4), Text( 'Client', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.9), ), ), ], ), ), ], ), ), const SizedBox(height: 32), // Section recherche TextField( decoration: InputDecoration( hintText: 'Rechercher un produit...', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), filled: true, fillColor: Colors.grey.shade100, ), ), const SizedBox(height: 24), // Liste des produits (vide pour le moment) const Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.shopping_bag_outlined, size: 80, color: Colors.grey), SizedBox(height: 16), Text( 'Aucun produit disponible', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.grey, ), ), SizedBox(height: 8), Text( 'Les produits apparaîtront ici', style: TextStyle(color: Colors.grey), ), ], ), ), ), ], ), ), floatingActionButton: FloatingActionButton.extended( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Filtres - À venir')), ); }, icon: const Icon(Icons.filter_list), label: const Text('Filtres'), backgroundColor: Colors.blue, ), ); } }Fichier : login_page.dart
Fichier : login_page.dart
import 'package:exemple01/views/register_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../controllers/auth_controller.dart'; import 'dashboard_router.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin { final emailController = TextEditingController(); final passwordController = TextEditingController(); final _formKey = GlobalKey<FormState>(); bool _obscurePassword = true; late AnimationController _animationController; late Animation<double> _opacityAnimation; late Animation<double> _scaleAnimation; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1000), ); _opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.6, curve: Curves.easeIn), ), ); _scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate( CurvedAnimation( parent: _animationController, curve: const Interval(0.2, 1.0, curve: Curves.elasticOut), ), ); _animationController.forward(); } @override void dispose() { emailController.dispose(); passwordController.dispose(); _animationController.dispose(); super.dispose(); } void _showComingSoon(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Fonctionnalité à venir...'), ), ); } Widget _buildSocialButton({ required IconData icon, required Color color, required VoidCallback onPressed, }) { return Container( width: 56, height: 56, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: IconButton( icon: Icon(icon, color: color, size: 28), onPressed: onPressed, ), ); } @override Widget build(BuildContext context) { final auth = Provider.of<AuthController>(context); final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final screenWidth = MediaQuery.of(context).size.width; return Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: isDark ? [ Colors.deepPurple.shade900, Colors.purple.shade800, Colors.indigo.shade900, ] : [ Colors.purple.shade50, Colors.blue.shade50, Colors.white, ], ), ), child: SafeArea( child: SingleChildScrollView( child: AnimatedBuilder( animation: _animationController, builder: (context, child) { return Opacity( opacity: _opacityAnimation.value, child: Transform.scale( scale: _scaleAnimation.value, child: Padding( padding: EdgeInsets.symmetric( horizontal: screenWidth > 600 ? 100 : 24, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 20), // Logo/Titre Center( child: Column( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.purpleAccent, Colors.blueAccent, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.purple.withOpacity(0.3), blurRadius: 15, spreadRadius: 2, offset: const Offset(0, 4), ), ], ), child: const Icon( Icons.lock_outline_rounded, size: 40, color: Colors.white, ), ), const SizedBox(height: 20), Text( 'Bienvenue', style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.purple.shade900, letterSpacing: 1.2, ), ), const SizedBox(height: 8), Text( 'Connectez-vous à votre compte', style: TextStyle( fontSize: 16, color: isDark ? Colors.grey.shade300 : Colors.grey.shade700, ), ), ], ), ), const SizedBox(height: 32), // Formulaire Form( key: _formKey, child: Column( children: [ // Champ Email Container( margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: TextFormField( controller: emailController, keyboardType: TextInputType.emailAddress, validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre email'; } if (!value.contains('@')) { return 'Email invalide'; } return null; }, decoration: InputDecoration( labelText: 'Email', labelStyle: TextStyle( color: isDark ? Colors.grey.shade400 : Colors.grey.shade600, fontWeight: FontWeight.w500, ), prefixIcon: Container( margin: const EdgeInsets.only( right: 12, left: 4), decoration: BoxDecoration( border: Border( right: BorderSide( color: isDark ? Colors.grey.shade700 : Colors.grey.shade300, width: 1.5, ), ), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16), child: Icon( Icons.email_outlined, color: Colors.purple.shade400, ), ), ), filled: true, fillColor: isDark ? Colors.grey.shade900 : Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide( color: isDark ? Colors.grey.shade800 : Colors.grey.shade300, width: 1.5, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide( color: Colors.purple.shade400, width: 2, ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 20, vertical: 18, ), ), ), ), // Champ Mot de passe Container( margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: TextFormField( controller: passwordController, obscureText: _obscurePassword, validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre mot de passe'; } if (value.length < 6) { return 'Le mot de passe doit contenir au moins 6 caractères'; } return null; }, decoration: InputDecoration( labelText: 'Mot de passe', labelStyle: TextStyle( color: isDark ? Colors.grey.shade400 : Colors.grey.shade600, fontWeight: FontWeight.w500, ), prefixIcon: Container( margin: const EdgeInsets.only( right: 12, left: 4), decoration: BoxDecoration( border: Border( right: BorderSide( color: isDark ? Colors.grey.shade700 : Colors.grey.shade300, width: 1.5, ), ), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16), child: Icon( Icons.lock_outline_rounded, color: Colors.purple.shade400, ), ), ), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.grey.shade500, ), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, ), filled: true, fillColor: isDark ? Colors.grey.shade900 : Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide( color: isDark ? Colors.grey.shade800 : Colors.grey.shade300, width: 1.5, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide( color: Colors.purple.shade400, width: 2, ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 20, vertical: 18, ), ), ), ), // Mot de passe oublié Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () { _showComingSoon(context); }, child: Text( 'Mot de passe oublié ?', style: TextStyle( color: Colors.purple.shade500, fontWeight: FontWeight.w500, ), ), ), ), const SizedBox(height: 16), // Message d'erreur if (auth.errorMessage != null) AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Container( key: ValueKey(auth.errorMessage), width: double.infinity, padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(bottom: 20), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.red.shade200, width: 1, ), ), child: Row( children: [ Icon( Icons.error_outline, color: Colors.red.shade600, ), const SizedBox(width: 12), Expanded( child: Text( auth.errorMessage!, style: TextStyle( color: Colors.red.shade700, fontWeight: FontWeight.w500, ), ), ), IconButton( icon: Icon( Icons.close, color: Colors.red.shade600, size: 20, ), onPressed: () => auth.clearError(), ), ], ), ), ), // Bouton de connexion - MODIFIÉ ICI ⬇️ SizedBox( width: double.infinity, height: 56, child: ElevatedButton( onPressed: auth.isLoading ? null : () async { if (_formKey.currentState!.validate()) { final success = await auth.login( emailController.text.trim(), passwordController.text.trim(), ); if (success && mounted) { // Redirection vers le dashboard approprié selon le rôle Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => DashboardRouter( user: auth.user!, ), ), ); } } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.purple.shade500, foregroundColor: Colors.white, elevation: 6, shadowColor: Colors.purple.withOpacity(0.3), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), padding: const EdgeInsets.symmetric( vertical: 16), ), child: auth.isLoading ? SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation<Color>( Colors.white, ), ), ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Se connecter', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), const SizedBox(width: 10), const Icon( Icons.arrow_forward_rounded, size: 20, ), ], ), ), ), const SizedBox(height: 20), // Séparateur Row( children: [ Expanded( child: Divider( color: isDark ? Colors.grey.shade800 : Colors.grey.shade300, thickness: 1, ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 16), child: Text( 'Ou continuer avec', style: TextStyle( color: isDark ? Colors.grey.shade500 : Colors.grey.shade600, fontSize: 14, ), ), ), Expanded( child: Divider( color: isDark ? Colors.grey.shade800 : Colors.grey.shade300, thickness: 1, ), ), ], ), const SizedBox(height: 16), // Boutons de connexion sociale Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildSocialButton( icon: Icons.facebook, color: const Color(0xFF1877F2), onPressed: () { _showComingSoon(context); }, ), const SizedBox(width: 20), _buildSocialButton( icon: Icons.g_mobiledata, color: const Color(0xFFDB4437), onPressed: () { _showComingSoon(context); }, ), const SizedBox(width: 20), _buildSocialButton( icon: Icons.apple, color: Colors.black, onPressed: () { _showComingSoon(context); }, ), ], ), const SizedBox(height: 24), // Lien vers inscription Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Pas encore de compte ? ', style: TextStyle( color: isDark ? Colors.grey.shade400 : Colors.grey.shade600, fontSize: 15, ), ), GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => const RegisterPage()), ); }, child: Text( 'S\'inscrire', style: TextStyle( color: Colors.purple.shade500, fontWeight: FontWeight.bold, fontSize: 15, ), ), ), ], ), ), const SizedBox(height: 20), ], ), ), ], ), ), ), ); }, ), ), ), ), ); } } - Description : Ce fichier gère l’interface et la logique de connexion utilisateur.
- L’utilisateur saisit son email et mot de passe.
- L’application envoie une requête POST au script PHP login.php.
- Si les identifiants sont corrects, l’utilisateur est redirigé vers la HomePage.
- En cas d’erreur, un message d’alerte s’affiche (ex. identifiants invalides).
Fichier : home.dart
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'client_model.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final _nameCtrl = TextEditingController(); final _familyCtrl = TextEditingController(); final _ageCtrl = TextEditingController(); String serverMsg = ''; final List<Client> _clientsList = []; final String baseUrl = "http://10.0.2.2/flutter/mon_api_php/client"; // Utilisation de 127.0.0.1 pour fonctionner sur navigateur Future<void> _postRequest() async { try { final response = await http.post( Uri.parse("$baseUrl/insert.php"), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'name': _nameCtrl.text, 'family': _familyCtrl.text, 'age': int.tryParse(_ageCtrl.text) ?? 0, }), ); _clearForm(); if (response.statusCode == 200) { serverMsg = "Data Successfully Inserted"; _getRequest(); } else { serverMsg = "Insert Failed"; } } catch (e) { serverMsg = e.toString(); } setState(() {}); } Future<void> _getRequest() async { try { final response = await http.get(Uri.parse("$baseUrl/get.php")); if (response.statusCode == 200) { final List jsonData = jsonDecode(response.body); _clientsList.clear(); _clientsList.addAll(jsonData.map((e) => Client.fromJson(e))); serverMsg = "Data Loaded"; } else { serverMsg = "Failed to Load"; } } catch (e) { serverMsg = e.toString(); } setState(() {}); } Future<void> _deleteRequest(int id) async { try { final response = await http.post( Uri.parse("$baseUrl/delete.php"), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'id': id}), ); if (response.statusCode == 200) { serverMsg = "Deleted Successfully"; _getRequest(); } else { serverMsg = "Delete Failed"; } } catch (e) { serverMsg = e.toString(); } setState(() {}); } Future<void> _updateRequest(Client client) async { try { final response = await http.post( Uri.parse("$baseUrl/update.php"), headers: {'Content-Type': 'application/json'}, body: jsonEncode(client.toJson()), ); if (response.statusCode == 200) { serverMsg = "Updated Successfully"; _getRequest(); } else { serverMsg = "Update Failed"; } } catch (e) { serverMsg = e.toString(); } setState(() {}); } void _clearForm() { _nameCtrl.clear(); _familyCtrl.clear(); _ageCtrl.clear(); } @override void initState() { super.initState(); _getRequest(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("CRUD Example")), body: SafeArea( child: Column( children: [ Container( height: 300, color: Colors.grey[200], padding: const EdgeInsets.all(20), child: Column( children: [ TextField( controller: _nameCtrl, decoration: const InputDecoration(labelText: 'Name'), ), TextField( controller: _familyCtrl, decoration: const InputDecoration(labelText: 'Family'), ), TextField( controller: _ageCtrl, decoration: const InputDecoration(labelText: 'Age'), keyboardType: TextInputType.number, ), const SizedBox(height: 16), Text( serverMsg, style: const TextStyle(color: Colors.blue), ), ], ), ), Expanded( child: ListView.builder( itemCount: _clientsList.length, itemBuilder: (context, index) { final client = _clientsList[index]; return ListTile( title: Text("${client.name} ${client.family}"), subtitle: Text("Age: ${client.age}"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit, color: Colors.green), onPressed: () { _nameCtrl.text = client.name; _familyCtrl.text = client.family; _ageCtrl.text = client.age.toString(); // Show dialog to update client showDialog( context: context, builder: (_) { return AlertDialog( title: Text("Modifier le client"), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _nameCtrl, decoration: const InputDecoration(labelText: "Name"), ), TextField( controller: _familyCtrl, decoration: const InputDecoration(labelText: "Family"), ), TextField( controller: _ageCtrl, decoration: const InputDecoration(labelText: "Age"), ), ], ), actions: [ TextButton( child: const Text("Annuler"), onPressed: () => Navigator.pop(context), ), ElevatedButton( child: const Text("Mettre à jour"), onPressed: () { _updateRequest(Client( id: client.id, name: _nameCtrl.text, family: _familyCtrl.text, age: int.tryParse(_ageCtrl.text) ?? 0, )); Navigator.pop(context); }, ), ], ); }, ); }, ), IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () { _deleteRequest(client.id); }, ), ], ), ); }, ), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _postRequest, child: const Icon(Icons.add), ), ); } } - Description : Ce fichier gère la page principale après connexion.
- Il affiche une liste ou un tableau de données récupérées depuis la base MySQL via les scripts PHP (GET/POST/CRUD).
- Il permet de naviguer vers d’autres fonctionnalités (ajout, modification, suppression).
- Il contient la logique pour actualiser l’interface après les opérations CRUD.
Étape 5 : Définir un modèle de données (Client)
- Description : Cette classe représente un objet
Client. Elle fournit une méthodefromJsonpour transformer une réponse JSON en objet Dart, et une méthodetoJsonpour envoyer les données dans le bon format au serveur.
Fichier :
client_model.dartclass Client { final int id; final String name; final String family; final int age; Client({ required this.id, required this.name, required this.family, required this.age, }); factory Client.fromJson(Map<String, dynamic> json) { return Client( id: int.parse(json['id'].toString()), name: json['name'], family: json['family'], age: int.parse(json['age'].toString()), ); } Map<String, dynamic> toJson() { return {'id': id, 'name': name, 'family': family, 'age': age}; } }- Description : Cette classe représente un objet
CRUD (client)
Créer la table
client:CREATE TABLE `client` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `family` varchar(100) NOT NULL, `age` int NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 ;Étape 1 : Créer le fichier
get_client.php- Ce fichier récupère la liste des clients depuis la base de données au format JSON. Il est utilisé par l'application Flutter pour afficher les données clients.
Fichier get_client.php
<?php header("Access-Control-Allow-Origin: *"); header("Content-Type: application/json"); require_once('../dbconnect.php'); $stmt = $pdo->query("SELECT * FROM client ORDER BY id DESC"); $clients = $stmt->fetchAll(PDO::FETCH_ASSOC); echo json_encode($clients);Étape 2 : Créer le fichier
insert_client.php- Ce fichier permet d’ajouter un nouveau client à la base de données. Il reçoit les données (nom, email, etc.) en POST depuis l’application Flutter.
Fichier insert_client.php
<?php // Autorisations CORS header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: POST, OPTIONS"); header("Access-Control-Allow-Headers: Content-Type"); header("Content-Type: application/json"); // Gestion de la requête OPTIONS (pré-vol CORS) if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } require_once('../dbconnect.php'); // Lecture des données JSON envoyées $data = json_decode(file_get_contents("php://input")); // Sécurité basique : initialisation $name = $data->name ?? ''; $family = $data->family ?? ''; $age = $data->age ?? 0; // Vérification des champs requis if (empty($name) || empty($family)) { echo json_encode(['success' => false, 'message' => 'Champs manquants']); exit; } // Requête d'insertion préparée $stmt = $pdo->prepare("INSERT INTO client (name, family, age) VALUES (?, ?, ?)"); $success = $stmt->execute([$name, $family, $age]); // Réponse JSON echo json_encode([ 'success' => $success, 'message' => $success ? 'Ajouté avec succès' : 'Erreur insertion' ]);Étape 3 : Créer le fichier
update_client.php- Ce fichier met à jour les informations d’un client existant dans la base de données. L'application Flutter envoie l’identifiant du client et les nouvelles données via POST.
Fichier: update_client.php
<?php // Autorisations CORS header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: POST, OPTIONS"); header("Access-Control-Allow-Headers: Content-Type"); header("Content-Type: application/json"); // Gestion de la requête préliminaire OPTIONS (pré-vol CORS) if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } require_once('../dbconnect.php'); // Lecture des données JSON envoyées $data = json_decode(file_get_contents("php://input")); $id = $data->id ?? 0; $name = $data->name ?? ''; $family = $data->family ?? ''; $age = $data->age ?? 0; // Vérification de l'ID if ($id <= 0) { echo json_encode(['success' => false, 'message' => 'ID invalide']); exit; } // Requête SQL préparée $stmt = $pdo->prepare("UPDATE client SET name = ?, family = ?, age = ? WHERE id = ?"); $success = $stmt->execute([$name, $family, $age, $id]); // Réponse echo json_encode([ 'success' => $success, 'message' => $success ? 'Mis à jour avec succès' : 'Échec de la mise à jour' ]);Étape 4 : Créer le fichier
delete_client.php- Ce fichier supprime un client de la base de données. L'application Flutter envoie l’identifiant du client à supprimer via une requête POST.
Fichier :delete_client.php
<?php header("Access-Control-Allow-Origin: *"); header("Content-Type: application/json"); require_once('../dbconnect.php'); $data = json_decode(file_get_contents("php://input")); $id = $data->id ?? 0; if ($id <= 0) { echo json_encode(['success' => false, 'message' => 'ID invalide']); exit; } $stmt = $pdo->prepare("DELETE FROM client WHERE id = ?"); $success = $stmt->execute([$id]); echo json_encode([ 'success' => $success, 'message' => $success ? 'Supprimé avec succès' : 'Erreur de suppression' ]);Étape 5 : Créer le fichier
home_page.dartFichier: home_page.dart
import 'dart:convert'; import 'package:exemple01/login_page.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'client_model.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final _nameCtrl = TextEditingController(); final _familyCtrl = TextEditingController(); final _ageCtrl = TextEditingController(); String serverMsg = ''; final List<Client> _clientsList = []; final String baseUrl = "http://10.0.2.2/flutter/mon_api_php/client"; // ----------- INSERT ------------ Future<void> _postRequest() async { try { final response = await http.post( Uri.parse("$baseUrl/insert.php"), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'name': _nameCtrl.text, 'family': _familyCtrl.text, 'age': int.tryParse(_ageCtrl.text) ?? 0, }), ); _clearForm(); serverMsg = response.statusCode == 200 ? "Client ajouté" : "Échec d'ajout"; await _getRequest(); setState(() {}); } catch (e) { serverMsg = e.toString(); setState(() {}); } } // ----------- LOAD LIST ------------ Future<void> _getRequest() async { try { final response = await http.get(Uri.parse("$baseUrl/get.php")); if (response.statusCode == 200) { final List jsonData = jsonDecode(response.body); _clientsList.clear(); _clientsList.addAll(jsonData.map((e) => Client.fromJson(e))); serverMsg = "Données chargées"; } else { serverMsg = "Erreur de chargement"; } } catch (e) { serverMsg = e.toString(); } setState(() {}); } // ----------- DELETE ------------ Future<void> _deleteRequest(int id) async { try { final response = await http.post( Uri.parse("$baseUrl/delete.php"), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'id': id}), ); serverMsg = response.statusCode == 200 ? "Supprimé" : "Échec de suppression"; await _getRequest(); setState(() {}); } catch (e) { serverMsg = e.toString(); setState(() {}); } } // ----------- UPDATE ------------ Future<void> _updateRequest(Client client) async { try { final response = await http.post( Uri.parse("$baseUrl/update.php"), headers: {'Content-Type': 'application/json'}, body: jsonEncode(client.toJson()), ); serverMsg = response.statusCode == 200 ? "Modifié" : "Échec de modification"; await _getRequest(); setState(() {}); } catch (e) { serverMsg = e.toString(); setState(() {}); } } // ----------- CLEAR FORM ------------ void _clearForm() { _nameCtrl.clear(); _familyCtrl.clear(); _ageCtrl.clear(); } @override void initState() { super.initState(); _getRequest(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], // ──────────────────── APPBAR ───────────────────── appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => LoginPage()), ); }, ), title: const Text("Gestion des Clients"), backgroundColor: Colors.indigo[600], actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { _clearForm(); _showAddDialog(); }, ) ], ), // ──────────────────── BODY : LIST ONLY ───────────────────── body: _buildClientList(), // --- Bouton flottant en bas à droite --- floatingActionButton: FloatingActionButton( onPressed: () { _clearForm(); _showAddDialog(); }, backgroundColor: Colors.indigo, child: const Icon(Icons.add), ), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } // ──────────────────── LIST ───────────────────── Widget _buildClientList() { return ListView.builder( padding: const EdgeInsets.all(12), itemCount: _clientsList.length, itemBuilder: (context, index) { final client = _clientsList[index]; return Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), margin: const EdgeInsets.symmetric(vertical: 8), elevation: 3, child: ListTile( title: Text( "${client.name} ${client.family}", style: const TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text("Âge : ${client.age}"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit, color: Colors.indigo), onPressed: () { _nameCtrl.text = client.name; _familyCtrl.text = client.family; _ageCtrl.text = client.age.toString(); _showUpdateDialog(client); }, ), IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _deleteRequest(client.id), ), ], ), ), ); }, ); } // ──────────────────── POPUP AJOUT ───────────────────── void _showAddDialog() { showDialog( context: context, builder: (_) { return AlertDialog( title: const Text("Ajouter un client"), content: Column( mainAxisSize: MainAxisSize.min, children: [ _textField(_nameCtrl, "Nom", Icons.person), const SizedBox(height: 8), _textField(_familyCtrl, "Prénom", Icons.family_restroom), const SizedBox(height: 8), _textField(_ageCtrl, "Âge", Icons.cake, keyboard: TextInputType.number), ], ), actions: [ TextButton( child: const Text("Annuler"), onPressed: () => Navigator.pop(context), ), ElevatedButton( child: const Text("Ajouter"), onPressed: () async { await _postRequest(); Navigator.pop(context); }, ), ], ); }, ); } // ──────────────────── POPUP MODIFICATION ───────────────────── void _showUpdateDialog(Client client) { showDialog( context: context, builder: (_) { return AlertDialog( title: const Text("Modifier le client"), content: Column( mainAxisSize: MainAxisSize.min, children: [ _textField(_nameCtrl, "Nom", Icons.person), const SizedBox(height: 8), _textField(_familyCtrl, "Prénom", Icons.family_restroom), const SizedBox(height: 8), _textField(_ageCtrl, "Âge", Icons.cake, keyboard: TextInputType.number), ], ), actions: [ TextButton( child: const Text("Annuler"), onPressed: () => Navigator.pop(context), ), ElevatedButton( child: const Text("Mettre à jour"), onPressed: () async { await _updateRequest( Client( id: client.id, name: _nameCtrl.text, family: _familyCtrl.text, age: int.tryParse(_ageCtrl.text) ?? 0, ), ); Navigator.pop(context); }, ), ], ); }, ); } // ──────────────────── TEXT FIELD ───────────────────── Widget _textField(TextEditingController controller, String label, IconData icon, {TextInputType keyboard = TextInputType.text}) { return TextField( controller: controller, keyboardType: keyboard, decoration: InputDecoration( labelText: label, prefixIcon: Icon(icon), filled: true, fillColor: Colors.grey[50], border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), ); } }
Exercices
- Exercice 1 : Créer une page d’inscription Flutter + script PHP
register.php. - Exercice 2 : Ajouter le hachage de mot de passe avec
password_hash()côté PHP. - Exercice 3 : Créer une page de profil affichant l’email après connexion.
- Exercice 1 : Créer une page d’inscription Flutter + script PHP
Fichiers réalisés
- connexion.php
- login.php
- main.dart
- pubspec.yaml
- Base de données MySQL : base_api_php / table users