API Mutation
MutationController
Le contrôleur de cycle de vie central pour une seule mutation.
| Paramètre de type | Rôle |
|---|---|
TData | La valeur retournée par le mutator en cas de succès |
TVariables | La valeur passée à mutate() |
TContext | Le snapshot retourné par onMutate (utilisé pour le rollback) |
Constructeur
MutationController({
required MutatorFunction<TData, TVariables> mutator,
MutationOptions<TData, TVariables, TContext>? options,
MutationTracker? tracker,
Map<String, Object?>? metadata,
})
final controller = MutationController<Post, String, List<Post>?>(
mutator: (title) => api.createPost(title),
options: MutationOptions(
onMutate: (title) async => client.getQueryData(['posts']),
onError: (err, _, prev) async => client.restoreQueryData(['posts'], prev),
onSuccess: (_, __, ___) async => client.invalidate(['posts']),
),
tracker: client,
metadata: {'category': 'content', 'resource': 'post'},
);
Propriétés
id
final String id
Identifiant unique pour cette instance de contrôleur. Format : mutation_N (incrémental monotone). Utilisé comme clé dans QoraClient.activeMutations et MutationUpdate.mutatorId.
mutator
final MutatorFunction<TData, TVariables> mutator
La fonction async qui effectue l'écriture. Nommée mutator pour faire écho à fetcher dans fetchQuery.
options
final MutationOptions<TData, TVariables, TContext>? options
Callbacks de cycle de vie et configuration de retry. Voir MutationOptions ci-dessous.
tracker
final MutationTracker? tracker
Tracker optionnel (typiquement QoraClient) qui reçoit les notifications de changement d'état pour l'observabilité DevTools. Quand null, le contrôleur opère en mode standalone.
metadata
final Map<String, Object?>? metadata
Paires clé-valeur arbitraires transmises telles quelles à chaque MutationUpdate émis par ce contrôleur. Utiliser pour l'étiquetage DevTools sans modifier le schéma central.
state
MutationState<TData, TVariables> get state
Le snapshot d'état synchrone actuel.
stream
Stream<MutationState<TData, TVariables>> get stream
Un stream par abonné qui émet immédiatement l'état actuel à l'écoute (capture synchrone), puis transmet toutes les transitions suivantes.
Supporte plusieurs abonnés concurrents. Chaque abonnement obtient son propre stream soutenu par le contrôleur broadcast partagé : aucun événement n'est perdu entre listen() et le premier microtask.
Méthodes
mutate
Future<TData?> mutate(TVariables variables)
Exécute la mutation. Séquence d'états :
- Appelle
onMutate(variables)→ le résultat devientTContext - →
MutationPending - Appelle
mutator(variables)(avec retry optionnel) - →
MutationSuccess+onSuccess+onSettled, ou →MutationFailure+onError+onSettled
Retourne TData en cas de succès, null en cas d'échec. Les erreurs ne sont jamais re-levées à l'appelant : elles sont capturées dans l'état MutationFailure.
Lève StateError si le contrôleur a été disposé.
reset
void reset()
Transition vers MutationIdle. Supprime également ce contrôleur de tracker.activeMutations.
Lève StateError si le contrôleur a été disposé.
dispose
void dispose()
Libère toutes les ressources et ferme le stream interne. Supprime silencieusement le contrôleur du tracker (aucun événement émis). Doit être appelé quand le contrôleur n'est plus nécessaire.
Après dispose, tout appel à mutate ou reset lève StateError.
MutationOptions
Callbacks de cycle de vie et configuration de retry par mutation.
Constructeur
const MutationOptions({
Future<TContext?> Function(TVariables variables)? onMutate,
Future<void> Function(TData data, TVariables variables, TContext? context)? onSuccess,
Future<void> Function(Object error, TVariables variables, TContext? context)? onError,
Future<void> Function(TData? data, Object? error, TVariables variables, TContext? context)? onSettled,
int retryCount = 0,
Duration retryDelay = const Duration(seconds: 1),
List<Object>? invalidates,
})
Propriétés
onMutate
Future<TContext?> Function(TVariables variables)? onMutate
Appelé avant le mutator. Retourne TContext : le snapshot optimiste passé à onError/onSettled pour le rollback. Si ce callback lève, le mutator n'est pas appelé et le contrôleur transite directement vers MutationFailure.
onSuccess
Future<void> Function(TData data, TVariables variables, TContext? context)? onSuccess
Appelé après un mutator réussi, avant onSettled.
onError
Future<void> Function(Object error, TVariables variables, TContext? context)? onError
Appelé après l'échec du mutator (après tous les retries), avant onSettled. Utiliser context (le snapshot de onMutate) pour effectuer un rollback via client.restoreQueryData.
onSettled
Future<void> Function(TData? data, Object? error, TVariables variables, TContext? context)? onSettled
Appelé après chaque résultat : succès ou échec. data est non-null en cas de succès ; error est non-null en cas d'échec.
retryCount
final int retryCount
Nombre de tentatives de retry après l'échec initial. Défaut : 0 (pas de retry). Chaque retry utilise un backoff exponentiel : retryDelay * 2^tentative.
retryDelay
final Duration retryDelay
Délai de base pour le premier retry. Défaut : Duration(seconds: 1). Les retries suivants doublent : 1s, 2s, 4s, ...
invalidates
final List<Object>? invalidates
Liste des clés de requête à invalider automatiquement après une mutation réussie. Chaque clé est passée à QoraClient.invalidate après l'exécution de onSuccess, déclenchant un refetch des requêtes actives dont la clé correspond.
Remplace le pattern manuel courant :
// Avant : invalidation manuelle dans onSuccess
onSuccess: (data, vars, _) async { client.invalidate(['posts']); }
// Après : automatique via les options
invalidates: [['posts']],
Fonctionne également avec la relecture de la file hors-ligne, les requêtes sont invalidées lors de la relecture réussie, pas lors de l'émission optimiste initiale.
Nécessite un QoraClient ou un callback invalidateQuery câblé via MutationController. QoraMutationBuilder et useMutation fournissent cela automatiquement quand un ancêtre QoraScope est présent.
invalidatesTags
final List<QueryTag>? invalidatesTags
Défaut : null
Tags à invalider après une mutation réussie. Chaque requête qui fournit un tag correspondant est automatiquement re-récupérée. Fonctionne parallèlement à invalidates (qui cible directement les clés de requête) et est traité après lui.
La correspondance générique s'applique : invalider QueryTag('post') re-récupère TOUTES les requêtes qui fournissent un tag de type 'post', indépendamment de l'id.
// Re-récupère TOUTES les requêtes fournissant un tag 'post' (n'importe quel id).
MutationOptions(
invalidatesTags: [QueryTag('post')],
)
// Re-récupère seulement les requêtes fournissant le tag spécifique 'post:123'.
MutationOptions(
invalidatesTags: [QueryTag('post', '123')],
)
Nécessite un QoraClient ou un callback invalidateTags câblé via MutationController. QoraMutationBuilder et useMutation fournissent cela automatiquement quand un ancêtre QoraScope est présent.
MutationState
Hiérarchie de classes sealed représentant tous les états possibles d'une mutation.
sealed MutationState
├── MutationIdle
├── MutationPending { variables }
├── MutationSuccess { data, variables }
└── MutationFailure { error, stackTrace?, variables }
Getters communs
Disponibles sur toutes les variantes :
| Getter | Type | Description |
|---|---|---|
isIdle | bool | true dans MutationIdle |
isPending | bool | true dans MutationPending |
isSuccess | bool | true dans MutationSuccess |
isError | bool | true dans MutationFailure |
dataOrNull | TData? | Non-null uniquement dans MutationSuccess |
errorOrNull | Object? | Non-null uniquement dans MutationFailure |
variablesOrNull | TVariables? | null uniquement dans MutationIdle |
when
void when({
void Function()? onIdle,
void Function(TVariables variables)? onPending,
void Function(TData data, TVariables variables)? onSuccess,
void Function(Object error, StackTrace? stackTrace, TVariables variables)? onError,
})
Appelle un callback pour la variante correspondante. Les variantes non gérées sont ignorées silencieusement.
maybeWhen
R maybeWhen<R>({
required R Function() orElse,
R Function()? onIdle,
R Function(TVariables)? onPending,
R Function(TData, TVariables)? onSuccess,
R Function(Object, StackTrace?, TVariables)? onError,
})
Retourne la valeur du callback correspondant, ou orElse() si aucun ne correspond.
fold (extension)
R fold<R>({
required R Function() onIdle,
required R Function(TVariables) onPending,
required R Function(TData, TVariables) onSuccess,
required R Function(Object, StackTrace?, TVariables) onError,
})
Mapper exhaustif : le compilateur impose que chaque variante soit gérée.
final label = state.fold(
onIdle: () => 'Créer',
onPending: (_) => 'Création...',
onSuccess: (post, _) => 'Créé : ${post.title}',
onError: (err, _, __) => 'Erreur : $err',
);
status (extension)
MutationStatus get status
Retourne la valeur d'enum MutationStatus à grain grossier : idle | pending | success | error.
MutationStatus
enum MutationStatus { idle, pending, success, error }
Statut à grain grossier utilisé par MutationUpdate pour éviter les problèmes d'effacement de type générique au niveau du bus d'événements.
MutationUpdate
Snapshot sans type d'une transition d'état de mutation. Émis par QoraClient.mutationEvents à chaque changement d'état de MutationController.
Propriétés
| Propriété | Type | Description |
|---|---|---|
mutatorId | String | Identifiant unique du contrôleur (mutation_N) |
status | MutationStatus | Statut à grain grossier au moment de l'émission |
data | Object? | Données de succès (type effacé). Non-null en cas de succès. |
error | Object? | Erreur. Non-null en cas d'échec. |
variables | Object? | Variables passées à mutate(). null quand idle. |
timestamp | DateTime | Quand cet événement a été émis |
metadata | Map<String, Object?>? | Transmis depuis MutationController.metadata |
Getters
| Getter | Type | Description |
|---|---|---|
isIdle | bool | status == MutationStatus.idle |
isPending | bool | status == MutationStatus.pending |
isSuccess | bool | status == MutationStatus.success |
isError | bool | status == MutationStatus.error |
isFinished | bool | isSuccess || isError : utilisé par QoraClient pour purger automatiquement activeMutations |
MutationTracker
Interface abstraite implémentée par QoraClient. Découple MutationController du client pour éviter les imports circulaires.
abstract interface class MutationTracker {
void trackMutation<TData, TVariables>(
String id,
MutationState<TData, TVariables> state, {
Map<String, Object?>? metadata,
});
void untrackMutation(String id);
}
Implémenter MutationTracker pour construire des DevTools personnalisés, un middleware de journalisation ou des espions de test :
class MutationLogger implements MutationTracker {
@override
void trackMutation<TData, TVariables>(
String id,
MutationState<TData, TVariables> state, {
Map<String, Object?>? metadata,
}) {
print('[$id] → ${state.runtimeType} ${metadata ?? ''}');
}
@override
void untrackMutation(String id) => print('[$id] disposé');
}
QoraClient : Observabilité des mutations
QoraClient implémente MutationTracker et expose deux surfaces pour observer les mutations suivies.
mutationEvents
Stream<MutationUpdate> get mutationEvents
Stream broadcast en temps réel de chaque MutationUpdate de tous les contrôleurs suivis. Émet à chaque transition d'état : y compris MutationIdle (reset) et MutationFailure.
client.mutationEvents.listen((event) {
if (event.isError) {
logger.error(
'[${event.mutatorId}] Mutation échouée',
event.error,
event.metadata,
);
}
});
mutationEvents est un stream broadcast : les événements émis avant votre abonnement sont perdus. Pour attraper les mutations démarrées avant l'abonnement, lire d'abord activeMutations.activeMutations
Map<String, MutationUpdate> get activeMutations
Snapshot non modifiable de toutes les mutations actuellement en cours (pending), indexées par MutationController.id.
- Entrée ajoutée quand un contrôleur transite vers
MutationPending - Entrée purgée automatiquement quand la mutation se termine (
MutationSuccessouMutationFailure) - Entrée supprimée quand le contrôleur revient à
MutationIdleou est disposé
// Pattern de connexion tardive DevTools :
// 1. Lire le snapshot pour voir ce qui est actuellement en vol
final pending = client.activeMutations;
// 2. S'abonner au stream pour les mises à jour en temps réel (y compris les complétions)
client.mutationEvents.listen((event) {
devtools.push(event);
});
activeMutations n'accumule jamais d'entrées "fantômes" pour les mutations terminées : c'est toujours un indicateur fiable de "en cours d'exécution".
debugInfo
QoraClient.debugInfo() inclut l'état des mutations dans sa sortie :
print(client.debugInfo());
// {
// total_queries: 5,
// active_queries: 2,
// inactive_queries: 3,
// pending_requests: 0,
// max_size: 100,
// active_mutations: 1, // ← mutations actuellement en attente
// keys: [...]
// }
MutatorFunction
typedef MutatorFunction<TData, TVariables> =
Future<TData> Function(TVariables variables);
Alias de type pour la fonction de mutation. Fait écho à QueryFunction<T> pour la cohérence avec la convention de nommage fetcher/mutator dans toute l'API.
Voir aussi
- Guide des mutations : guide conceptuel avec tous les patterns
- QoraMutationBuilder : référence du widget Flutter
- QoraClient : référence complète de l'API client
QoraClientConfig
Configuration globale de QoraClient. Appliquée comme référence pour chaque requête. Les QoraOptions par requête sont fusionnées par-dessus.
PersistQoraClient
Sous-classe de QoraClient qui persiste les résultats des requêtes dans un StorageAdapter et les restaure au démarrage via hydrate().