Se connecter à Flutter avec Node js MySQL
[sommaire]
Se connecter à Flutter avec Node js MySQL
-
Objectif
- À la fin de ce tutoriel, vous serez capable de :
- Créer une base de données MySQL simple pour l’authentification.
- Écrire un backend Node.js pour la connexion à MySQL.
- Échanger des données entre Flutter et Node.js via HTTP.
- Créer une interface de connexion Flutter avec vérification côté serveur.
-
Présentation
- Ce tutoriel explique comment connecter une application Flutter à une base de données MySQL à l’aide d’un backend Node.js. Flutter ne peut pas accéder directement à MySQL, d’où l’importance de l’API Node.js.
-
Gestion des projets et dépendances
-
Configuration du projet
-
Étape 1 : Créer la base de données MySQL
- Créer la base de données :
- Créer la table
utilisateurs: - Créer la table
client: -
Étape 2 : Créer le dossier du projet
-
Créer le dossier du projet
- Clique droit dans le dossier où tu veux travailler → Nouveau dossier
- Nomme-le par exemple : flutter_nodeJS
- Clique droit sur ce dossier → Ouvrir avec Terminal (ou PowerShell ou Invite de commandes)
-
Initialiser le projet Node.js
- Dans le terminal ouvert dans le dossier flutter_nodeJS, tape :
npm init -y - Normalement, tu dois taper :
npm init… et répondre à plusieurs questions : nom du projet, version, description, auteur etc. - Avec
npm init -y, tout est automatique ! - Le -y signifie « yes » à toutes les questions.
- Cela crée un fichier package.json.
-
Installer les modules nécessaires
- Toujours dans le terminal :
npm install express mysql2 bcrypt body-parser - Cette commande installe quatre modules Node.js nécessaires pour développer un serveur avec Node.js.
- 1. express
- C’est le framework le plus utilisé pour créer des serveurs web avec Node.js.
- Il permet de gérer les routes, les requêtes HTTP, les réponses, etc.
- 2. mysql2
- C’est un module permettant de connecter Node.js à une base de données MySQL.
- Plus rapide et moderne que le module mysql.
- Il permet d’exécuter des requêtes SQL depuis votre serveur Node.js.
- 3. bcrypt
- Sert à chiffrer (hasher) les mots de passe avant de les enregistrer dans votre base de données.
- Très important pour la sécurité.
- 4. body-parser
- Permet à Express de lire les données envoyées par un formulaire ou par POST.
- Aujourd’hui, Express intègre déjà une partie de body-parser, mais le module reste utilisé.
-
Étape 3 : Créer les fichiers un par un
-
Créer le fichier
config.js - Dans ton dossier flutter_nodeJS créer le fichier config.js :
- Dans Node.js,
module.exportspermet d’envoyer des données (ou fonctions) vers d’autres fichiers. - Cela signifie : »Rendre ces valeurs disponibles pour être utilisées ailleurs dans mon projet. »
-
Créer le fichier
db.js: - Ce code permet d’établir une connexion à une base de données MySQL en utilisant la bibliothèque mysql2 dans un projet Node.js.
- On importe la bibliothèque mysql2 qui fournit des fonctions pour interagir avec une base de données MySQL.
- On importe un fichier de configuration local config.js (ou .json) qui contient les paramètres de connexion à la base de données (hôte, utilisateur, mot de passe, etc.).
- On crée un objet connection qui représente la connexion à la base de données MySQL.
- Les paramètres utilisés sont :
- host: l’adresse du serveur MySQL (ex : localhost ou une IP)
- user: le nom d’utilisateur MySQL
- password: le mot de passe associé
- database: le nom de la base de données à utiliser
- charset: le jeu de caractères (ex : utf8mb4)
- Ici, on lance la connexion vers MySQL.
- Si la connexion échoue, on affiche l’erreur dans la console et on quitte le processus Node.js (process.exit(1)).
- Sinon, on affiche un message de succès.
- On exporte l’objet connection pour pouvoir le réutiliser dans d’autres fichiers du projet (par exemple, pour exécuter des requêtes SQL).
-
Créer le fichier
login.js: - Ce code crée un serveur web avec Express qui écoute sur le port 3000 et propose un endpoint POST /login permettant à un utilisateur de se connecter via email et mot de passe. Les mots de passe sont sécurisés avec bcrypt.
- express pour créer le serveur HTTP.
- db est un module local qui gère la connexion à la base de données (MySQL).
- bcrypt pour comparer les mots de passe hashés.
- body-parser pour parser les requêtes JSON.
- On initialise une instance Express app.
- Middleware qui ajoute des headers CORS pour autoriser toutes les origines à accéder à l’API (utile pour le développement et appels cross-origin).
- Il permet aussi de dire que l’en-tête Content-Type est autorisé dans les requêtes.
- Le next() permet de passer au middleware suivant.
- Route POST /login : on récupère email et password depuis le corps JSON de la requête.
- Si l’un des deux est manquant, on répond immédiatement avec un message d’erreur.
- On interroge la base MySQL via db.query pour chercher un utilisateur par email.
- Si erreur ou aucun résultat, on renvoie un message d’authentification échouée.
- On récupère le premier utilisateur trouvé.
- On compare le mot de passe envoyé avec le hash stocké en base via bcrypt.compareSync.
- Si le mot de passe ne correspond pas, on renvoie une erreur.
- Si tout est valide, on renvoie un objet JSON avec success: true et les infos utilisateur (id, email, rôles convertis depuis JSON string).
- Démarre le serveur sur le port 3000 et affiche un message dans la console.
-
Créer le fichier
insert_user_index.jspour ajouter un utilisateur : - On commence par l’importation des modules
app.use(express.json());:permettre de lire automatiquement le JSON des requêtes- CORS = Cross-Origin Resource Sharing
- origin: ‘*’:N’importe qui peut appeler l’API (ou Flutter).
- methods:Liste des méthodes autorisées (POST, GET…).
- allowedHeaders: Autorise l’envoi de Content-Type (important pour JSON).
- credentials:Autorise l’envoi de cookies (pas obligatoire ici).
-
Étape 4 : Fichiers server.js
-
Créer le fichier
server.jspour ajouter gérerer les clients : -
Étape 4 : Fichiers auth.routes.js
-
Créer le fichier
auth.routes.jspour ajouter gérerer les clients : -
Créer le fichier
client.routes.jspour ajouter gérerer les clients : -
Étape 5 : Lancer les scripts
- Dans le terminal, tu fais :
- Dans votre dossier …\flutter_nodeJs, exécutez :
npm install cors - Pour insérer un utilisateur test :
node insert_user_index.js - Pour lancer le serveur :
node login.js -
Créer le projet Flutter
- Créer le projet Flutter :
- 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. -
Créer le fichier de connexion
- Description : Ce fichier gère l’interface de connexion utilisateur. Lorsqu’un utilisateur saisit ses identifiants, l’application envoie une requête POST à l’API
Node.js(route/login). Si les identifiants sont valides, il est redirigé vers une autre page. -
Étape 5 : Définir un modèle de données (Utilisateur)
- Description : Cette classe représente un utilisateur. Elle permet de convertir les données reçues du backend Node.js (en JSON) en objet Dart, et inversement.
-
Étape 5 : Créer le fichier
home_page.dart - Ce fichier Flutter est la page principale de l’application. Il permet d’afficher la liste des clients, d’en ajouter, modifier ou supprimer via l’API Node.js.
-
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 : flutter_app / table users
| Commande | Description |
|---|---|
| npm init | Initialise un nouveau projet Node.js (crée package.json) |
| npm init -y | Initialise avec les valeurs par défaut |
| npm install | Installe toutes les dépendances du package.json |
| npm install <package> | Installe un package spécifique |
| npm install –save <package> | Installe et ajoute à dependencies |
| npm install –save-dev <package> | Installe et ajoute à devDependencies |
| npm install -g <package> | Installe globalement |
| npm update | Met à jour tous les packages |
| npm update <package> | Met à jour un package spécifique |
| npm uninstall <package> | Désinstalle un package |
| npm audit | Vérifie les vulnérabilités de sécurité |
| npm audit fix | Corrige automatiquement les vulnérabilités |
| npm list | Liste les packages installés |
| npm list –depth=0 | Liste seulement les packages de premier niveau |
| npm outdated | Vérifie les packages obsolètes |
| npm run <script> | Exécute un script défini dans package.json |
CREATE DATABASE flutter_app;
CREATE TABLE `utilisateurs` (
`id` int NOT NULL AUTO_INCREMENT,
`email` varchar(180) NOT NULL,
`roles` json NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQ_EMAIL` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
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=9 DEFAULT CHARSET=utf8mb4;
module.exports = {
DB_HOST: '127.0.0.1',
DB_NAME: 'flutter_app',
DB_USER: 'root',
DB_PASSWORD: '',
DB_CHARSET: 'utf8mb4'
};
const mysql = require('mysql2');
const config = require('./config');
const connection = mysql.createConnection({
host: config.DB_HOST,
user: config.DB_USER,
password: config.DB_PASSWORD,
database: config.DB_NAME,
charset: config.DB_CHARSET
});
connection.connect(err => {
if (err) {
console.error("Erreur de connexion MySQL :", err.message);
process.exit(1);
} else {
console.log("Connexion MySQL réussie !");
}
});
module.exports = connection;
const mysql = require('mysql2');
const config = require('./config');
const connection = mysql.createConnection({
host: config.DB_HOST,
user: config.DB_USER,
password: config.DB_PASSWORD,
database: config.DB_NAME,
charset: config.DB_CHARSET
});
connection.connect(err => {
if (err) {
console.error("Erreur de connexion MySQL :", err.message);
process.exit(1);
} else {
console.log("Connexion MySQL réussie !");
}
});
module.exports = connection;
const express = require('express');
const db = require('./db');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
next();
});
app.post('/login', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.json({ success: false, message: "Email ou mot de passe manquant." });
}
db.query("SELECT * FROM utilisateurs WHERE email = ?", [email], (err, results) => {
if (err || results.length === 0) {
return res.json({ success: false, message: "Identifiants invalides" });
}
const user = results[0];
const passwordMatch = bcrypt.compareSync(password, user.password);
if (!passwordMatch) {
return res.json({ success: false, message: "Mot de passe incorrect" });
}
return res.json({
success: true,
user: {
id: user.id,
email: user.email,
roles: JSON.parse(user.roles)
}
});
});
});
app.listen(3000, () => console.log("Serveur Node.js en écoute sur http://localhost:3000"));
const express = require('express');
const db = require('./db');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
const app = express();
On importe les modules nécessaires :
app.use(bodyParser.json());
On utilise body-parser pour que Express puisse comprendre les données JSON envoyées dans le corps des requêtes (important pour POST).
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
next();
});
app.post('/login', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.json({ success: false, message: "Email ou mot de passe manquant." });
}
db.query("SELECT * FROM utilisateurs WHERE email = ?", [email], (err, results) => {
if (err || results.length === 0) {
return res.json({ success: false, message: "Identifiants invalides" });
}
const user = results[0];
const passwordMatch = bcrypt.compareSync(password, user.password);
if (!passwordMatch) {
return res.json({ success: false, message: "Mot de passe incorrect" });
}
return res.json({
success: true,
user: {
id: user.id,
email: user.email,
roles: JSON.parse(user.roles)
}
});
});
});
app.listen(3000, () => console.log("Serveur Node.js en écoute sur http://localhost:3000"));
const db = require('./db');
const bcrypt = require('bcrypt');
const email = 'nodejs@example.com';
const password = bcrypt.hashSync('123456', 10);
const roles = JSON.stringify(['ROLE_USER']);
db.query(
"INSERT INTO utilisateurs (email, password, roles) VALUES (?, ?, ?)",
[email, password, roles],
(err, result) => {
if (err) {
console.error("Erreur d'insertion :", err.message);
} else {
console.log("Utilisateur ajouté avec ID :", result.insertId);
}
process.exit();
}
);
-
✔ express: Framework permettant de créer un serveur HTTP facilement.
✔ cors: Module qui permet d’autoriser les requêtes venant d’autres applications (comme Flutter).
✔ app = express(): On crée l’application Express, qui représente notre serveur.
-
Sans cette ligne : Express ne comprend pas le JSON envoyé par Flutter.
req.body serait undefined.
// server.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
// 1. Importation des modules de routes
const authRoutes = require('./auth.routes');
//const clientRoutes = require('./client.routes');
const app = express();
// Configuration des Middlewares
app.use(cors());
app.use(bodyParser.json());
// 2. Montage des routes (Les routes /login et /clients sont maintenant disponibles)
app.use(authRoutes);
app.use(clientRoutes);
// -------------------- SERVER START --------------------
const PORT = 3000;
app.listen(PORT, () => console.log(`Serveur Node.js en écoute sur http://localhost:${PORT}`));
// -------------------- IMPORTATIONS --------------------
// On importe le framework Express pour créer des routes HTTP
const express = require('express');
// On crée un router Express. Le router permet de créer des routes modulaires,
// séparées du fichier principal de l'application.
const router = express.Router(); // Utilisation du Router d'Express
// On importe le module de connexion à la base de données
// Il doit contenir la configuration et la méthode query pour exécuter des requêtes SQL.
const db = require('./db');
// On importe bcrypt pour le hachage des mots de passe
// Il permet de comparer le mot de passe saisi avec le mot de passe stocké haché.
const bcrypt = require('bcrypt');
// -------------------- ROUTE LOGIN --------------------
// Route POST pour la connexion d'un utilisateur
router.post('/login', (req, res) => {
// Récupération de l'email et du mot de passe envoyés dans le corps de la requête
const { email, password } = req.body;
// Vérification que l'utilisateur a bien fourni un email et un mot de passe
if (!email || !password) {
// Si l'email ou le mot de passe est manquant, on renvoie un message d'erreur
return res.json({ success: false, message: "Email ou mot de passe manquant." });
}
// Recherche de l'utilisateur dans la base de données par email
db.query("SELECT * FROM utilisateurs WHERE email = ?", [email], (err, results) => {
// Si une erreur SQL se produit ou si aucun utilisateur n'est trouvé
if (err || results.length === 0) {
return res.json({ success: false, message: "Identifiants invalides" });
}
// On récupère le premier utilisateur trouvé
const user = results[0];
// Vérification du mot de passe : on compare le mot de passe saisi avec le mot de passe haché
const passwordMatch = bcrypt.compareSync(password, user.password);
// Si le mot de passe ne correspond pas, on renvoie un message d'erreur
if (!passwordMatch) {
return res.json({ success: false, message: "Mot de passe incorrect" });
}
// Gestion des rôles de l'utilisateur
let rolesArray;
try {
// On essaie de parser le champ roles qui est stocké en JSON dans la base
rolesArray = JSON.parse(user.roles); // parse la chaîne JSON
// Si le résultat n'est pas un tableau, on le transforme en tableau
if (!Array.isArray(rolesArray)) rolesArray = [rolesArray];
} catch (e) {
// Si ce n'est pas du JSON valide, on crée un tableau contenant directement la valeur
rolesArray = [user.roles];
}
// Réponse JSON en cas de succès de la connexion
// On renvoie l'ID, l'email et les rôles de l'utilisateur
return res.json({
success: true,
user: {
id: user.id,
email: user.email,
roles: rolesArray
}
});
});
});
// Export du router pour pouvoir l'utiliser dans le fichier principal de l'application
module.exports = router;
// -------------------- IMPORTATIONS --------------------
// On importe le framework Express pour créer des routes HTTP
const express = require('express');
// Création d'un router Express pour définir des routes modulaires
const router = express.Router(); // Utilisation du Router d'Express
// On importe le module de connexion à la base de données
const db = require('./db');
// -------------------- ROUTE GET /clients --------------------
// Récupérer la liste de tous les clients
router.get('/clients', (req, res) => {
// Requête SQL pour sélectionner tous les clients, triés par ID décroissant
const sql = "SELECT * FROM client ORDER BY id DESC";
// Exécution de la requête
db.query(sql, (err, results) => {
if (err) {
// Si erreur SQL, renvoyer un statut 500 et un message d'erreur
return res.status(500).json({ error: "Erreur serveur" });
}
// Sinon, renvoyer les résultats (liste des clients) en JSON
res.json(results);
});
});
// -------------------- ROUTE POST /add-client --------------------
// Ajouter un nouveau client
router.post('/add-client', (req, res) => {
// Récupération des données envoyées dans le corps de la requête
// On fournit des valeurs par défaut si les champs sont absents
const { name = "", family = "", age = 0 } = req.body;
// Vérification que le nom et le prénom ne sont pas vides
if (!name.trim() || !family.trim()) {
return res.status(400).json({ message: "Nom et prénom requis" });
}
// Requête SQL pour insérer un nouveau client
const sql = "INSERT INTO client (name, family, age) VALUES (?, ?, ?)";
db.query(sql, [name, family, age], (err, result) => {
if (err) {
console.error("Erreur SQL:", err);
return res.status(500).json({ message: "Erreur BD" });
}
// Envoi d'une réponse avec l'ID du nouveau client et un message de succès
res.status(201).json({ id: result.insertId, message: "Client ajouté" });
});
});
// -------------------- ROUTE PUT /clients/:id --------------------
// Mettre à jour les informations d'un client existant
router.put('/clients/:id', (req, res) => {
// Récupération de l'ID du client depuis les paramètres de l'URL
const { id } = req.params;
// Récupération des nouvelles valeurs depuis le corps de la requête
const { name, family, age } = req.body;
// Requête SQL pour mettre à jour le client
const sql = "UPDATE client SET name=?, family=?, age=? WHERE id=?";
db.query(sql, [name, family, age, id], (err, result) => {
if (err) return res.status(500).json({ message: "Erreur mise à jour" });
// Si aucun client n'a été affecté, l'ID n'existe pas
if (result.affectedRows === 0) {
return res.status(404).json({ message: "Client introuvable" });
}
// Réponse de succès
res.json({ message: "Client mis à jour" });
});
});
// -------------------- ROUTE DELETE /clients/:id --------------------
// Supprimer un client existant
router.delete('/clients/:id', (req, res) => {
// Récupération de l'ID du client depuis les paramètres de l'URL
const { id } = req.params;
// Requête SQL pour supprimer le client
const sql = "DELETE FROM client WHERE id=?";
db.query(sql, [id], (err, result) => {
if (err) return res.status(500).json({ message: "Erreur suppression" });
// Si aucun client n'a été supprimé, l'ID n'existe pas
if (result.affectedRows === 0) {
return res.status(404).json({ message: "Client introuvable" });
}
// Réponse de succès
res.json({ message: "Client supprimé" });
});
});
// Export du router pour l'utiliser dans l'application principale
module.exports = router;
flutter create flutter_login_app
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:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'home_page.dart'; // Import de ta page Home
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State createState() => _LoginPageState();
}
class _LoginPageState extends State {
final _emailCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
String errorMsg = '';
bool _isLoading = false;
bool _obscurePassword = true;
// Utilise l'IP de ta machine, ici la même que dans ta page Home
final String serverUrl = "http://10.0.2.2:3000/login";
Future _login() async {
try {
setState(() {
errorMsg = "";
_isLoading = true;
});
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);
// 🚀 CORRECTION ICI : Utilisez 'success' au lieu de 'status'
if (data['success'] == true) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
} else {
setState(() {
// Le message d'erreur est sous la clé 'message' si 'success' est false
errorMsg = data['message'] ?? 'Identifiants incorrects';
});
}
} catch (e) {
print('Erreur: $e');
setState(() {
errorMsg = 'Erreur de connexion au serveur.';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 40),
/// HEADER
Text(
"Connexion",
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.indigo[700],
),
),
const SizedBox(height: 8),
Text(
"Bienvenue, connectez-vous pour continuer",
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 40),
/// EMAIL FIELD
TextField(
controller: _emailCtrl,
decoration: InputDecoration(
labelText: "Adresse email",
prefixIcon: const Icon(Icons.email),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (_) => setState(() => errorMsg = ""),
),
const SizedBox(height: 20),
/// PASSWORD FIELD
TextField(
controller: _passwordCtrl,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: "Mot de passe",
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined),
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (_) => setState(() => errorMsg = ""),
),
const SizedBox(height: 10),
/// FORGOT PASSWORD
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {},
child: Text(
"Mot de passe oublié ?",
style: TextStyle(color: Colors.indigo[600]),
),
),
),
const SizedBox(height: 20),
/// LOGIN BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation(Colors.white),
),
)
: const Text(
"Se connecter",
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
const SizedBox(height: 15),
/// ERROR MESSAGE
if (errorMsg.isNotEmpty)
Container(
margin: const EdgeInsets.only(top: 10),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red[600]),
const SizedBox(width: 10),
Expanded(
child: Text(
errorMsg,
style: TextStyle(
color: Colors.red[700],
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const SizedBox(height: 30),
/// FOOTER
Center(
child: Text(
"Développé pour cours Flutter",
style: TextStyle(color: Colors.grey[500]),
),
),
],
),
),
),
);
}
}
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: json['id'] as int? ?? 0, // Gestion null-safe
name: (json['name'] ?? '') as String,
family: (json['family'] ?? '') as String,
age: (json['age'] ?? 0) as int,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'family': family,
'age': age,
};
}
Fichier : home_page.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 createState() => _HomePageState();
}
class _HomePageState extends State {
final _nameCtrl = TextEditingController();
final _familyCtrl = TextEditingController();
final _ageCtrl = TextEditingController();
String serverMsg = '';
final List _clientsList = [];
final String baseUrl = "http://10.0.2.2:3000";
// ------------------ INSERT ------------------
Future _postRequest() async {
try {
final response = await http.post(
Uri.parse("$baseUrl/add-client"),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'name': _nameCtrl.text.trim(),
'family': _familyCtrl.text.trim(),
'age': int.tryParse(_ageCtrl.text) ?? 0,
}),
);
final res = jsonDecode(response.body);
serverMsg = response.statusCode == 201 ? "Client ajouté" : res["message"];
_clearForm();
await _getRequest();
setState(() {});
} catch (e) {
serverMsg = e.toString();
setState(() {});
}
}
// ------------------ LOAD LIST ------------------
Future _getRequest() async {
try {
final response = await http.get(Uri.parse("$baseUrl/clients"));
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";
} catch (e) {
serverMsg = e.toString();
}
setState(() {});
}
// ------------------ DELETE ------------------
Future _deleteRequest(int id) async {
try {
final response =
await http.delete(Uri.parse("$baseUrl/clients/$id"));
serverMsg =
response.statusCode == 200 ? "Client supprimé" : "Erreur suppression";
await _getRequest();
setState(() {});
} catch (e) {
serverMsg = e.toString();
setState(() {});
}
}
// ------------------ UPDATE ------------------
Future _updateRequest(Client client) async {
try {
final response = await http.put(
Uri.parse("$baseUrl/clients/${client.id}"),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(client.toJson()),
);
serverMsg = response.statusCode == 200
? "Client mis à jour"
: "Erreur mise à jour";
await _getRequest();
setState(() {});
} 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(
backgroundColor: Colors.grey[100],
// ------------------ APPBAR ------------------
appBar: AppBar(
title: const Text("Gestion des Clients"),
backgroundColor: Colors.indigo,
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
_clearForm();
_showAddDialog();
},
)
],
),
// ------------------ LIST ------------------
body: _buildClientList(),
floatingActionButton: FloatingActionButton(
onPressed: () {
_clearForm();
_showAddDialog();
},
backgroundColor: Colors.indigo,
child: const Icon(Icons.add),
),
);
}
// ------------------ LIST BUILDER ------------------
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)),
elevation: 3,
margin: const EdgeInsets.symmetric(vertical: 8),
child: ListTile(
title: Text(
"${client.name} ${client.family}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text("Âge : ${client.age}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// EDIT
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);
},
),
// DELETE
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _deleteRequest(client.id),
),
],
),
),
);
},
);
}
// ------------------ DIALOG 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: 10),
_textField(_familyCtrl, "Prénom", Icons.family_restroom),
const SizedBox(height: 10),
_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);
},
),
],
);
},
);
}
// ------------------ DIALOG UPDATE ------------------
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: 10),
_textField(_familyCtrl, "Prénom", Icons.family_restroom),
const SizedBox(height: 10),
_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);
},
),
],
);
},
);
}
// ------------------ TEXTFIELD DESIGN ------------------
Widget _textField(TextEditingController ctrl, String label, IconData icon,
{TextInputType keyboard = TextInputType.text}) {
return TextField(
controller: ctrl,
keyboardType: keyboard,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
}
