Connecter Flutter avec une API REST étape par étape

Connecter Flutter avec une API REST étape par étape

Il y a quelque chose de très satisfaisant dans le fait de voir une application Flutter dialoguer avec un vrai backend. Au début, on construit l’interface, on aligne des boutons, on soigne les marges, on pense aux couleurs, puis vient le moment où l’application doit enfin vivre. Elle doit lire des données, en envoyer, se synchroniser avec un serveur, afficher des messages d’erreur, réagir quand la connexion est lente, et parfois même fonctionner avec des identifiants, des jetons d’accès ou des pages de résultats. C’est là que l’intégration avec une API REST devient centrale. Et bonne nouvelle : Flutter s’y prête très bien, à condition de prendre le sujet dans le bon ordre, sans brûler les étapes, et en construisant une base propre dès le départ.

Dans cet article, nous allons voir comment connecter Flutter à une API REST étape par étape, de manière claire, concrète et réaliste. Nous allons partir d’un cas simple, puis enrichir progressivement l’exemple pour couvrir les situations qu’on rencontre réellement dans un projet : récupération de données en GET, envoi de données en POST, sérialisation JSON, gestion des erreurs, architecture en couches, chargement, authentification, mise à jour d’interface et bonnes pratiques de maintenance. L’objectif n’est pas seulement de faire “marcher” l’application. L’objectif est de faire quelque chose de propre, compréhensible et extensible, pour ne pas avoir à tout réécrire le lendemain.

Comprendre ce qu’est une API REST

Avant d’écrire la moindre ligne de code, il faut poser une base simple. Une API REST est une interface qui permet à une application cliente, comme une application Flutter, de communiquer avec un serveur via des requêtes HTTP. Le principe est très pratique : au lieu de stocker toutes les informations localement, l’application demande ce dont elle a besoin au serveur, reçoit une réponse, puis affiche ces données à l’utilisateur. Cela peut être une liste d’articles, un profil utilisateur, une commande, un commentaire, une tâche, une facture, ou n’importe quelle ressource exposée par le backend.

Dans la plupart des cas, on utilise les verbes HTTP suivants : GET pour lire des données, POST pour créer une ressource, PUT ou PATCH pour modifier, et DELETE pour supprimer. Le format de réponse le plus courant est JSON, parce qu’il est léger, lisible et très bien supporté par Dart. Flutter n’a donc pas besoin de magie particulière : il lui faut surtout une manière propre de faire des requêtes, de convertir le JSON en objets Dart, et de gérer les états de l’interface selon que la requête réussisse, échoue ou attend une réponse.

Préparer le projet Flutter

Commençons par créer un projet Flutter classique, puis ajoutons le paquet http, qui reste l’un des moyens les plus simples pour consommer une API REST.

flutter create flutter_api_demo
cd flutter_api_demo
flutter pub add http

Ce paquet nous permettra d’effectuer des requêtes HTTP simplement depuis Dart. Dans un projet plus avancé, vous pourriez aussi utiliser dio, qui offre davantage de fonctionnalités avancées comme les interceptors, l’annulation des requêtes ou la gestion plus poussée des fichiers. Mais pour apprendre proprement les bases, http est parfait.

Ensuite, pensez à organiser votre projet dès le départ. Beaucoup de développeurs débutants placent toute la logique dans main.dart, puis se retrouvent avec un fichier énorme, difficile à lire et compliqué à faire évoluer. Une structure plus saine ressemble à ceci :

lib/
  main.dart
  models/
    user.dart
  services/
    api_service.dart
  screens/
    home_screen.dart
  widgets/
    user_tile.dart

Cette séparation n’est pas juste “jolie”. Elle vous aide à garder une application compréhensible. Les modèles représentent les données, les services s’occupent de la communication avec l’API, les écrans gèrent l’interface, et les widgets réutilisables simplifient l’affichage.

Choisir une API de test

Pour cet article, nous allons imaginer qu’une API REST renvoie une liste d’utilisateurs. Cela permet d’illustrer clairement la récupération de données, l’affichage et même la création d’éléments. Vous pouvez adapter le même modèle à un blog, un catalogue de produits, un tableau de tâches ou un système d’authentification.

Supposons que le backend expose cette route :

GET https://jsonplaceholder.typicode.com/users

La réponse ressemble à une liste d’objets JSON contenant des informations comme id, name, email, phone et website. C’est un très bon terrain d’entraînement, même si, dans une vraie application, l’API viendra souvent de votre propre backend Laravel, Node.js, Django, FastAPI ou Spring Boot.

Créer le modèle Dart

Quand une API renvoie du JSON, il est fortement conseillé de le transformer en objet Dart. Cela rend le code beaucoup plus lisible et évite de manipuler des Map<String, dynamic> partout. Voici un modèle simple pour représenter un utilisateur :

class User {
  final int id;
  final String name;
  final String email;
  final String phone;
  final String website;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.phone,
    required this.website,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] ?? 0,
      name: json['name'] ?? '',
      email: json['email'] ?? '',
      phone: json['phone'] ?? '',
      website: json['website'] ?? '',
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'phone': phone,
      'website': website,
    };
  }
}

Ce petit morceau de code est très important. Il assure la conversion du JSON vers un objet exploitable par Flutter, et inversement si vous devez envoyer des données vers l’API. Quand on travaille avec des applications sérieuses, cette étape devient presque incontournable.

Créer le service API

La logique de communication avec l’API doit être séparée de l’interface. C’est le rôle du service. Voici un exemple simple de service qui récupère la liste des utilisateurs :

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<List<User>> fetchUsers() async {
    final url = Uri.parse('$baseUrl/users');

    final response = await http.get(url);

    if (response.statusCode == 200) {
      final List<dynamic> data = jsonDecode(response.body);
      return data.map((json) => User.fromJson(json)).toList();
    } else {
      throw Exception('Erreur lors du chargement des utilisateurs');
    }
  }
}

Ce service fait trois choses essentielles : il construit l’URL, il envoie la requête, puis il transforme la réponse JSON en liste d’objets User. C’est simple, mais déjà très utile. Dans la pratique, il faut souvent enrichir cette logique avec des délais d’attente, des en-têtes HTTP, des jetons d’authentification et une meilleure gestion des erreurs.

Afficher les données dans l’interface

Maintenant que nous avons un modèle et un service, il est temps d’afficher les données dans l’écran principal. Pour cela, nous allons utiliser un FutureBuilder, qui est très pratique pour gérer une requête asynchrone.

import 'package:flutter/material.dart';
import '../models/user.dart';
import '../services/api_service.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final ApiService apiService = ApiService();

  late Future<List<User>> usersFuture;

  @override
  void initState() {
    super.initState();
    usersFuture = apiService.fetchUsers();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Utilisateurs'),
      ),
      body: FutureBuilder<List<User>>(
        future: usersFuture,
        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 || snapshot.data!.isEmpty) {
            return const Center(
              child: Text('Aucun utilisateur trouvé'),
            );
          }

          final users = snapshot.data!;

          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];
              return ListTile(
                leading: CircleAvatar(
                  child: Text(user.name.isNotEmpty ? user.name[0] : '?'),
                ),
                title: Text(user.name),
                subtitle: Text(user.email),
              );
            },
          );
        },
      ),
    );
  }
}

Ce code donne déjà une application fonctionnelle. L’écran affiche un loader pendant le chargement, puis montre les utilisateurs ou un message d’erreur si la requête échoue. Ce genre de structure est très important, car l’utilisateur ne doit jamais avoir l’impression que l’application est “figée” sans raison.

Lancer l’application

Votre main.dart peut rester très simple :

import 'package:flutter/material.dart';
import 'screens/home_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter API Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomeScreen(),
    );
  }
}

À ce stade, vous avez déjà un flux complet : Flutter appelle une API REST, reçoit les données, les transforme, puis les affiche. C’est la base solide sur laquelle on peut construire tout le reste.

Ajouter une requête POST

Lire des données, c’est bien. En envoyer, c’est encore mieux. Une bonne application REST ne se limite pas à l’affichage : elle permet aussi de créer des ressources. Prenons un exemple de création d’utilisateur.

Dans notre service :

Future<User> createUser({
  required String name,
  required String email,
  required String phone,
  required String website,
}) async {
  final url = Uri.parse('$baseUrl/users');

  final response = await http.post(
    url,
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode({
      'name': name,
      'email': email,
      'phone': phone,
      'website': website,
    }),
  );

  if (response.statusCode == 201 || response.statusCode == 200) {
    final Map<String, dynamic> data = jsonDecode(response.body);
    return User.fromJson(data);
  } else {
    throw Exception('Erreur lors de la création de l’utilisateur');
  }
}

Ici, nous envoyons un corps JSON avec la méthode POST. Le serveur peut répondre avec un code 201 Created, ce qui signifie que la ressource a bien été créée. Attention toutefois : certaines API renvoient 200 OK même pour une création. Il faut donc rester souple selon le backend utilisé.

Vous pouvez ensuite déclencher cette méthode depuis un formulaire. Un bouton “Créer” peut envoyer les données à l’API, puis actualiser la liste affichée à l’écran.

Construire un formulaire simple

Un vrai projet nécessite souvent une interface pour saisir les données. Voici un exemple de formulaire minimaliste :

final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
final _websiteController = TextEditingController();

@override
void dispose() {
  _nameController.dispose();
  _emailController.dispose();
  _phoneController.dispose();
  _websiteController.dispose();
  super.dispose();
}

Puis dans le build :

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        controller: _nameController,
        decoration: const InputDecoration(labelText: 'Nom'),
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'Le nom est obligatoire';
          }
          return null;
        },
      ),
      TextFormField(
        controller: _emailController,
        decoration: const InputDecoration(labelText: 'Email'),
      ),
      TextFormField(
        controller: _phoneController,
        decoration: const InputDecoration(labelText: 'Téléphone'),
      ),
      TextFormField(
        controller: _websiteController,
        decoration: const InputDecoration(labelText: 'Site web'),
      ),
      ElevatedButton(
        onPressed: () async {
          if (_formKey.currentState!.validate()) {
            final user = await apiService.createUser(
              name: _nameController.text,
              email: _emailController.text,
              phone: _phoneController.text,
              website: _websiteController.text,
            );

            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Utilisateur créé : ${user.name}')),
            );
          }
        },
        child: const Text('Créer'),
      ),
    ],
  ),
)

Le formulaire n’est pas seulement un moyen de saisir des données. C’est aussi le lieu où vous rassurez l’utilisateur. Un bouton qui réagit, une validation qui parle clairement, un message de succès ou d’échec, tout cela participe à une expérience plus fluide et plus humaine.

Gérer les erreurs proprement

L’intégration API ne se passe pas toujours parfaitement. Et c’est normal. Il peut y avoir une coupure réseau, un serveur indisponible, une URL incorrecte, un problème de token, une réponse vide ou un JSON mal formé. Un bon développeur Flutter ne cherche pas seulement à faire marcher le “happy path”. Il prépare aussi les cas de panne.

Voici une version plus robuste du service :

Future<List<User>> fetchUsers() async {
  final url = Uri.parse('$baseUrl/users');

  try {
    final response = await http
        .get(url)
        .timeout(const Duration(seconds: 10));

    if (response.statusCode == 200) {
      final List<dynamic> data = jsonDecode(response.body);
      return data.map((json) => User.fromJson(json)).toList();
    } else if (response.statusCode == 401) {
      throw Exception('Non autorisé');
    } else if (response.statusCode == 404) {
      throw Exception('Ressource introuvable');
    } else {
      throw Exception('Erreur serveur : ${response.statusCode}');
    }
  } catch (e) {
    throw Exception('Impossible de charger les données : $e');
  }
}

L’ajout d’un timeout est utile, car il évite d’attendre indéfiniment une réponse. Le message d’erreur devient plus explicite et plus exploitable, aussi bien pour le développeur que pour l’utilisateur final. Dans une vraie application, vous pourriez même afficher des messages différents selon le type d’erreur, par exemple “Vérifiez votre connexion internet” ou “Session expirée, veuillez vous reconnecter”.

Ajouter des en-têtes HTTP

Dans les projets réels, une API REST demande souvent des en-têtes spécifiques : type de contenu, langue, token d’accès, version de l’application ou identifiant client. Les en-têtes sont transmis dans chaque requête pour aider le serveur à comprendre le contexte.

Exemple :

headers: {
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Authorization': 'Bearer $token',
}

Le token est souvent obtenu après connexion de l’utilisateur. C’est un point crucial dans les applications modernes. Si votre API exige une authentification, Flutter doit envoyer ce jeton dans les requêtes protégées. Sans cela, le serveur renverra souvent 401 Unauthorized.

Gérer l’authentification par token

Un scénario très courant consiste à connecter l’utilisateur, recevoir un token, puis utiliser ce token pour accéder aux routes protégées. Le principe est le suivant : l’utilisateur saisit ses identifiants, l’application appelle l’API de connexion, le backend vérifie les informations, puis renvoie un jeton d’accès. Ensuite, Flutter stocke ce jeton de manière sécurisée et l’envoie dans les requêtes suivantes.

Exemple de login :

Future<String> login(String email, String password) async {
  final url = Uri.parse('$baseUrl/login');

  final response = await http.post(
    url,
    headers: {
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'email': email,
      'password': password,
    }),
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    return data['token'];
  } else {
    throw Exception('Échec de connexion');
  }
}

Dans une vraie application, il ne suffit pas de garder ce token dans une variable mémoire. Il faut le stocker correctement, souvent avec flutter_secure_storage, afin de le conserver entre les sessions sans exposer les données sensibles.

Stocker et relire le token

Pour sécuriser le token, on utilise souvent le paquet suivant :

flutter pub add flutter_secure_storage

Puis :

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenStorage {
  final _storage = const FlutterSecureStorage();

  Future<void> saveToken(String token) async {
    await _storage.write(key: 'auth_token', value: token);
  }

  Future<String?> getToken() async {
    return await _storage.read(key: 'auth_token');
  }

  Future<void> deleteToken() async {
    await _storage.delete(key: 'auth_token');
  }
}

Cette petite couche améliore énormément la qualité de l’application. Elle vous permet de relancer l’app sans obliger l’utilisateur à se reconnecter à chaque fois. Et surtout, elle vous aide à garder une architecture plus nette.

Mettre à jour l’interface après une action API

Une fois les données créées, modifiées ou supprimées, l’interface doit souvent se rafraîchir. C’est un détail qui paraît évident, mais qui est souvent oublié dans les premières versions. Après une opération POST, PUT ou DELETE, il faut soit recharger les données, soit mettre à jour l’état local directement.

Par exemple, après une création :

await apiService.createUser(
  name: _nameController.text,
  email: _emailController.text,
  phone: _phoneController.text,
  website: _websiteController.text,
);

setState(() {
  usersFuture = apiService.fetchUsers();
});

Ce simple setState relance le chargement de la liste. Pour des applications plus évoluées, vous préférerez sans doute un gestionnaire d’état plus structuré comme Provider, Riverpod, Bloc ou Redux. Le but reste le même : faire en sorte que l’interface reflète correctement l’état réel des données.

Utiliser Provider pour mieux organiser l’état

Quand l’application devient plus grande, FutureBuilder et setState peuvent commencer à montrer leurs limites. Dans ce cas, un gestionnaire d’état léger comme Provider peut rendre le code beaucoup plus maintenable.

Installation :

flutter pub add provider

Exemple d’un ChangeNotifier :

import 'package:flutter/material.dart';
import '../models/user.dart';
import '../services/api_service.dart';

class UserProvider extends ChangeNotifier {
  final ApiService _apiService = ApiService();

  List<User> _users = [];
  bool _isLoading = false;
  String? _errorMessage;

  List<User> get users => _users;
  bool get isLoading => _isLoading;
  String? get errorMessage => _errorMessage;

  Future<void> loadUsers() async {
    _isLoading = true;
    _errorMessage = null;
    notifyListeners();

    try {
      _users = await _apiService.fetchUsers();
    } catch (e) {
      _errorMessage = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

Et dans l’interface, vous pouvez écouter ce provider pour afficher automatiquement les changements. Cette approche sépare clairement la logique métier de l’UI, ce qui devient très confortable sur les projets professionnels.

Gérer les listes volumineuses et la pagination

Dans beaucoup d’applications, l’API ne renvoie pas toute la liste d’un coup. Elle renvoie une page de résultats à la fois, avec des paramètres comme page, limit ou offset. C’est une très bonne pratique, car elle réduit la charge serveur et améliore les performances côté application.

Exemple :

Future<List<User>> fetchUsers({int page = 1}) async {
  final url = Uri.parse('$baseUrl/users?page=$page');

  final response = await http.get(url);

  if (response.statusCode == 200) {
    final List<dynamic> data = jsonDecode(response.body);
    return data.map((json) => User.fromJson(json)).toList();
  } else {
    throw Exception('Erreur pagination');
  }
}

Côté interface, vous pouvez charger la page suivante quand l’utilisateur atteint le bas de la liste. Cela donne une sensation plus fluide et évite de saturer la mémoire avec des milliers d’éléments chargés en une seule fois.

Rafraîchir les données avec pull to refresh

Les utilisateurs aiment pouvoir actualiser les informations eux-mêmes. Le geste “tirer vers le bas pour rafraîchir” est devenu presque naturel. Flutter le permet facilement grâce à RefreshIndicator.

RefreshIndicator(
  onRefresh: () async {
    setState(() {
      usersFuture = apiService.fetchUsers();
    });
  },
  child: ListView.builder(
    itemCount: users.length,
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(users[index].name),
      );
    },
  ),
)

C’est une petite fonctionnalité, mais elle améliore beaucoup l’expérience utilisateur. Elle montre aussi que l’application sait se mettre à jour sans demander à l’utilisateur de tout fermer puis relancer.

Supprimer une ressource avec DELETE

La suppression est aussi une opération fréquente. Voici un exemple classique :

Future<void> deleteUser(int id) async {
  final url = Uri.parse('$baseUrl/users/$id');

  final response = await http.delete(url);

  if (response.statusCode == 200 || response.statusCode == 204) {
    return;
  } else {
    throw Exception('Erreur lors de la suppression');
  }
}

Et dans l’interface :

IconButton(
  icon: const Icon(Icons.delete),
  onPressed: () async {
    await apiService.deleteUser(user.id);
    setState(() {
      usersFuture = apiService.fetchUsers();
    });
  },
)

On oublie souvent de soigner le retour visuel après une suppression. Il est pourtant très utile d’afficher une confirmation, un message de succès ou même une possibilité d’annulation temporaire. Une bonne UX, ce n’est pas seulement du beau design ; c’est aussi du bon sens.

Transformer le JSON avec attention

Le JSON est simple, mais il peut devenir piégeux quand les données ne sont pas exactement celles que vous attendez. Certains champs peuvent être null, des nombres peuvent arriver sous forme de chaînes, ou certaines clés peuvent être absentes. Il faut donc garder un code de conversion prudent.

Exemple :

factory User.fromJson(Map<String, dynamic> json) {
  return User(
    id: json['id'] is int ? json['id'] : int.tryParse(json['id'].toString()) ?? 0,
    name: json['name']?.toString() ?? '',
    email: json['email']?.toString() ?? '',
    phone: json['phone']?.toString() ?? '',
    website: json['website']?.toString() ?? '',
  );
}

Ce type de protection évite beaucoup de crashs inutiles. Dans une vraie application, les données ne sont jamais aussi parfaites que dans un exemple de tutoriel. Un backend évolue, un champ devient optionnel, une API tierce change légèrement, et soudain le client mobile casse. Mieux vaut anticiper.

Gérer les temps de chargement avec élégance

Un écran qui attend une réponse API doit rassurer l’utilisateur. Il ne doit pas sembler vide ni bloqué. Les indicateurs de chargement, les skeleton loaders et les placeholders sont très utiles. Le plus simple reste :

const Center(
  child: CircularProgressIndicator(),
)

Mais dans une application plus raffinée, vous pouvez afficher une structure de contenu grisé, proche du rendu final. L’utilisateur perçoit alors que l’application travaille pour lui, ce qui rend l’expérience plus agréable.

Séparer les responsabilités

Si vous retenez une seule idée de cet article, retenez celle-ci : ne mélangez pas tout. Une application Flutter qui consomme une API REST doit idéalement respecter une séparation claire :

Le modèle décrit la structure des données.
Le service gère les appels réseau.
Le gestionnaire d’état orchestre les données et les chargements.
L’écran affiche le contenu.
Le widget réutilisable simplifie l’interface.

Cette discipline rend le code plus facile à relire, à tester et à faire évoluer. Au départ, cela peut sembler un peu plus long. Mais très vite, on gagne énormément de temps. Le jour où vous devez changer d’API, ajouter un filtre ou modifier le format des réponses, vous serez heureux d’avoir pris cette habitude.

Exemple complet d’architecture simple

Voici une version condensée mais cohérente de tout ce que nous avons vu.

models/user.dart

class User {
  final int id;
  final String name;
  final String email;
  final String phone;
  final String website;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.phone,
    required this.website,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] ?? 0,
      name: json['name'] ?? '',
      email: json['email'] ?? '',
      phone: json['phone'] ?? '',
      website: json['website'] ?? '',
    );
  }
}

services/api_service.dart

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<List<User>> fetchUsers() async {
    final url = Uri.parse('$baseUrl/users');
    final response = await http.get(url);

    if (response.statusCode == 200) {
      final List<dynamic> data = jsonDecode(response.body);
      return data.map((json) => User.fromJson(json)).toList();
    } else {
      throw Exception('Erreur de chargement');
    }
  }

  Future<User> createUser(User user) async {
    final url = Uri.parse('$baseUrl/users');

    final response = await http.post(
      url,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(user.toJson()),
    );

    if (response.statusCode == 200 || response.statusCode == 201) {
      final Map<String, dynamic> data = jsonDecode(response.body);
      return User.fromJson(data);
    } else {
      throw Exception('Erreur de création');
    }
  }
}

screens/home_screen.dart

import 'package:flutter/material.dart';
import '../models/user.dart';
import '../services/api_service.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final ApiService apiService = ApiService();
  late Future<List<User>> usersFuture;

  @override
  void initState() {
    super.initState();
    usersFuture = apiService.fetchUsers();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Connexion API REST'),
      ),
      body: FutureBuilder<List<User>>(
        future: usersFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          if (snapshot.hasError) {
            return Center(
              child: Text('Erreur : ${snapshot.error}'),
            );
          }

          final users = snapshot.data ?? [];

          return RefreshIndicator(
            onRefresh: () async {
              setState(() {
                usersFuture = apiService.fetchUsers();
              });
            },
            child: ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) {
                final user = users[index];
                return ListTile(
                  title: Text(user.name),
                  subtitle: Text(user.email),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

Ce mini-projet montre déjà une vraie logique applicative : récupérer des données, les afficher, rafraîchir la liste et structurer le code intelligemment.

Bonnes pratiques à retenir

Même si Flutter simplifie beaucoup de choses, une intégration API REST réussie dépend surtout de votre discipline technique. Voici quelques habitudes précieuses : toujours prévoir la gestion des erreurs, séparer les couches de responsabilité, éviter de mettre toute la logique réseau dans l’interface, transformer le JSON en modèles propres, tester les cas de connexion lente, et garder une structure de projet évolutive.

Il faut aussi penser à la sécurité. N’envoyez jamais de données sensibles sans HTTPS. Évitez d’exposer des secrets dans le code source. Stockez les tokens correctement. Validez les entrées utilisateur avant de les envoyer au serveur. Et surtout, ne supposez jamais que l’API renverra toujours des données parfaites. Une application robuste est une application qui sait rester calme quand le backend ne l’est pas.

Aller plus loin avec Flutter et REST

Une fois les bases maîtrisées, vous pourrez aller beaucoup plus loin. Vous pourrez ajouter la pagination infinie, la recherche côté serveur, le tri, le filtrage, le cache local, le mode hors ligne, la synchronisation différée, les uploads de fichiers, les websockets pour le temps réel, les notifications push, ou encore la gestion multi-environnements avec des URLs différentes selon le mode développement ou production.

Vous pourrez aussi créer une architecture plus avancée avec Repository Pattern, Clean Architecture ou Domain Driven Design. Ce n’est pas obligatoire au début, mais ce sont de très bonnes directions quand l’application grandit. L’important est d’avancer étape par étape, sans complexifier inutilement ce qui peut encore rester simple.

Conclusion

Connecter Flutter à une API REST n’est pas seulement une compétence technique. C’est une manière de donner vie à une application mobile. C’est le passage entre une interface statique et un produit réellement utile, connecté, réactif et capable de dialoguer avec un backend. En partant d’un simple GET, puis en ajoutant un modèle propre, un service dédié, des requêtes POST, la gestion des erreurs, l’authentification et une structure claire, vous construisez bien plus qu’un exemple de code : vous construisez une base solide pour des applications sérieuses.

Le plus important est de garder une approche progressive. Commencez simple. Faites fonctionner la récupération des données. Puis ajoutez l’envoi. Ensuite seulement, améliorez l’architecture, la sécurité et l’expérience utilisateur. C’est souvent comme cela que naissent les projets durables : un petit pas bien fait, puis un autre, puis un autre, jusqu’à obtenir une application fluide, stable et agréable à maintenir.

Et au fond, c’est peut-être cela le plus beau avec Flutter : on part d’un écran vide, et peu à peu, avec une API REST bien connectée, on voit l’application respirer, apprendre, réagir, évoluer. C’est à ce moment-là qu’un simple projet devient vraiment un produit.

Bonus : un exemple de flux complet en quelques lignes

Pour finir, voici un résumé ultra concret du cycle complet :

final response = await http.get(Uri.parse('https://api.example.com/users'));

if (response.statusCode == 200) {
  final data = jsonDecode(response.body);
  final users = data.map((e) => User.fromJson(e)).toList();
  setState(() {
    // afficher les utilisateurs
  });
} else {
  // gérer l'erreur
}

Ce petit enchaînement contient l’essentiel : appel réseau, lecture de la réponse, transformation des données, mise à jour de l’interface. Tout le reste n’est qu’une montée en maturité autour de ce cœur.

#Flutter #API REST #Dart #HTTP #développement mobile #intégration API #JSON #GET #POST #authentification #meilleures pratiques #application mobile #tutoriel Flutter #REST API Flutter

Abonnez-vous à notre newsletter

12k+

Abonnés

Hebdomadaire

Fréquence

Gratuit

Toujours