Application Flutter pour consommer JSONPlaceholder API
Sommaire
- 1- Objectif
- 2- Structure du projet
- 3- Code complet
- 3.1- Fichier
pubspec.yaml
- 3.2- Modèle
lib/models/user.dart
- 3.3- Service API
lib/services/api_service.dart
- 3.4- Écran liste utilisateurs
lib/screens/users_screen.dart
- 3.5- Écran ajout/modification utilisateur
lib/screens/add_edit_user_screen.dart
- 3.6- Écran détails utilisateur
lib/screens/user_detail_screen.dart
- 3.7- Fichier principal
lib/main.dart
- 3.7.1- Cours Flutter
Consommation d’API REST et GraphQL
-
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
-
Code complet
-
Fichier
pubspec.yaml
-
Modèle
lib/models/user.dart
-
Service API
lib/services/api_service.dart
-
Écran liste utilisateurs
lib/screens/users_screen.dart
-
Écran ajout/modification utilisateur
lib/screens/add_edit_user_screen.dart
-
Écran détails utilisateur
lib/screens/user_detail_screen.dart
-
Fichier principal
lib/main.dart
-
lib/
├── main.dart
├── models/
│ └── user.dart
├── services/
│ └── api_service.dart
└── screens/
├── users_screen.dart
├── user_detail_screen.dart
└── add_edit_user_screen.dart
name: 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
class 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(Map json) {
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,
);
}
}
import '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');
}
}
}
import '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
State createState() => _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),
),
);
}
}
import '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
State createState() => _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'),
),
],
),
),
),
);
}
}
import '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}'),
],
),
),
),
],
),
),
);
}
}
import '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,
);
}
}