Référence de l'API

API Mutation

Référence complète de MutationController, MutationState, MutationOptions, MutationUpdate et l'observabilité des mutations sur QoraClient.

MutationController

Le contrôleur de cycle de vie central pour une seule mutation.

Paramètre de typeRôle
TDataLa valeur retournée par le mutator en cas de succès
TVariablesLa valeur passée à mutate()
TContextLe 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,
})
post_repository.dart
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 :

  1. Appelle onMutate(variables) → le résultat devient TContext
  2. → MutationPending
  3. Appelle mutator(variables) (avec retry optionnel)
  4. → 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 :

GetterTypeDescription
isIdlebooltrue dans MutationIdle
isPendingbooltrue dans MutationPending
isSuccessbooltrue dans MutationSuccess
isErrorbooltrue dans MutationFailure
dataOrNullTData?Non-null uniquement dans MutationSuccess
errorOrNullObject?Non-null uniquement dans MutationFailure
variablesOrNullTVariables?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.

create_post_screen.dart
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éTypeDescription
mutatorIdStringIdentifiant unique du contrôleur (mutation_N)
statusMutationStatusStatut à grain grossier au moment de l'émission
dataObject?Données de succès (type effacé). Non-null en cas de succès.
errorObject?Erreur. Non-null en cas d'échec.
variablesObject?Variables passées à mutate(). null quand idle.
timestampDateTimeQuand cet événement a été émis
metadataMap<String, Object?>?Transmis depuis MutationController.metadata

Getters

GetterTypeDescription
isIdleboolstatus == MutationStatus.idle
isPendingboolstatus == MutationStatus.pending
isSuccessboolstatus == MutationStatus.success
isErrorboolstatus == MutationStatus.error
isFinishedboolisSuccess || 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 :

mutation_logger.dart
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.

app_error_handler.dart
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 (MutationSuccess ou MutationFailure)
  • Entrée supprimée quand le contrôleur revient à MutationIdle ou est disposé
devtools_panel.dart
// 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 :

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

Copyright © 2026