Étude de cas : Conscience réseau
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.pausedhors ligne ; rejouent automatiquement à la reconnexionNetworkMode.offlineFirst: sert les données en cache immédiatement, planifie la revalidation en arrière-plan quand de retour en ligneNetworkMode.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 reconnexionReconnectStrategy:maxConcurrent+jitterpréviennent le thundering-herd quand des dizaines de requêtes reprennent simultanémentNetworkStatusIndicator: widget de bannière hors ligne automatique, zéro configurationFlutterConnectivityManager: câbleconnectivity_plusdansQoraScopeen 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 :
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 :
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 :
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 :
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 :
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 aussi
- Recette de conscience réseau:
NetworkMode,FetchStatus,OfflineMutationQueueetReconnectStrategyen profondeur - Statut réseau: widgets
NetworkStatusIndicatoretNetworkStatusBuilder - Étude de cas Offline-First: persistance entre redémarrages (préoccupation distincte de la conscience réseau)
- API QoraOptions:
networkMode,refetchOnReconnect
Étude de cas : Application Todo offline-first
Une application de todos qui continue de fonctionner sans réseau : données sauvegardées au démarrage, requêtes en pause hors-ligne, écritures mises en file et rejouées dans l'ordre à la reconnexion.
Étude de cas : Intégration des hooks
Remplacer l'imbrication des builders QoraBuilder par des appels de hooks plats et composables : useQuery, useMutation, useInfiniteQuery et indicateurs de fetch globaux dans un HookWidget.