Se connecter à Flutter avec PHP MySQL
Se connecter à Flutter avec PHP MySQL
-
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.
-
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.
-
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 : - 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.
-
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
-
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: -
Étape 2 : Créer les scripts PHP
- Ajouter un utilisateur de test en exécutant le fichier add_user.php:
-
Étape 3 : Créer le projet Flutter
- Créer le projet :
- Ajouter la dépendance HTTP dans
pubspec.yaml: -
Étape 4 : Créer l’interface de connexion Flutter
- Description : Ce fichier est le point d’entrée de votre application Flutter. Il lance le widget
MainAppqui affiche directement la page de connexionLoginPage. - 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).
- 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. -
CRUD (client)
-
Créer la table
client: -
É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.
-
É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.
-
É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.
-
É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.
-
Étape 5 : Créer le fichier
home_page.dart -
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.
-
Fichiers réalisés
- connexion.php
- login.php
- main.dart
- pubspec.yaml
- Base de données MySQL : base_test / table users
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json");| 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() |
CREATE DATABASE base_test;
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 config.php :
<?php
define('DB_HOST', '127.0.0.1'); // l'hôte de la base de données MySQL
define('DB_NAME', 'base_test'); // 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 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é.";
?>
flutter create exemple01_php
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
Fichier : main.dart
import 'package:flutter/material.dart';
import 'login_page.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: LoginPage(),
);
}
}
Fichier : login_page.dart
import 'dart:convert';
import 'package:exemple01/home_page.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _emailCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
String errorMsg = '';
final String serverUrl = "http://localhost/flutter/mon_api_php/login.php";
Future<void> _login() async {
try {
final response = await http.post(
Uri.parse(serverUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': _emailCtrl.text,
'password': _passwordCtrl.text,
}),
);
print('Statut: ${response.statusCode}');
print('Corps: ${response.body}');
final data = jsonDecode(response.body);
if (data['success'] == true) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
} else {
setState(() {
errorMsg = data['message'];
});
}
} catch (e) {
print('Erreur: $e');
setState(() {
errorMsg = 'Erreur de connexion au serveur.';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Login")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(controller: _emailCtrl, decoration: const InputDecoration(labelText: "Email")),
TextField(controller: _passwordCtrl, decoration: const InputDecoration(labelText: "Password"), obscureText: true),
const SizedBox(height: 16),
ElevatedButton(onPressed: _login, child: const Text("Login")),
if (errorMsg.isNotEmpty) Text(errorMsg, style: const TextStyle(color: Colors.red)),
],
),
),
);
}
}
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),
),
);
}
}
Fichier : client_model.dart
class 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};
}
}
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 ;
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);
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'
]);
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'
]);
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'
]);
Fichier: 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),
),
),
);
}
}
