Aide

FAQ

Réponses aux questions fréquentes sur l'API Qora, la sémantique du cache et l'intégration Flutter.

API principale

Quelle est la différence entre fetchQuery et watchQuery ?

fetchQuery<T> exécute un fetch unique et retourne un Future<T>. Il ne s'abonne pas aux changements d'état.

watchQuery<T> retourne un Stream<QoraState<T>> qui émet à chaque transition d'état. Il déclenche un fetch au premier abonnement et gère automatiquement stale-while-revalidate, la déduplication et les événements de cycle de vie. Utiliser watchQuery dès qu'un widget doit refléter l'état live du cache.


Quelle est la différence entre getQueryData et getQueryState ?

getQueryData<T>(key) retourne T? : la valeur de données brute de l'entrée de cache courante, ou null si aucune donnée n'est disponible.

getQueryState<T>(key) retourne QoraState<T> : l'état sealed complet incluant le variant (Initial, Loading, Success, Failure) et tous les champs associés tels que updatedAt et previousData.

Utiliser getQueryData quand seule la valeur de données est nécessaire (par exemple, pour un snapshot avant une écriture optimiste). Utiliser getQueryState quand l'état complet est requis pour l'affichage ou la logique de branchement.


Quelle est la différence entre restoreQueryData et setQueryData ?

setQueryData<T>(key, T data) requiert un T non-null. Il produit toujours un état Success avec updatedAt: DateTime.now().

restoreQueryData<T>(key, T? data) accepte T?. Quand data est null, il appelle removeQuery(key), ce qui restaure l'état Initial. Quand data est non-null, il appelle setQueryData(key, data). restoreQueryData est le choix correct pour le rollback : le snapshot retourné par getQueryData peut être null s'il n'y avait pas de données précédentes, et passer ce null à restoreQueryData supprime proprement l'entrée optimiste.


Quelle est la différence entre invalidate, invalidateWhere et invalidateQueries ?

invalidate(key) marque une clé spécifique comme périmée.

invalidateWhere(bool Function(List<dynamic> key) predicate) marque comme périmée chaque clé en cache satisfaisant le prédicat. Permet d'invalider une famille de clés liées en un seul appel (par exemple, toutes les clés commençant par 'users').

invalidateQueries({required QueryFilter filter}) est l'équivalent typé de invalidateWhere, acceptant un objet QueryFilter plutôt qu'un prédicat brut.

Les trois déclenchent un refetch en arrière-plan pour chaque abonné actuellement actif sur les clés affectées.


Quelle est la différence entre removeQuery et invalidate ?

invalidate(key) marque l'entrée comme périmée et déclenche un refetch en arrière-plan pour les abonnés actifs. L'entrée reste dans le cache.

removeQuery(key) supprime entièrement l'entrée du cache. Les abonnés actifs transitent vers Initial. Sur PersistQoraClient, il supprime également l'entrée persistée depuis StorageAdapter.


Qu'est-ce que QoraStatus et quand l'utiliser à la place de la classe sealed ?

QoraStatus est un enum simple (initial | loading | success | error) accessible via state.status. Il est utile quand la logique de branchement n'a pas besoin d'accéder aux champs associés de l'état (par exemple, basculer un spinner via state.status.isLoading). Quand data, error, previousData ou updatedAt sont nécessaires, pattern-matcher directement la classe sealed.


Sémantique du cache

Quelle est la différence entre staleTime et gcTime ?

staleTime contrôle la fraîcheur. Les données dans la fenêtre de fraîcheur sont servies depuis le cache sans appel réseau. Après expiration de staleTime, les données sont toujours servies depuis le cache, mais une revalidation en arrière-plan est déclenchée simultanément (stale-while-revalidate).

gcTime contrôle l'éviction. Quand le dernier abonné watchQuery se désabonne, un timer GC est démarré. Si aucun nouvel abonné ne s'attache avant l'expiration de gcTime, l'entrée est supprimée du cache. Le prochain accès repart de Initial.

Configuration courante : staleTime < gcTime. Les données deviennent périmées et déclenchent une revalidation en arrière-plan, mais l'entrée survit suffisamment longtemps pour que la navigation retour vers un écran soit instantanée.


Quand QoraBuilder refetch-il au montage ?

Par défaut (refetchOnMount: true au niveau QoraClientConfig), watchQuery déclenche un fetch au premier abonnement quel que soit l'état du cache existant.

Quand refetchOnMount: false (défini par requête via QoraOptions ou globalement via QoraClientConfig), watchQuery ne fetche que si l'entrée est dans l'état Initial. Si un état en cache existe, y compris un Success périmé, aucun fetch n'est déclenché au montage.


Que se passe-t-il quand maxCacheSize est atteint ?

Quand le cache atteint maxCacheSize, Qora évince l'entrée inactive la moins récemment utilisée (une entrée sans abonné watchQuery actif). Les entrées actives ne sont jamais évincées par la pression LRU. Le callback onCacheEvict dans QoraClientConfig est invoqué pour chaque entrée évincée.


Intégration Flutter

Quelle est la différence entre QoraBuilder et QoraStateBuilder ?

QoraBuilder<T> appelle watchQuery au montage, ce qui déclenche un fetch et s'abonne aux changements d'état. Il prend un builder à 3 paramètres : Widget Function(BuildContext, QoraState<T>, FetchStatus).

QoraStateBuilder<T> appelle watchState à la place. Il observe l'état du cache existant sans jamais déclencher de fetch. Il prend un builder à 2 paramètres : Widget Function(BuildContext, QoraState<T>). A utiliser quand un autre widget est propriétaire du cycle de vie du fetch et que ce widget doit uniquement afficher le résultat.


Comment obtenir un indicateur de chargement global ?

Avec qora_hooks, utiliser useIsFetching(). Retourne true tant qu'une requête a une demande en vol et ne rebuild que lors du franchissement de la frontière zéro :

lib/shared/widgets/app_progress_bar.dart
class AppProgressBar extends HookWidget {
  const AppProgressBar({super.key});

  @override
  Widget build(BuildContext context) {
    final isFetching = useIsFetching();
    return AnimatedOpacity(
      opacity: isFetching ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 250),
      child: const LinearProgressIndicator(minHeight: 2),
    );
  }
}

Sans hooks, s'abonner directement à client.fetchingCountStream.


FlutterLifecycleManager et FlutterConnectivityManager sont-ils obligatoires ?

Les deux sont optionnels. Sans FlutterLifecycleManager, les requêtes ne refetchent pas quand l'app revient au premier plan. Sans FlutterConnectivityManager, les requêtes ne se mettent pas en pause hors ligne et ne rejouent pas à la reconnexion : tous les fetches s'exécutent toujours quel que soit l'état de la connectivité.

Pour les apps avec support hors ligne, fournir les deux. Pour les apps dans des environnements contrôlés (tableaux de bord, outils internes), les omettre est acceptable.

FlutterConnectivityManager ne prend aucun paramètre de constructeur. FlutterLifecycleManager requiert qoraClient: client.


Peut-on utiliser Qora sans Flutter ?

Oui. Le package qora est du Dart pur sans dépendance Flutter. QoraClient, QoraState, QoraOptions, MutationController et PersistQoraClient sont tous disponibles dans les environnements non-Flutter (CLI Dart, côté serveur, tests).

Le package qora_flutter ajoute QoraScope, QoraBuilder, QoraMutationBuilder, InfiniteQueryBuilder, FlutterLifecycleManager, FlutterConnectivityManager et NetworkStatusIndicator.


Persistance

Faut-il appeler StorageAdapter.init() avant hydrate() ?

Oui. Appeler await storage.init() avant await client.hydrate(). Appeler hydrate() avant init() produit un comportement indéfini pour les adaptateurs qui ouvrent des handles de fichiers ou des connexions de base de données pendant l'initialisation.

Séquence de démarrage correcte :

main.dart
await storage.init();
client.registerSerializer<User>(userSerializer, name: 'User');
await client.hydrate();
runApp(...);

Pourquoi le name du sérialiseur est-il important ?

La chaîne name est stockée avec chaque entrée persistée comme discriminant de type. Lors de hydrate(), Qora l'utilise pour trouver le convertisseur fromJson correct. Si name est omis, il est défini par défaut à T.toString(), qui est altéré par l'obfuscation Dart (--obfuscate) et la minification dans les builds Flutter Web.

Toujours passer un name explicite pour les types persistés dans les builds release. Maintenir le name stable entre les versions de l'app : le modifier invalide toutes les entrées précédemment persistées pour ce type.


Mutations

Faut-il utiliser QoraMutationBuilder ou useMutation ?

QoraMutationBuilder fonctionne dans tout StatelessWidget ou StatefulWidget. useMutation requiert HookWidget. Les deux créent et disposent un MutationController lié au cycle de vie du widget.

Sur les écrans combinant une requête et une mutation, useMutation aux côtés de useQuery évite les callbacks de builder imbriqués et réduit la profondeur d'indentation. Sur les écrans avec uniquement une mutation ou dans les bases de code n'utilisant pas flutter_hooks, QoraMutationBuilder est le choix correct.


Quand onSuccess s'exécute-t-il pour les mutations mises en file hors ligne ?

onSuccess s'exécute uniquement quand la mutation se complète avec une vraie réponse serveur (isOptimistic: false). Il ne s'exécute pas quand une réponse optimiste est appliquée localement en mode hors ligne. Cela garantit que l'invalidation du cache via invalidate ou setQueryData n'a lieu qu'après confirmation de l'écriture par le serveur.


DevTools

Quelle surface DevTools utiliser ?

L'overlay in-app (qora_devtools_overlay) est stable et disponible. Il s'affiche à l'intérieur de l'app en cours d'exécution, et n'affiche rien en build release.

Une extension Flutter DevTools est en développement et n'est pas publiée. Utilisez l'overlay.


Pourquoi l'overlay est-il vide ?

Vérifiez que l'application tourne en mode debug (flutter run, pas --release ni --profile) : sinon l'overlay ne construit rien. Vérifiez ensuite que la même instance d'OverlayTracker est passée à QoraClient(tracker: ...) et à QoraInspector(tracker: ...). Un client créé sans le tracker n'émet aucun événement.

Copyright © 2026