Conception de la couche réseau dans Flutter : isoler les impacts des changements d'API

En développement mobile, une modification côté serveur – comme le renommage d'un champ – peut entraîner des dizaines de fichiers à corriger côté client. Lorsque la structure de réponse évolue, par exemple avec l'ajout d'un niveau d'enveloppe data, les erreurs se multiplient. Ce n'est souvent pas l'instabilité de l'API qui est en cause, mais bien la conception de la couche réseau de l'application.

Si la couche d'interface utilisateur (UI) dépend directement du format JSON brut, toute évolution de l'API aura un impact direct sur la majorité des écrans. Une approceh par couches permet d'introduire une barrière d'abstraction essentielle.

Architecture en couches pour maîtriser les changements

Une structure robuste s'articule généralement autour de trois niveaux :

Couche UI (Widgets)
    ↓
Couche Repository
    ↓
Couche DataSource (Client HTTP, ex: Dio)

DataSource : Gère les appels réseau, le parsing initial du JSON et la levée d'exceptions standardisées.

Repository : Transforme les objets de transfert (DTO) en modèles métier (Entity) et applique une logique de cache ou de fusion si nécessaire.

UI : Ne consomme que les modèles métier, sans connaissance de la structure de l'API.

Lorsqu'un champ d'API change, la modification est confinée au niveau du DataSource ou du DTO, laissant la couche UI inchangée.

Modèles de données : DTO vs Entity

La séparation des modèles n'est pas une formalité ; c'est un investissement pour la stabilité à long terme.

// DTO : reflète la structure exacte de l'API
class UserDTO {
  final String username;
  UserDTO.fromJson(Map<string dynamic=""> json)
      : username = json['username'] ?? '';
}

// Entity : modèle utilisé dans la logique applicative
class User {
  final String displayName;
  User(this.displayName);
}
</string>

La conversion s'effectue au sein du Repository :

User transformToUser(UserDTO dto) {
  return User(dto.username);
}

En cas de changement dans l'API (ex: usernamefullName), seule la classe UserDTO et la fonction de transformation sont ajustées.

Gestion centralisée des erreurs

Un traitement des erreurs dispersé dans chaque écran est source d'incohérence et de maintenance difficile. Il est préférable de définir des types d'exceptions applicatifs :

sealed class ApiException implements Exception {}
class NetworkException extends ApiException {}
class ServerException extends ApiException { final int code; ServerException(this.code); }
class UnauthorizedException extends ApiException {}

Un intercepteur au niveau du client HTTP peut alors gérer les cas globaux (comme le rafraîchissement d'un token) et propager les erreurs normalisées.

final dio = Dio();
dio.interceptors.add(InterceptorsWrapper(
  onError: (error, handler) {
    if (error.response?.statusCode == 401) {
      // Logique de rafraîchissement ou de déconnexion globale
    }
    // On propage une exception typée
    handler.next(error);
  },
));

Cette centralisation évite la duplication du code de gestion d'erreur et garantit un comportement uniforme dans toute l'application.

Isoler l'interface utilisateur de la structure de l'API

Un principe fondamental est que la couche UI ne doit jamais parser de JSON brut. Elle travaille exclusivement avec des modèles métier (Entity).

// À éviter : parsing direct dans le widget
final name = responseData['data']['user']['username'];

// Préférable : utilisation d'un modèle métier
final user = userRepository.currentUser;
Text(user.displayName);

Cette séparation garantit que les modifications de l'API restent sans impact sur l'interface.

Stratégies d'évolution et Mocking

Pour anticiper les changements, plusieurs pratiques sont utiles :

  • Couche Mock : Permet de développer les écrans indépendamment de l'API réelle et d'effectuer des tests reproductibles.
  • Versionnement d'API : Gérer les différentes versions (/v1/, /v2/) permet une transition progressive.
  • Compatibilité ascendante : Temporairement accepter à la fois l'ancien et le nouveau format d'API pendant les transitions.

Ces mécanismes réduisent le couplage et offrent une flexibilité face aux évolutions inévitables des interfaces.

La valeur d'une architecture réseau bien pensée ne réside pas seulement dans son fonctionnement initial, mais dans sa capacité à absorber les changements avec un impact minimal sur le code de l'application.

Étiquettes: Flutter Dart architecture réseau API séparation des responsabilités

Publié le 6 juillet à 05h19