Se connecter à Flutter avec Node js MySQL
Sommaire
- 1- Objectif
- 2- Présentation
- 3- Configuration du projet
- 3.1- Étape 1 : Créer la base de données MySQL
- 3.2- Étape 2 : Créer le dossier du projet
- 3.2.1- Créer le dossier du projet
- 3.2.2- Initialiser le projet Node.js
- 3.2.3- Installer les modules nécessaires
- 3.3- Étape 3 : Créer les fichiers un par un
- 3.4- Étape 4 : Lancer les scripts
- 4- Créer le projet Flutter
- 4.1- Étape 4 : Créer l'interface de connexion Flutter
- 4.2- Étape 5 : Définir un modèle de données (Utilisateur)
- 5- CRUD (client)
- 5.1- Étape 1 : Créer le fichier
get_client.php
- 5.2- Étape 2 : Créer le fichier
insert_client.php
- 5.3- Étape 3 : Créer le fichier
update_client.php
- 5.4- Étape 4 : Créer le fichier
delete_client.php
- 5.5- Étape 5 : Créer le fichier
home_page.dart
- 6- Exercices
- 7- Fichiers réalisés
- 7.1.1- Cours Flutter
Lire et écrire dans MySQL avec Node.js
-
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.
-
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
: -
É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 : mon_api_node
- 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 mon_api_node, tape :
npm init -y
- Cela crée un fichier package.json.
-
Installer les modules nécessaires
- Toujours dans le terminal :
npm install express mysql2 bcrypt body-parser
-
Étape 3 : Créer les fichiers un par un
- Créer le fichier config.js :
- Enregistre le fichier sous config.js dans ton dossier mon_api_node
- Créer le fichier db.js :
- Créer le fichier insert_user.js pour ajouter un utilisateur :
- Créer le fichier login.js :
- Créer le fichier login.js :
-
Étape 4 : Lancer les scripts
- Dans le terminal, tu fais :
- Pour insérer un utilisateur test :
node insert_user.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
- Fichier : main.dart
- Description : Ce fichier est le point d’entrée de votre application Flutter. Il lance le widget
MainApp
qui affiche directement la page de connexionLoginPage
. - Fichier : login_page.dart
- 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)
- Fichier :
user_model.dart
- 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.
-
CRUD (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 : flutter_app / table users
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;
module.exports = {
DB_HOST: '127.0.0.1',
DB_NAME: 'base_test',
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 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();
}
);
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();
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"));
flutter create flutter_login_app
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
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(),
);
}
}
import 'dart:convert';
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 = '';
// ⚠️ Remplacer localhost par l'IP de votre PC si vous utilisez un vrai téléphone
final String serverUrl = "http://localhost:3000/login";
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) {
// Redirection vers la HomePage (à créer)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const Placeholder()), // Remplacer par 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)),
],
),
),
);
}
}
class Utilisateur {
final int id;
final String email;
final List roles;
Utilisateur({
required this.id,
required this.email,
required this.roles,
});
factory Utilisateur.fromJson(Map<String, dynamic> json) {
return Utilisateur(
id: int.parse(json['id'].toString()),
email: json['email'],
roles: json['roles'],
);
}
Map<String, dynamic> toJson() {
return {'id': id, 'email': email, 'roles': roles};
}
}
<?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);
<?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'
]);
<?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'
]);
<?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'
]);
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/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),
),
);
}
}