Requêtes
Anatomie d'une requête
Toute requête Qora est composée de trois éléments :
await client.fetchQuery<User>(
// 1. Clé de requête : identifiant unique pour cette donnée
key: ['users', userId],
// 2. Fetcher : fonction asynchrone qui retourne la donnée
fetcher: () => api.getUser(userId),
// 3. Options (optionnel) : configuration par requête
options: QoraOptions(
staleTime: Duration(minutes: 5),
retryCount: 3,
),
);
1. Clé de requête
La clé de requête identifie la donnée de façon unique dans le cache. Elle pilote :
- Les recherches dans le cache (hit / miss)
- La revalidation en arrière-plan
- L'invalidation ciblée après les mutations
// Segment unique
['users']
// Entité + ID
['users', userId]
// Entité + filtres (map imbriquée)
['posts', {'status': 'published', 'page': page}]
// Wrapper typé (sémantique identique)
QoraKey.withId('users', userId)
QoraKey.withFilter('posts', {'status': 'published'})
['users', 1] et ['users', 1] sont identiques même s'ils sont créés indépendamment.Voir Clés de requête pour le guide complet.
2. Fetcher
Le fetcher est toute Future<T> Function() qui retourne la donnée :
// Inline : cas simple
fetcher: () => api.getUser(userId),
// Closure asynchrone avec dépendances
fetcher: () async {
final token = await authService.getToken();
return api.getUser(userId, token: token);
},
null ou ignorer les erreurs empêche Qora de passer à l'état Failure et de déclencher les retries.3. Options
QoraOptions configure le cache, les retries et le polling par requête :
options: QoraOptions(
// Fenêtre de fraîcheur (0 = toujours périmé)
staleTime: Duration(minutes: 5),
// Durée de rétention des données inutilisées en cache
gcTime: Duration(minutes: 10),
// Retry automatique en cas d'échec
retryCount: 3,
retryDelay: Duration(seconds: 1), // délai de base pour le backoff exponentiel
// Désactiver la requête conditionnellement
enabled: userId != null,
// Intervalle de polling (watchQuery uniquement)
refetchInterval: Duration(seconds: 30),
)
Voir QoraOptions pour la liste complète des champs.
Deux modes de récupération
fetchQuery : récupération ponctuelle
Retourne un Future<T>. Adapté aux appels impératifs (appui sur un bouton, chargement de page) :
final user = await client.fetchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
);
Comportement du cache :
| État | Action |
|---|---|
| Frais (non périmé) | Retourne les données en cache, pas d'appel réseau |
| Périmé | Retourne les données périmées immédiatement, refetch en arrière-plan |
| Absent | Fetch, attend et retourne le résultat |
watchQuery : stream réactif
Retourne un Stream<QoraState<T>>. Adapté aux liaisons UI :
StreamBuilder<QoraState<User>>(
stream: client.watchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
),
builder: (context, snapshot) {
return switch (snapshot.data) {
null || Initial() => const SizedBox.shrink(),
Loading(:final previousData) => previousData != null
? UserCard(previousData)
: const CircularProgressIndicator(),
Success(:final data) => UserCard(data),
Failure(:final error) => Text('Erreur : $error'),
};
},
)
Le stream :
- Émet immédiatement l'état courant du cache lors de l'abonnement
- Déclenche un fetch automatique si les données sont absentes ou périmées (contrôlé par
refetchOnMount) - Propage les mises à jour depuis toute source : fetch,
setQueryData,invalidate - Effectue un polling quand
refetchIntervalest défini et le stream est actif - Planifie le GC quand le dernier abonné se désabonne
prefetch : préchargement en arrière-plan
Alimente le cache avant que l'utilisateur ne navigue vers un écran :
// Au survol / tap probable
onEnter: (_) => client.prefetch<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
),
Sans effet si les données sont déjà fraîches. Ne crée pas de stream.
Cycle de vie d'une requête
[Initial]
│ premier watchQuery / fetchQuery
▼
[Loading(previousData: null)]
│ fetcher réussit
▼
[Success(data, updatedAt)]
│ staleTime écoulé → refetch en arrière-plan
▼
[Loading(previousData: data)] ← données périmées visibles pendant le rechargement
│ fetcher réussit
▼
[Success(newData, updatedAt)]
│ staleTime écoulé → fetcher échoue après tous les retries
▼
[Failure(error, previousData: data)] ← données précédentes toujours accessibles
Voir États des requêtes pour la documentation complète.
Invalidation
Marquer les requêtes comme périmées après une mutation. Les streams watchQuery actifs passent immédiatement à Loading :
// Après la création d'un post
await api.createPost(payload);
// Invalider une clé exacte
client.invalidate(['posts', postId]);
// Invalider toutes les requêtes posts
client.invalidateWhere((key) => key.firstOrNull == 'posts');
Mises à jour manuelles du cache
Injecter des données directement dans le cache. Tous les streams actifs reçoivent la mise à jour instantanément :
// Snapshot avant mise à jour (pour rollback)
final snapshot = client.getQueryData<User>(['users', userId]);
// Mise à jour optimiste : l'UI reflète le changement immédiatement
client.setQueryData(['users', userId], user.copyWith(name: 'Alice'));
try {
await api.updateUser(userId, payload);
} catch (_) {
// Rollback en cas d'échec
client.restoreQueryData(['users', userId], snapshot);
}
Exemple complet
import 'package:qora/qora.dart';
class UserRepository {
final QoraClient _client;
final UserApi _api;
UserRepository(this._client, this._api);
Future<User> getUser(int userId) => _client.fetchQuery<User>(
key: ['users', userId],
fetcher: () => _api.getUser(userId),
options: const QoraOptions(staleTime: Duration(minutes: 5)),
);
Stream<QoraState<User>> watchUser(int userId) => _client.watchQuery<User>(
key: ['users', userId],
fetcher: () => _api.getUser(userId),
options: const QoraOptions(
staleTime: Duration(minutes: 5),
refetchInterval: Duration(minutes: 1),
),
);
void invalidateUser(int userId) => _client.invalidate(['users', userId]);
void patchLocally(User updated) =>
_client.setQueryData(['users', updated.id], updated);
}
Bonnes pratiques
À faire
// Structure de clé cohérente : entité en premier, puis ID/filtres
['users', userId]
['posts', {'status': 'published', 'page': page}]
// Toujours lever une exception : ne jamais retourner null
fetcher: () => api.getUser(userId),
// Traiter les quatre états de façon exhaustive
switch (state) {
case Initial(): ...
case Loading(): ...
case Success(): ...
case Failure(): ...
}
À éviter
// Ordre de clé incohérent
['users', userId]
[userId, 'users'] // Entrée de cache différente
// Ignorer les erreurs
fetcher: () async {
try { return await api.getUser(userId); }
catch (_) { return null; } // Qora ne voit jamais l'échec
},
// Objets sans surcharge == dans les clés
['users', UserFilter(status: 'active')] // Toujours un cache miss
Étapes suivantes
- Clés de requête : Structure, égalité et patterns d'invalidation
- États des requêtes : Exploration approfondie de la machine d'état scellée
- Cache : LRU, stale time, GC et gestion du cache
- Stale-While-Revalidate : Fonctionnement de la revalidation en arrière-plan
Assistants IA
Donner à Claude, Cursor, Copilot ou tout assistant de code le contexte nécessaire pour écrire du code Qora correct.
Clés de requête
Les clés de requête sont le fondement du système de cache de Qora. Elles identifient les requêtes de façon unique et permettent la gestion automatique du cache, l'invalidation et le refetch.