Concepts clés

Requêtes

Les requêtes sont le fondement de Qora. Une requête est une demande asynchrone liée à une clé, avec cache automatique, déduplication, retries et state réactif.

Anatomie d'une requête

Toute requête Qora est composée de trois éléments :

user_repository.dart
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
keys.dart
// 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'})
Les clés utilisent l'égalité structurelle profonde. ['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 :

user_repository.dart
// 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);
},
Le fetcher doit lever une exception en cas d'échec. Retourner 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 :

user_repository.dart
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) :

user_repository.dart
final user = await client.fetchQuery<User>(
  key: ['users', userId],
  fetcher: () => api.getUser(userId),
);

Comportement du cache :

ÉtatAction
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
AbsentFetch, attend et retourne le résultat

watchQuery : stream réactif

Retourne un Stream<QoraState<T>>. Adapté aux liaisons UI :

user_page.dart
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 refetchInterval est 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 :

user_list_page.dart
// 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 :

post_repository.dart
// 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 :

user_repository.dart
// 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

repositories/user_repository.dart
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

main.dart
// 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

main.dart
// 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

Copyright © 2026