Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Flutter

CRUD en utilisant Flutter avec Spring Boot

Un cours complet pour maîtriser CRUD en utilisant Flutter avec Spring Boot.

0 chapitres 0 QCM Accès gratuit
 
  1. Objectif

    • Créer une application complète de gestion de clients avec opérations CRUD (Create, Read, Update, Delete) utilisant Flutter comme frontend et Spring Boot comme backend.
  2. Présentation

  3. Partie 1: Configuration du Backend Spring Boot

    1. Étape 1: Entité JPA - Client.java
        package com.example.myapi.entity;
        
        import jakarta.persistence.*;
        
        @Entity
        @Table(name = "clients")
        public class Client {
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;
            
            @Column(nullable = false)
            private String nom;
            
            @Column(nullable = false)
            private String prenom;
            
            @Column(nullable = false, unique = true)
            private String email;
            
            private String telephone;
            
            // Constructeurs
            public Client() {}
            
            public Client(String nom, String prenom, String email, String telephone) {
                this.nom = nom;
                this.prenom = prenom;
                this.email = email;
                this.telephone = telephone;
            }
            
            // Getters et Setters
            public Long getId() { return id; }
            public void setId(Long id) { this.id = id; }
            
            public String getNom() { return nom; }
            public void setNom(String nom) { this.nom = nom; }
            
            public String getPrenom() { return prenom; }
            public void setPrenom(String prenom) { this.prenom = prenom; }
            
            public String getEmail() { return email; }
            public void setEmail(String email) { this.email = email; }
            
            public String getTelephone() { return telephone; }
            public void setTelephone(String telephone) { this.telephone = telephone; }
        }
    2. Étape 2: Repository - ClientRepository.java
        package com.example.myapi.repository;
        
        import com.example.myapi.entity.Client;
        import org.springframework.data.jpa.repository.JpaRepository;
        import org.springframework.stereotype.Repository;
        
        import java.util.List;
        
        @Repository
        public interface ClientRepository extends JpaRepository {
            List findByNomContainingIgnoreCase(String nom);
            boolean existsByEmail(String email);
            Client findByEmail(String email);
        }
    3. Étape 3: Service - ClientService.java
          package com.example.myapi.service;
          
          import com.example.myapi.entity.Client;
          import com.example.myapi.repository.ClientRepository;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.stereotype.Service;
          
          import java.util.List;
          import java.util.Optional;
          
          @Service
          public class ClientService {
              
              @Autowired
              private ClientRepository clientRepository;
              
              public List getAllClients() {
                  return clientRepository.findAll();
              }
              
              public Optional getClientById(Long id) {
                  return clientRepository.findById(id);
              }
              
              public Client createClient(Client client) {
                  if (clientRepository.existsByEmail(client.getEmail())) {
                      throw new RuntimeException("Email déjà utilisé");
                  }
                  return clientRepository.save(client);
              }
              
              public Client updateClient(Long id, Client clientDetails) {
                  Client client = clientRepository.findById(id)
                      .orElseThrow(() -> new RuntimeException("Client non trouvé"));
                  
                  // Vérifier si l'email est déjà utilisé par un autre client
                  if (!client.getEmail().equals(clientDetails.getEmail()) && 
                      clientRepository.existsByEmail(clientDetails.getEmail())) {
                      throw new RuntimeException("Email déjà utilisé");
                  }
                  
                  client.setNom(clientDetails.getNom());
                  client.setPrenom(clientDetails.getPrenom());
                  client.setEmail(clientDetails.getEmail());
                  client.setTelephone(clientDetails.getTelephone());
                  
                  return clientRepository.save(client);
              }
              
              public void deleteClient(Long id) {
                  Client client = clientRepository.findById(id)
                      .orElseThrow(() -> new RuntimeException("Client non trouvé"));
                  clientRepository.delete(client);
              }
              
              public List searchClients(String nom) {
                  return clientRepository.findByNomContainingIgnoreCase(nom);
              }
          }
      • Étape 4: Controller - ClientController.java
          package com.example.myapi.controller;
          
          import com.example.myapi.entity.Client;
          import com.example.myapi.service.ClientService;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.http.ResponseEntity;
          import org.springframework.web.bind.annotation.*;
          
          import java.util.List;
          
          @RestController
          @RequestMapping("/api/clients")
          @CrossOrigin(origins = "*")
          public class ClientController {
              
              @Autowired
              private ClientService clientService;
              
              @GetMapping
              public List getAllClients() {
                  return clientService.getAllClients();
              }
              
              @GetMapping("/{id}")
              public ResponseEntity getClientById(@PathVariable Long id) {
                  return clientService.getClientById(id)
                      .map(ResponseEntity::ok)
                      .orElse(ResponseEntity.notFound().build());
              }
              
              @PostMapping
              public Client createClient(@RequestBody Client client) {
                  return clientService.createClient(client);
              }
              
              @PutMapping("/{id}")
              public ResponseEntity updateClient(@PathVariable Long id, @RequestBody Client clientDetails) {
                  try {
                      Client updatedClient = clientService.updateClient(id, clientDetails);
                      return ResponseEntity.ok(updatedClient);
                  } catch (RuntimeException e) {
                      return ResponseEntity.notFound().build();
                  }
              }
              
              @DeleteMapping("/{id}")
              public ResponseEntity> deleteClient(@PathVariable Long id) {
                  try {
                      clientService.deleteClient(id);
                      return ResponseEntity.ok().build();
                  } catch (RuntimeException e) {
                      return ResponseEntity.notFound().build();
                  }
              }
              
              @GetMapping("/search")
              public List searchClients(@RequestParam String nom) {
                  return clientService.searchClients(nom);
              }
          }
      • Étape 5: Configuration CORS globale
          package com.example.myapi.config;
          
          import org.springframework.context.annotation.Bean;
          import org.springframework.context.annotation.Configuration;
          import org.springframework.web.cors.CorsConfiguration;
          import org.springframework.web.cors.CorsConfigurationSource;
          import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
          import org.springframework.web.servlet.config.annotation.CorsRegistry;
          import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
          
          import java.util.Arrays;
          
          @Configuration
          public class CorsConfig implements WebMvcConfigurer {
              
              @Override
              public void addCorsMappings(CorsRegistry registry) {
                  registry.addMapping("/**")
                      .allowedOrigins("*")
                      .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                      .allowedHeaders("*")
                      .allowCredentials(false);
              }
              
              @Bean
              public CorsConfigurationSource corsConfigurationSource() {
                  CorsConfiguration configuration = new CorsConfiguration();
                  configuration.setAllowedOrigins(Arrays.asList("*"));
                  configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
                  configuration.setAllowedHeaders(Arrays.asList("*"));
                  configuration.setAllowCredentials(false);
                  
                  UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
                  source.registerCorsConfiguration("/**", configuration);
                  return source;
              }
          }
  4. Partie 2: Configuration Flutter

    1. Étape 1: Modèle Client - client_model.dart
        class Client {
          int? id;
          String nom;
          String prenom;
          String email;
          String telephone;
        
          Client({
            this.id,
            required this.nom,
            required this.prenom,
            required this.email,
            required this.telephone,
          });
        
          factory Client.fromJson(Map json) {
            return Client(
              id: json['id'],
              nom: json['nom'] ?? '',
              prenom: json['prenom'] ?? '',
              email: json['email'] ?? '',
              telephone: json['telephone'] ?? '',
            );
          }
        
          Map toJson() {
            return {
              'id': id,
              'nom': nom,
              'prenom': prenom,
              'email': email,
              'telephone': telephone,
            };
          }
        }
      • Étape 2: Service API - api_service.dart
          import 'dart:convert';
          import 'package:http/http.dart' as http;
          import 'client_model.dart';
          
          class ApiService {
            static const String baseUrl = "http://10.0.2.2:8080/api/clients";
          
            static Future> getClients() async {
              final response = await http.get(Uri.parse(baseUrl));
              
              if (response.statusCode == 200) {
                List data = json.decode(response.body);
                return data.map((json) => Client.fromJson(json)).toList();
              } else {
                throw Exception('Erreur lors du chargement des clients');
              }
            }
          
            static Future getClient(int id) async {
              final response = await http.get(Uri.parse('$baseUrl/$id'));
              
              if (response.statusCode == 200) {
                return Client.fromJson(json.decode(response.body));
              } else {
                throw Exception('Erreur lors du chargement du client');
              }
            }
          
            static Future createClient(Client client) async {
              final response = await http.post(
                Uri.parse(baseUrl),
                headers: {'Content-Type': 'application/json'},
                body: json.encode(client.toJson()),
              );
              
              if (response.statusCode == 200) {
                return Client.fromJson(json.decode(response.body));
              } else {
                throw Exception('Erreur lors de la création du client');
              }
            }
          
            static Future updateClient(Client client) async {
              final response = await http.put(
                Uri.parse('$baseUrl/${client.id}'),
                headers: {'Content-Type': 'application/json'},
                body: json.encode(client.toJson()),
              );
              
              if (response.statusCode == 200) {
                return Client.fromJson(json.decode(response.body));
              } else {
                throw Exception('Erreur lors de la mise à jour du client');
              }
            }
          
            static Future deleteClient(int id) async {
              final response = await http.delete(Uri.parse('$baseUrl/$id'));
              
              if (response.statusCode != 200) {
                throw Exception('Erreur lors de la suppression du client');
              }
            }
          
            static Future<List<Client>> searchClients(String query) async {
              final response = await http.get(Uri.parse('$baseUrl/search?nom=$query'));
              
              if (response.statusCode == 200) {
                List<dynamic> data = json.decode(response.body);
                return data.map((json) => Client.fromJson(json)).toList();
              } else {
                throw Exception('Erreur lors de la recherche');
              }
            }
          }
      • Étape 3: Page Principale - home_page.dart
          import 'package:flutter/material.dart';
          import 'api_service.dart';
          import 'client_model.dart';
          import 'client_form.dart';
          
          class HomePage extends StatefulWidget {
            const HomePage({super.key});
          
            @override
            _HomePageState createState() => _HomePageState();
          }
          
          class _HomePageState extends State {
            List _clients = [];
            bool _isLoading = true;
            String _searchQuery = '';
          
            @override
            void initState() {
              super.initState();
              _loadClients();
            }
          
            Future _loadClients() async {
              setState(() => _isLoading = true);
              try {
                final clients = await ApiService.getClients();
                setState(() => _clients = clients);
              } catch (e) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Erreur: $e')),
                );
              } finally {
                setState(() => _isLoading = false);
              }
            }
          
            Future _searchClients() async {
              if (_searchQuery.isEmpty) {
                _loadClients();
                return;
              }
          
              setState(() => _isLoading = true);
              try {
                final clients = await ApiService.searchClients(_searchQuery);
                setState(() => _clients = clients);
              } catch (e) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Erreur: $e')),
                );
              } finally {
                setState(() => _isLoading = false);
              }
            }
          
            Future _deleteClient(int id) async {
              try {
                await ApiService.deleteClient(id);
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Client supprimé avec succès')),
                );
                _loadClients();
              } catch (e) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Erreur: $e')),
                );
              }
            }
          
            void _showDeleteDialog(Client client) {
              showDialog(
                context: context,
                builder: (BuildContext context) {
                  return AlertDialog(
                    title: const Text('Confirmer la suppression'),
                    content: Text('Êtes-vous sûr de vouloir supprimer ${client.prenom} ${client.nom} ?'),
                    actions: [
                      TextButton(
                        onPressed: () => Navigator.of(context).pop(),
                        child: const Text('Annuler'),
                      ),
                      TextButton(
                        onPressed: () {
                          Navigator.of(context).pop();
                          _deleteClient(client.id!);
                        },
                        child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
                      ),
                    ],
                  );
                },
              );
            }
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  title: const Text('Gestion des Clients'),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.refresh),
                      onPressed: _loadClients,
                    ),
                  ],
                ),
                body: Column(
                  children: [
                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: TextField(
                        decoration: InputDecoration(
                          labelText: 'Rechercher par nom',
                          suffixIcon: IconButton(
                            icon: const Icon(Icons.search),
                            onPressed: _searchClients,
                          ),
                        ),
                        onChanged: (value) => _searchQuery = value,
                        onSubmitted: (_) => _searchClients(),
                      ),
                    ),
                    Expanded(
                      child: _isLoading
                          ? const Center(child: CircularProgressIndicator())
                          : _clients.isEmpty
                              ? const Center(child: Text('Aucun client trouvé'))
                              : ListView.builder(
                                  itemCount: _clients.length,
                                  itemBuilder: (context, index) {
                                    final client = _clients[index];
                                    return ListTile(
                                      title: Text('${client.prenom} ${client.nom}'),
                                      subtitle: Text(client.email),
                                      trailing: Row(
                                        mainAxisSize: MainAxisSize.min,
                                        children: [
                                          IconButton(
                                            icon: const Icon(Icons.edit, color: Colors.blue),
                                            onPressed: () => _navigateToForm(client),
                                          ),
                                          IconButton(
                                            icon: const Icon(Icons.delete, color: Colors.red),
                                            onPressed: () => _showDeleteDialog(client),
                                          ),
                                        ],
                                      ),
                                      onTap: () => _navigateToForm(client),
                                    );
                                  },
                                ),
                    ),
                  ],
                ),
                floatingActionButton: FloatingActionButton(
                  onPressed: () => _navigateToForm(null),
                  child: const Icon(Icons.add),
                ),
              );
            }
          
            void _navigateToForm(Client? client) async {
              final result = await Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ClientFormPage(client: client),
                ),
              );
              
              if (result == true) {
                _loadClients();
              }
            }
          }
      • Étape 4: Formulaire Client - client_form.dart
          import 'package:flutter/material.dart';
          import 'api_service.dart';
          import 'client_model.dart';
          
          class ClientFormPage extends StatefulWidget {
            final Client? client;
          
            const ClientFormPage({super.key, this.client});
          
            @override
            _ClientFormPageState createState() => _ClientFormPageState();
          }
          
          class _ClientFormPageState extends State {
            final _formKey = GlobalKey();
            final _nomController = TextEditingController();
            final _prenomController = TextEditingController();
            final _emailController = TextEditingController();
            final _telephoneController = TextEditingController();
          
            bool _isLoading = false;
          
            @override
            void initState() {
              super.initState();
              if (widget.client != null) {
                _nomController.text = widget.client!.nom;
                _prenomController.text = widget.client!.prenom;
                _emailController.text = widget.client!.email;
                _telephoneController.text = widget.client!.telephone;
              }
            }
          
            Future _saveClient() async {
              if (!_formKey.currentState!.validate()) return;
          
              setState(() => _isLoading = true);
          
              try {
                final client = Client(
                  id: widget.client?.id,
                  nom: _nomController.text,
                  prenom: _prenomController.text,
                  email: _emailController.text,
                  telephone: _telephoneController.text,
                );
          
                if (widget.client == null) {
                  await ApiService.createClient(client);
                } else {
                  await ApiService.updateClient(client);
                }
          
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Client ${widget.client == null ? 'créé' : 'modifié'} avec succès')),
                );
                
                Navigator.pop(context, true);
              } catch (e) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Erreur: $e')),
                );
              } finally {
                setState(() => _isLoading = false);
              }
            }
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  title: Text(widget.client == null ? 'Nouveau Client' : 'Modifier Client'),
                ),
                body: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Form(
                    key: _formKey,
                    child: Column(
                      children: [
                        TextFormField(
                          controller: _nomController,
                          decoration: const InputDecoration(labelText: 'Nom'),
                          validator: (value) {
                            if (value == null || value.isEmpty) {
                              return 'Veuillez entrer un nom';
                            }
                            return null;
                          },
                        ),
                        TextFormField(
                          controller: _prenomController,
                          decoration: const InputDecoration(labelText: 'Prénom'),
                          validator: (value) {
                            if (value == null || value.isEmpty) {
                              return 'Veuillez entrer un prénom';
                            }
                            return null;
                          },
                        ),
                        TextFormField(
                          controller: _emailController,
                          decoration: const InputDecoration(labelText: 'Email'),
                          keyboardType: TextInputType.emailAddress,
                          validator: (value) {
                            if (value == null || value.isEmpty) {
                              return 'Veuillez entrer un email';
                            }
                            if (!value.contains('@')) {
                              return 'Email invalide';
                            }
                            return null;
                          },
                        ),
                        TextFormField(
                          controller: _telephoneController,
                          decoration: const InputDecoration(labelText: 'Téléphone'),
                          keyboardType: TextInputType.phone,
                        ),
                        const SizedBox(height: 20),
                        _isLoading
                            ? const CircularProgressIndicator()
                            : ElevatedButton(
                                onPressed: _saveClient,
                                child: Text(widget.client == null ? 'Créer' : 'Modifier'),
                              ),
                      ],
                    ),
                  ),
                ),
              );
            }
          
            @override
            void dispose() {
              _nomController.dispose();
              _prenomController.dispose();
              _emailController.dispose();
              _telephoneController.dispose();
              super.dispose();
            }
          }
    2. Partie 3: Configuration de la Base de Données

        CREATE TABLE clients (
            id BIGINT AUTO_INCREMENT PRIMARY KEY,
            nom VARCHAR(255) NOT NULL,
            prenom VARCHAR(255) NOT NULL,
            email VARCHAR(255) NOT NULL UNIQUE,
            telephone VARCHAR(20),
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
        );
    3. Partie 4: Tests et Déploiement

        # Récupérer tous les clients
        curl http://localhost:8080/api/clients
        
        # Créer un client
        curl -X POST http://localhost:8080/api/clients \
          -H "Content-Type: application/json" \
          -d '{"nom":"Dupont", "prenom":"Jean", "email":"jean.dupont@email.com", "telephone":"0123456789"}'
        
        # Modifier un client
        curl -X PUT http://localhost:8080/api/clients/1 \
          -H "Content-Type: application/json" \
          -d '{"nom":"Dupont", "prenom":"Jean", "email":"jean.dupont@email.com", "telephone":"0987654321"}'
        
        # Supprimer un client
        curl -X DELETE http://localhost:8080/api/clients/1
    4. Exercices d'Approfondissement

      • Ajouter la pagination dans l'API Spring Boot et Flutter
      • Implémenter l'authentification JWT pour sécuriser les endpoints
      • Ajouter la validation avancée des emails et numéros de téléphone
      • Créer des tests unitaires pour le service Spring Boot
      • Ajouter le refresh token pour maintenir la session utilisateur
      • Implémenter la recherche avancée avec multiple critères
      • Ajouter l'exportation des données en CSV/Excel
      • Créer un dashboard avec des statistiques clients