Prise en main

Migrer depuis d'Autres Librairies

Faire correspondre les concepts de flutter_query et cached_query à Qora et migrer écran par écran.

Ce guide fait correspondre les concepts de flutter_query et cached_query à ceux de Qora. Les deux librairies sont solides ; le but n'est pas de les critiquer mais de rendre la migration prévisible.

Les détails d'API des autres librairies ont été vérifiés sur flutter_query 0.11 et cached_query 3.7. Sur une autre version, vérifiez les noms dans leur propre documentation.

Migrer progressivement

Pas besoin de tout réécrire d'un coup. Un QoraScope peut cohabiter avec un autre provider : vous pouvez migrer un écran ou une fonctionnalité à la fois.

void main() {
  runApp(
    QoraScope.auto(
      client: QoraClient(),
      child: const MyApp(), // les providers existants restent à l'intérieur
    ),
  );
}

Depuis flutter_query

flutter_query repose sur des hooks (flutter_hooks) et suit TanStack Query de près. Le chemin le plus rapide est qora_hooks, qui garde le même style.

Correspondance des concepts

Conceptflutter_queryQora
ProviderQueryClientProvider(client: QueryClient())QoraScope(client: QoraClient())
Requête (hooks)useQuery(key, (context) => fn())useQuery(key: key, fetcher: fn)
Requête (widget)non fourniQoraBuilder<T>
MutationuseMutation((vars, context) => fn(vars))useMutation(mutator: fn) ou QoraMutationBuilder
Fenêtre de fraîcheurstaleDurationQoraOptions.staleTime
Rétention du cache inactifgcDurationQoraOptions.gcTime
Placeholder / seedplaceholder, seedplaceholderData, initialData
Invalider (préfixe, défaut)client.invalidateQueries(queryKey: ['todos'])client.invalidatePrefix(['todos'])
Invalider (exact)invalidateQueries(queryKey: k, exact: true)client.invalidate(k)
Lire / écrire le cachegetQueryData, setQueryDatagetQueryData, setQueryData
invalidateQueries de flutter_query matche par préfixe par défaut. invalidate(key) de Qora est exact. Utilisez invalidatePrefix là où vous comptiez sur le comportement par défaut.

Lire l'état

flutter_query expose des booléens sur un objet résultat. Qora retourne un QoraState<T> scellé : le compilateur vérifie que chaque cas est traité, et les données périmées restent disponibles pendant un refetch ou une erreur.

// flutter_query
final result = useQuery(const ['user'], (context) => api.getUser());
if (result.isLoading) return const CircularProgressIndicator();
if (result.isError) return Text('Error: ${result.error}');
return UserCard(result.data!);

// Qora (qora_hooks)
final state = useQuery<User>(key: const ['user'], fetcher: api.getUser);
return switch (state) {
  Success(:final data) => UserCard(data),
  Failure(:final error, :final previousData) =>
    previousData != null ? UserCard(previousData) : Text('Error: $error'),
  Loading(:final previousData) =>
    previousData != null ? UserCard(previousData) : const CircularProgressIndicator(),
  Initial() => const SizedBox.shrink(),
};

Mutations et invalidation

// flutter_query
final mutation = useMutation<Todo, Exception, String, void>(
  (title, context) => api.createTodo(title),
  onSuccess: (data, variables, onMutateResult, context) {
    // invalider ['todos'] via le QueryClient
  },
);

// Qora : on déclare ce qu'il faut rafraîchir, sans callback manuel
final mutation = useMutation<Todo, String>(
  mutator: api.createTodo,
  options: const MutationOptions(invalidates: [['todos']]),
);
mutation.mutate('Buy milk');

Depuis cached_query

cached_query est orienté objet : on crée des instances Query / Mutation (souvent dans un repository) et on écoute leurs streams, avec les widgets Flutter dans cached_query_flutter.

Correspondance des concepts

Conceptcached_queryQora
Config globaleCachedQuery.instance.config(...)QoraClient(config: QoraClientConfig(...))
RequêteQuery(key: 'user', queryFn: fn)client.watchQuery(key: ['user'], fetcher: fn)
Streamquery.stream (QueryStatus)watchQuery / watchState (QoraState)
Lecture ponctuelleawait query.resultawait client.fetchQuery(key: ..., fetcher: ...)
WidgetQueryBuilderQoraBuilder<T>
Forcer un refetchquery.refetch()client.invalidate(key) (les watchers actifs refetch)
Invalider en masseCachedQuery.instance.invalidateCache(...)invalidatePrefix, invalidateWhere, invalidateTags
Fraîcheur / rétentionstaleDuration, cacheDurationstaleTime, gcTime
Liste infinieInfiniteQuery<T, Arg>InfiniteQueryBuilder<TData, TPageParam>
MutationMutation(mutationFn: fn, invalidateQueries: [...])QoraMutationBuilder / useMutation avec MutationOptions.invalidates
Persistancecached_storage (sqflite)PersistQoraClient + votre StorageAdapter
Refetch au retour / à la reconnexionrefetchOnResume, refetchOnConnectionQoraScope.auto()

Clés

Les clés cached_query sont en général des chaînes. Les clés Qora sont des listes, ce qui rend l'invalidation par préfixe naturelle :

// cached_query
Query(key: 'user-$id', queryFn: () => api.getUser(id));

// Qora
client.watchQuery(key: ['users', id], fetcher: () => api.getUser(id));
client.invalidatePrefix(['users']); // toutes les requêtes utilisateur

Ce que vous gagnez

  • File de mutations hors ligne : les écritures faites hors ligne sont mises en file et rejouées dans l'ordre à la reconnexion, avec une UI optimiste entre-temps.
  • État à deux axes : QoraState (données) et FetchStatus (fetching / paused / idle) sont indépendants, donc « hors ligne avec données périmées » est un état de première classe.
  • Invalidation par tags : les mutations invalident des tags logiques plutôt que des clés codées en dur.
  • Overlay DevTools intégré à l'app pour inspecter le cache, les timelines et les mutations.

Gestion des erreurs

Les deux librairies exposent les erreurs brutes. Avec Qora, on les mappe une seule fois via QoraClientConfig.errorMapper, qui reçoit l'erreur et sa stack trace et retourne une QoraException :

import 'package:qora_flutter/qora_flutter.dart';

final client = QoraClient(
  config: QoraClientConfig(
    errorMapper: (Object error, StackTrace? stackTrace) => error is QoraException
        ? error
        : QoraException(error.toString(), originalError: error, stackTrace: stackTrace),
  ),
);

Voir l'intégration Dio pour un mapper complet.

Copyright © 2026