Exemples

Étude de cas : Conscience réseau

Les requêtes se mettent automatiquement en pause hors ligne, les mutations se mettent en file pour plus tard, et tout rejoue à la reconnexion : avec prévention du thundering-herd intégrée.

Objectif

Contexte : Quand un appareil perd la connectivité, une implémentation naïve soit plante (exception réseau non capturée), soit gèle (spinner qui ne se résout jamais), soit abandonne silencieusement la dernière action de l'utilisateur (mutation perdue à jamais). Aucune de ces options n'est acceptable dans une app de production.

Cet exemple démontre la couche de conscience réseau de Qora : les requêtes se mettent automatiquement en pause hors ligne et rejouent à la reconnexion, les mutations sont mises en file et rejouées dans l'ordre FIFO, et le rejeu à la reconnexion utilise une concurrence contrôlée pour qu'un afflux soudain de requêtes en pause ne surcharge pas le serveur.


Fonctionnalités démontrées

  • NetworkMode.online (défaut) : les requêtes se mettent en pause à FetchStatus.paused hors ligne ; rejouent automatiquement à la reconnexion
  • NetworkMode.offlineFirst : sert les données en cache immédiatement, planifie la revalidation en arrière-plan quand de retour en ligne
  • NetworkMode.always : contourne entièrement les vérifications de connectivité (pour les requêtes sauvegardées en base de données locale)
  • FetchStatus.paused : expose l'état "en attente de connexion" à l'UI sans aucun booléen personnalisé
  • OfflineMutationQueue : file FIFO ; les mutations déclenchées hors ligne sont rejouées dans l'ordre à la reconnexion
  • ReconnectStrategy : maxConcurrent + jitter préviennent le thundering-herd quand des dizaines de requêtes reprennent simultanément
  • NetworkStatusIndicator : widget de bannière hors ligne automatique, zéro configuration
  • FlutterConnectivityManager : câble connectivity_plus dans QoraScope en une seule ligne

Points d'architecture

La conscience réseau est orthogonale à l'état des requêtes. Une requête peut être dans l'état Success (données visibles) pendant que FetchStatus est paused (connectivité perdue, revalidation en arrière-plan en attente). L'arbre de widgets n'est jamais bloqué : il a toujours quelque chose à rendre.

main.dart
└── QoraScope(
      client: QoraClient(config: reconnectStrategy + networkMode)
      connectivityManager: FlutterConnectivityManager()  ← branche connectivity_plus
      lifecycleManager: FlutterLifecycleManager(...)     ← refetch à la reprise
      child: NetworkStatusIndicator(                ← bannière hors ligne, zéro config
        child: MyApp(),
      ),
    )

QoraClient est configuré avec une ReconnectStrategy qui contrôle comment les requêtes en pause sont rejouées quand la connectivité revient :

lib/main.dart
final client = QoraClient(
  config: QoraClientConfig(
    defaultOptions: const QoraOptions(
      networkMode: NetworkMode.online, // pause hors ligne, rejeu à la reconnexion
      staleTime: Duration(minutes: 5),
    ),
    reconnectStrategy: const ReconnectStrategy(
      maxConcurrent: 5, // rejouer 5 requêtes à la fois
      jitter: Duration(milliseconds: 200), // délai aléatoire entre les lots
    ),
    debugMode: kDebugMode,
  ),
);

runApp(
  QoraScope(
    client: client,
    connectivityManager: FlutterConnectivityManager(),
    lifecycleManager:    FlutterLifecycleManager(qoraClient: client),
    child: NetworkStatusIndicator(child: const MyApp()),
  ),
);

Détails d'implémentation

ReconnectStrategy : prévention du thundering-herd

Considérez une app avec 20 requêtes actives sur 8 écrans. L'utilisateur prend le métro pendant 20 minutes : les 20 requêtes se mettent en pause. À la reconnexion, sans stratégie, 20 requêtes simultanées se déclenchent. Si 200 utilisateurs sortent de la même station de métro en même temps, le serveur reçoit 4 000 requêtes simultanées.

ReconnectStrategy(maxConcurrent: 5, jitter: Duration(milliseconds: 200)) étale le rejeu en lots de 5, avec un délai aléatoire (jusqu'à 200 ms) entre chaque lot :

lib/main.dart
const ReconnectStrategy(
  maxConcurrent: 5,
  jitter: Duration(milliseconds: 200),
)

// Constructeurs nommés pour les configurations courantes :
ReconnectStrategy.instant() // tout à la fois : utile pour les tests
ReconnectStrategy.conservative() // maxConcurrent: 2, jitter: 500ms : économe en batterie

Le jitter est aléatoire par instance de client : ainsi, même si 1 000 appareils se reconnectent simultanément, leurs fenêtres de rejeu sont réparties sur 200 ms, distribuant la charge.

FetchStatus.paused : état UI précis sans flags personnalisés

QoraBuilder expose FetchStatus en tant que troisième argument du builder. paused signifie "un fetch est en file mais attend la connectivité", distinct de fetching (en cours d'exécution) et idle (rien en attente). Cela permet aux widgets de rendre un indicateur hors ligne précis sans un seul bool _isOffline :

lib/features/feed/ui/feed_screen.dart
QoraBuilder<List<Post>>(
  queryKey: const ['posts'],
  fetcher: api.getPosts,
  builder: (context, state, fetchStatus) {
    return switch (state) {
      // Premier chargement, hors ligne: rien à afficher encore
      Initial() || Loading(previousData: null) when fetchStatus == FetchStatus.paused =>
          const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.wifi_off, size: 48, color: Colors.grey),
                SizedBox(height: 12),
                Text('Pas de connexion : connectez-vous pour charger les posts'),
              ],
            ),
          ),

      // Premier chargement, en ligne: spinner standard
      Initial() || Loading(previousData: null) =>
          const Center(child: CircularProgressIndicator()),

      // Données disponibles: afficher le feed quelle que soit la connectivité
      _ => Column(
          children: [
            // En pause: hors ligne mais données visibles
            if (fetchStatus == FetchStatus.paused)
              const _OfflineBanner(),
            // En fetch : revalidation SWR en arrière-plan en cours
            if (fetchStatus == FetchStatus.fetching)
              const LinearProgressIndicator(minHeight: 2),
            if (state is Failure<List<Post>>)
              _ErrorBanner(error: state.error),
            Expanded(child: PostList(posts: state.dataOrNull!)),
          ],
        ),
    };
  },
)

File de mutations hors ligne : écrire maintenant, synchroniser plus tard

Les mutations déclenchées hors ligne sont mises en file et rejouées dans l'ordre FIFO à la reconnexion. Le flag isOptimistic sur MutationSuccess pilote un visuel "synchronisation en attente" pour que l'UI ne soit jamais trompeuse :

lib/features/feed/ui/compose_screen.dart
QoraMutationBuilder<Post, String, void>(
  mutator: (content) => api.createPost(content),
  options: MutationOptions(
    offlineQueue: true,
    optimisticResponse: (content) => Post.optimistic(content: content),
    onSuccess: (_, __, ___) async =>
        context.qora.invalidate(const ['posts']),
  ),
  builder: (context, state, mutate) {
    final isQueued = state is MutationSuccess && state.isOptimistic;

    return Column(
      children: [
        if (isQueued)
          const _SyncPendingBanner(), // "Se synchronisera quand en ligne"
        SubmitButton(
          label: isQueued ? 'En file' : 'Publier',
          icon: isQueued ? Icons.schedule : Icons.send,
          onPressed: state.isPending ? null : () => mutate(_controller.text),
        ),
      ],
    );
  },
)

À la reconnexion, QoraClient rejoue la file dans l'ordre FIFO automatiquement. Le callback onSuccess (qui appelle invalidate) se déclenche uniquement pour les vraies confirmations serveur (isOptimistic: false), jamais pour l'écriture optimiste.

NetworkMode.always pour les requêtes de base de données locale

Toutes les requêtes n'ont pas besoin de connectivité. Une requête sauvegardée par une boîte Hive locale ou SQLite ne doit jamais être mise en pause :

settings_screen.dart
QoraBuilder<Settings>(
  queryKey: const ['settings'],
  fetcher: localDb.getSettings, // lit depuis Hive: pas de réseau nécessaire
  options: const QoraOptions(
    networkMode: NetworkMode.always, // ne se met jamais en pause, n'attend jamais la connectivité
    staleTime: Duration(hours: 1),
  ),
  builder: (context, state, _) { /* ... */ },
)

Ce snippet ne fait pas partie de l'application d'exemple, qui ne parle qu'à une API distante.

Mélangez librement NetworkMode.always (local) et NetworkMode.online (distant) dans le même écran: Qora les gère indépendamment.


Code source

examples/ex_05_network_aware/
├── lib/
│   ├── main.dart                     ← QoraClient + ReconnectStrategy + QoraScope
│   ├── features/
│   │   └── feed/
│   │       ├── data/feed_api.dart    ← couche HTTP
│   │       ├── models/post.dart      ← modèle Post
│   │       └── ui/
│   │           ├── feed_screen.dart  ← gestion FetchStatus.paused
│   │           └── compose_screen.dart ← OfflineMutationQueue + isOptimistic
│   └── shared/
│       ├── connectivity/simulated_connectivity_manager.dart ← bascule en ligne / hors-ligne pour la démo
│       └── widgets/
│           └── offline_banner.dart   ← bannière personnalisée autour de NetworkStatusIndicator
└── pubspec.yaml

Voir le code source complet


Voir aussi

Copyright © 2026