Objectif
- Permettre à l’apprenant de maîtriser la consommation d’API REST et GraphQL dans une application Flutter afin d’interagir efficacement avec des services distants (lecture, création, mise à jour et suppression de données).
Structure du projet
-
lib/
├── main.dart
├── models/
│ └── user.dart
├── services/
│ └── api_service.dart
└── screens/
├── users_screen.dart
├── user_detail_screen.dart
└── add_edit_user_screen.dart
-
Code complet
-
Fichier
pubspec.yamlname: jsonplaceholder_app description: Une application Flutter pour gérer les utilisateurs via JSONPlaceholder API publish_to: 'none' version: 1.0.0+1 environment: sdk: '>=3.0.0 <4.0.0' dependencies: flutter: sdk: flutter http: ^1.1.0 provider: ^6.0.5 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 flutter: uses-material-design: true Modèle
lib/models/user.dartclass User { final int? id; final String name; final String username; final String email; final String phone; final String website; final Address address; final Company company; User({ this.id, required this.name, required this.username, required this.email, required this.phone, required this.website, required this.address, required this.company, }); factory User.fromJson(Mapjson) { return User( id: json['id'], name: json['name'], username: json['username'], email: json['email'], phone: json['phone'], website: json['website'], address: Address.fromJson(json['address']), company: Company.fromJson(json['company']), ); } Map toJson() { return { if (id != null) 'id': id, 'name': name, 'username': username, 'email': email, 'phone': phone, 'website': website, 'address': address.toJson(), 'company': company.toJson(), }; } User copyWith({ int? id, String? name, String? username, String? email, String? phone, String? website, Address? address, Company? company, }) { return User( id: id ?? this.id, name: name ?? this.name, username: username ?? this.username, email: email ?? this.email, phone: phone ?? this.phone, website: website ?? this.website, address: address ?? this.address, company: company ?? this.company, ); } } class Address { final String street; final String suite; final String city; final String zipcode; final Geo geo; Address({ required this.street, required this.suite, required this.city, required this.zipcode, required this.geo, }); factory Address.fromJson(Map json) { return Address( street: json['street'], suite: json['suite'], city: json['city'], zipcode: json['zipcode'], geo: Geo.fromJson(json['geo']), ); } Map toJson() { return { 'street': street, 'suite': suite, 'city': city, 'zipcode': zipcode, 'geo': geo.toJson(), }; } Address copyWith({ String? street, String? suite, String? city, String? zipcode, Geo? geo, }) { return Address( street: street ?? this.street, suite: suite ?? this.suite, city: city ?? this.city, zipcode: zipcode ?? this.zipcode, geo: geo ?? this.geo, ); } } class Geo { final String lat; final String lng; Geo({ required this.lat, required this.lng, }); factory Geo.fromJson(Map json) { return Geo( lat: json['lat'], lng: json['lng'], ); } Map toJson() { return { 'lat': lat, 'lng': lng, }; } } class Company { final String name; final String catchPhrase; final String bs; Company({ required this.name, required this.catchPhrase, required this.bs, }); factory Company.fromJson(Map json) { return Company( name: json['name'], catchPhrase: json['catchPhrase'], bs: json['bs'], ); } Map toJson() { return { 'name': name, 'catchPhrase': catchPhrase, 'bs': bs, }; } Company copyWith({ String? name, String? catchPhrase, String? bs, }) { return Company( name: name ?? this.name, catchPhrase: catchPhrase ?? this.catchPhrase, bs: bs ?? this.bs, ); } } Service API
lib/services/api_service.dartimport 'dart:convert'; import 'package:http/http.dart' as http; import '../models/user.dart'; class ApiService { static const String baseUrl = 'https://jsonplaceholder.typicode.com'; Future- > getUsers() async {
final response = await http.get(Uri.parse('$baseUrl/users'));
if (response.statusCode == 200) {
List
data = json.decode(response.body); return data.map((json) => User.fromJson(json)).toList(); } else { throw Exception('Échec du chargement des utilisateurs'); } } Future getUser(int id) async { final response = await http.get(Uri.parse('$baseUrl/users/$id')); if (response.statusCode == 200) { return User.fromJson(json.decode(response.body)); } else { throw Exception('Échec du chargement de l\'utilisateur'); } } Future createUser(User user) async { final response = await http.post( Uri.parse('$baseUrl/users'), headers: {'Content-Type': 'application/json; charset=UTF-8'}, body: json.encode(user.toJson()), ); if (response.statusCode == 201) { // JSONPlaceholder renvoie l'utilisateur créé avec un nouvel ID return User.fromJson(json.decode(response.body)); } else { throw Exception('Échec de la création de l\'utilisateur'); } } Future updateUser(User user) async { final response = await http.put( Uri.parse('$baseUrl/users/${user.id}'), headers: {'Content-Type': 'application/json; charset=UTF-8'}, body: json.encode(user.toJson()), ); if (response.statusCode == 200) { return User.fromJson(json.decode(response.body)); } else { throw Exception('Échec de la mise à jour de l\'utilisateur'); } } Future deleteUser(int id) async { final response = await http.delete(Uri.parse('$baseUrl/users/$id')); if (response.statusCode != 200) { throw Exception('Échec de la suppression de l\'utilisateur'); } } } Écran liste utilisateurs
lib/screens/users_screen.dartimport 'package:flutter/material.dart'; import '../models/user.dart'; import '../services/api_service.dart'; import 'user_detail_screen.dart'; import 'add_edit_user_screen.dart'; class UsersScreen extends StatefulWidget { const UsersScreen({super.key}); @override StatecreateState() => _UsersScreenState(); } class _UsersScreenState extends State { final ApiService _apiService = ApiService(); late Future - > _futureUsers;
final List
_users = []; @override void initState() { super.initState(); _futureUsers = _apiService.getUsers(); } void _refreshUsers() { setState(() { _futureUsers = _apiService.getUsers(); }); } void _navigateToAddUser() async { final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => const AddEditUserScreen()), ); if (result == true) { _refreshUsers(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Utilisateur créé avec succès')), ); } } void _navigateToEditUser(User user) async { final result = await Navigator.push( context, MaterialPageRoute(builder: (context) => AddEditUserScreen(user: user)), ); if (result == true) { _refreshUsers(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Utilisateur modifié avec succès')), ); } } void _deleteUser(int id) async { try { await _apiService.deleteUser(id); _refreshUsers(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Utilisateur supprimé avec succès')), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Erreur: ${e.toString()}')), ); } } void _showDeleteDialog(User user) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Confirmer la suppression'), content: Text('Êtes-vous sûr de vouloir supprimer ${user.name}?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), TextButton( onPressed: () { _deleteUser(user.id!); Navigator.of(context).pop(); }, child: const Text('Supprimer', style: TextStyle(color: Colors.red)), ), ], ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Utilisateurs JSONPlaceholder'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _refreshUsers, ), ], ), body: FutureBuilder - >(
future: _futureUsers,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Erreur: ${snapshot.error}'));
} else if (snapshot.hasData) {
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: ListTile(
leading: CircleAvatar(
child: Text(user.name[0]),
),
title: Text(user.name),
subtitle: Text(user.email),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserDetailScreen(user: user),
),
);
},
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => _navigateToEditUser(user),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _showDeleteDialog(user),
),
],
),
),
);
},
);
} else {
return const Center(child: Text('Aucun utilisateur trouvé'));
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: _navigateToAddUser,
child: const Icon(Icons.add),
),
);
}
}
Écran ajout/modification utilisateur
lib/screens/add_edit_user_screen.dartimport 'package:flutter/material.dart'; import '../models/user.dart'; import '../services/api_service.dart'; class AddEditUserScreen extends StatefulWidget { final User? user; const AddEditUserScreen({super.key, this.user}); @override StatecreateState() => _AddEditUserScreenState(); } class _AddEditUserScreenState extends State { final _formKey = GlobalKey (); final ApiService _apiService = ApiService(); late TextEditingController _nameController; late TextEditingController _usernameController; late TextEditingController _emailController; late TextEditingController _phoneController; late TextEditingController _websiteController; late TextEditingController _streetController; late TextEditingController _suiteController; late TextEditingController _cityController; late TextEditingController _zipcodeController; late TextEditingController _latController; late TextEditingController _lngController; late TextEditingController _companyNameController; late TextEditingController _catchPhraseController; late TextEditingController _bsController; @override void initState() { super.initState(); final user = widget.user; _nameController = TextEditingController(text: user?.name ?? ''); _usernameController = TextEditingController(text: user?.username ?? ''); _emailController = TextEditingController(text: user?.email ?? ''); _phoneController = TextEditingController(text: user?.phone ?? ''); _websiteController = TextEditingController(text: user?.website ?? ''); _streetController = TextEditingController(text: user?.address.street ?? ''); _suiteController = TextEditingController(text: user?.address.suite ?? ''); _cityController = TextEditingController(text: user?.address.city ?? ''); _zipcodeController = TextEditingController(text: user?.address.zipcode ?? ''); _latController = TextEditingController(text: user?.address.geo.lat ?? ''); _lngController = TextEditingController(text: user?.address.geo.lng ?? ''); _companyNameController = TextEditingController(text: user?.company.name ?? ''); _catchPhraseController = TextEditingController(text: user?.company.catchPhrase ?? ''); _bsController = TextEditingController(text: user?.company.bs ?? ''); } @override void dispose() { _nameController.dispose(); _usernameController.dispose(); _emailController.dispose(); _phoneController.dispose(); _websiteController.dispose(); _streetController.dispose(); _suiteController.dispose(); _cityController.dispose(); _zipcodeController.dispose(); _latController.dispose(); _lngController.dispose(); _companyNameController.dispose(); _catchPhraseController.dispose(); _bsController.dispose(); super.dispose(); } Future _saveUser() async { if (_formKey.currentState!.validate()) { try { final address = Address( street: _streetController.text, suite: _suiteController.text, city: _cityController.text, zipcode: _zipcodeController.text, geo: Geo( lat: _latController.text, lng: _lngController.text, ), ); final company = Company( name: _companyNameController.text, catchPhrase: _catchPhraseController.text, bs: _bsController.text, ); final user = User( id: widget.user?.id, name: _nameController.text, username: _usernameController.text, email: _emailController.text, phone: _phoneController.text, website: _websiteController.text, address: address, company: company, ); if (widget.user == null) { await _apiService.createUser(user); } else { await _apiService.updateUser(user); } Navigator.pop(context, true); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Erreur: ${e.toString()}')), ); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.user == null ? 'Ajouter un utilisateur' : 'Modifier l\'utilisateur'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: ListView( children: [ const Text('Informations personnelles', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), TextFormField( controller: _nameController, decoration: const InputDecoration(labelText: 'Nom'), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer un nom'; } return null; }, ), TextFormField( controller: _usernameController, decoration: const InputDecoration(labelText: 'Nom d\'utilisateur'), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer un nom d\'utilisateur'; } return null; }, ), TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer un email'; } if (!value.contains('@')) { return 'Veuillez entrer un email valide'; } return null; }, ), TextFormField( controller: _phoneController, decoration: const InputDecoration(labelText: 'Téléphone'), ), TextFormField( controller: _websiteController, decoration: const InputDecoration(labelText: 'Site web'), ), const SizedBox(height: 20), const Text('Adresse', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), TextFormField( controller: _streetController, decoration: const InputDecoration(labelText: 'Rue'), ), TextFormField( controller: _suiteController, decoration: const InputDecoration(labelText: 'Appartement/Suite'), ), TextFormField( controller: _cityController, decoration: const InputDecoration(labelText: 'Ville'), ), TextFormField( controller: _zipcodeController, decoration: const InputDecoration(labelText: 'Code postal'), ), TextFormField( controller: _latController, decoration: const InputDecoration(labelText: 'Latitude'), ), TextFormField( controller: _lngController, decoration: const InputDecoration(labelText: 'Longitude'), ), const SizedBox(height: 20), const Text('Entreprise', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), TextFormField( controller: _companyNameController, decoration: const InputDecoration(labelText: 'Nom de l\'entreprise'), ), TextFormField( controller: _catchPhraseController, decoration: const InputDecoration(labelText: 'Slogan'), ), TextFormField( controller: _bsController, decoration: const InputDecoration(labelText: 'BS'), ), const SizedBox(height: 20), ElevatedButton( onPressed: _saveUser, child: Text(widget.user == null ? 'Créer' : 'Mettre à jour'), ), ], ), ), ), ); } } Écran détails utilisateur
lib/screens/user_detail_screen.dartimport 'package:flutter/material.dart'; import '../models/user.dart'; class UserDetailScreen extends StatelessWidget { final User user; const UserDetailScreen({super.key, required this.user}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(user.name), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Informations personnelles', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), Text('Nom: ${user.name}'), Text('Nom d\'utilisateur: ${user.username}'), Text('Email: ${user.email}'), Text('Téléphone: ${user.phone}'), Text('Site web: ${user.website}'), ], ), ), ), const SizedBox(height: 16), Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Adresse', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), Text('Rue: ${user.address.street}'), Text('Appartement/Suite: ${user.address.suite}'), Text('Ville: ${user.address.city}'), Text('Code postal: ${user.address.zipcode}'), Text('Coordonnées: ${user.address.geo.lat}, ${user.address.geo.lng}'), ], ), ), ), const SizedBox(height: 16), Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Entreprise', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), Text('Nom: ${user.company.name}'), Text('Slogan: ${user.company.catchPhrase}'), Text('BS: ${user.company.bs}'), ], ), ), ), ], ), ), ); } }Fichier principal
lib/main.dartimport 'package:flutter/material.dart'; import 'screens/users_screen.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'JSONPlaceholder App', theme: ThemeData( primarySwatch: Colors.blue, useMaterial3: true, ), home: const UsersScreen(), debugShowCheckedModeBanner: false, ); } }
-