Usage avec Flutter

QoraMutationBuilder

Le widget qui gère le cycle de vie d'un MutationController et reconstruit l'UI à chaque transition d'état de mutation.

QoraMutationBuilder<TData, TVariables, TContext> crée un MutationController au montage, le dispose au démontage et reconstruit à chaque changement d'état de mutation. Le callback builder reçoit le MutationState courant et un callback mutate(variables) pour déclencher l'écriture depuis l'UI.

QoraMutationBuilder ne nécessite pas d'ancêtre QoraScope. Il se câble automatiquement au client quand un scope est présent, et se replie gracieusement en mode autonome dans le cas contraire.

Utilisation de base

create_post_screen.dart
QoraMutationBuilder<Post, String, void>(
  mutator: (title) => api.createPost(title),
  builder: (context, state, mutate) {
    return ElevatedButton(
      onPressed: state.isPending ? null : () => mutate('Nouveau post'),
      child: state.isPending
          ? const CircularProgressIndicator()
          : const Text('Créer'),
    );
  },
)

Le bouton est désactivé pendant l'exécution de la mutation. Passer null à onPressed : Flutter désactive le bouton automatiquement.


UI pilotée par l'état

Effectuer une correspondance de motifs directement sur le MutationState scellé pour une couverture UI exhaustive :

create_post_screen.dart
QoraMutationBuilder<Post, String, void>(
  mutator: (title) => api.createPost(title),
  builder: (context, state, mutate) {
    return switch (state) {
      MutationIdle() => SubmitButton(onTap: () => mutate('Post')),
      MutationPending() => const CircularProgressIndicator(),
      MutationSuccess(:final data) => SuccessBanner('Créé : ${data.title}'),
      MutationFailure(:final error) => Column(children: [
          ErrorBanner(error),
          TextButton(
            onPressed: () => mutate('Post'), // Réessayer
            child: const Text('Réessayer'),
          ),
        ]),
    };
  },
)

Props

PropTypeDéfautDescription
mutatorFuture<TData> Function(TVariables)requisFonction d'écriture asynchrone. Nommée mutator en miroir de fetcher.
builderWidget Function(context, MutationState, mutate)requisConstruit l'arbre de widgets depuis l'état courant.
optionsMutationOptions<TData, TVariables, TContext>?nullHooks, retry, file hors-ligne et config de réponse optimiste.
metadataMap<String, Object?>?nullPaires clé-valeur transmises à chaque MutationEvent.

Le callback builder

Widget Function(
  BuildContext context,
  MutationState<TData, TVariables> state,
  Future<TData?> Function(TVariables variables) mutate,
)
  • state : le MutationState scellé courant. Effectuer une correspondance de motifs ou utiliser les helpers .isPending, .isSuccess, .isError.
  • mutate(variables) : déclenche la mutation. Retourne TData en cas de succès ou null en cas d'échec. Les erreurs ne se propagent jamais à l'appelant ; elles apparaissent dans l'état MutationFailure.

Hooks de cycle de vie

Utiliser options pour effectuer des effets de bord autour de la mutation :

settings_screen.dart
QoraMutationBuilder<void, UserUpdate, void>(
  mutator: (update) => api.updateUser(update),
  options: MutationOptions(
    invalidates: [['users', currentUserId]], // Rafraîchissement automatique
    onSuccess: (_, update, __) async {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Paramètres sauvegardés')),
      );
    },
    onError: (error, _, __) async {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Erreur : $error')),
      );
    },
  ),
  builder: (context, state, mutate) {
    return SaveButton(
      isLoading: state.isPending,
      onTap: () => mutate(formData),
    );
  },
)
Capturer context dans onSuccess/onError est sûr tant que le widget est monté. Préférer ScaffoldMessenger.of(context) : il est résilient aux changements de l'arbre de widgets. Éviter Navigator.of(context) dans les callbacks asynchrones sans vérification de montage.

File de mutations hors-ligne

Quand l'appareil est hors-ligne, les mutations avec offlineQueue: true sont enfilées en ordre FIFO et rejouées automatiquement quand la connectivité revient. Fournir optimisticResponse pour un retour instantané même sans réponse serveur :

lib/ui/todo_screen.dart
QoraMutationBuilder<void, Todo, void>(
  mutator: (todo) => api.createTodo(todo),
  options: MutationOptions(
    invalidates: [['todos']], // Rafraîchissement automatique en cas de succès ou relecture hors-ligne
    // Opter pour la file hors-ligne
    offlineQueue: true,

    // Fournir un TData optimiste pour que l'UI se mette à jour immédiatement
    optimisticResponse: (todo) => null,
  ),
  builder: (context, state, mutate) {
    return AddTodoButton(
      onTap: () => mutate(newTodo),
    );
  },
)

isOptimistic : styliser les éléments non confirmés

Quand une mutation est en file hors-ligne et que optimisticResponse est fourni, l'état passe à MutationSuccess(isOptimistic: true). Utiliser le flag isOptimistic pour appliquer un visuel d'attente (grisé, icône d'horloge, barré) afin que les utilisateurs sachent que l'élément n'est pas encore confirmé par le serveur :

lib/ui/create_post_screen.dart
QoraMutationBuilder<Post, String, void>(
  mutator: api.createPost,
  options: MutationOptions(
    offlineQueue: true,
    optimisticResponse: (title) => Post.optimistic(title),
  ),
  builder: (context, state, mutate) {
    // Extraire isOptimistic depuis l'état de succès
    final isOptimistic = switch (state) {
      MutationSuccess(:final isOptimistic) => isOptimistic,
      _ => false,
    };

    return Column(
      children: [
        if (isOptimistic)
          const Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Icon(Icons.schedule, size: 14, color: Colors.grey),
              SizedBox(width: 4),
              Text('En file : synchronisation quand en ligne',
                  style: TextStyle(color: Colors.grey)),
            ],
          ),
        ElevatedButton(
          onPressed: state.isPending ? null : () => mutate('Nouveau post'),
          child: const Text('Créer'),
        ),
      ],
    );
  },
)

isOptimistic est également disponible directement sur MutationState :

// Sur la classe scellée
state.isOptimistic  // false pour tous les états sauf MutationSuccess avec isOptimistic: true

// Dans when()
state.when(
  onSuccess: (data, variables, isOptimistic) {
    if (isOptimistic) showPendingIndicator();
    else showConfirmedBadge();
  },
  ...
)
isOptimistic: true est émis uniquement quand offlineQueue: trueetoptimisticResponse est fourni et l'appareil est hors-ligne. Un succès serveur réel produit toujours isOptimistic: false.

Mises à jour optimistes

Combiner onMutate (écriture optimiste) et onError (rollback) pour un retour instantané pendant que le serveur confirme le changement :

todo_list_screen.dart
QoraMutationBuilder<void, Todo, List<Todo>?>(
  // TContext = List<Todo>? : le snapshot pour le rollback
  mutator: (todo) => api.addTodo(todo),
  options: MutationOptions(
    invalidates: [['todos']], // 4. Récupérer les données confirmées depuis le serveur
    onMutate: (todo) async {
      // 1. Snapshot de la liste courante
      final prev = context.qora.getQueryData<List<Todo>>(['todos']);

      // 2. Appliquer de façon optimiste : l'UI se met à jour avant l'appel réseau
      context.qora.setQueryData<List<Todo>>(
        ['todos'],
        [...?prev, todo.copyWith(isOptimistic: true)],
      );

      return prev; // Retourné comme TContext
    },
    onError: (_, __, prev) async {
      // 3. Rollback en cas d'échec
      context.qora.restoreQueryData(['todos'], prev);
    },
  ),
  builder: (context, state, mutate) {
    return AddTodoButton(
      isLoading: state.isPending,
      onTap: () => mutate(newTodo),
    );
  },
)

context.qora est le raccourci pour QoraScope.of(context) : disponible quand le widget est dans un QoraScope.


Reset

Après affichage d'un résultat de succès ou d'échec, appeler reset() pour revenir à MutationIdle et permettre à l'utilisateur de déclencher une autre mutation.

QoraMutationBuilder possédant le contrôleur en interne, le reset peut être piloté via l'état du widget externe :

create_post_screen.dart
QoraMutationBuilder<Post, String, void>(
  mutator: (title) => api.createPost(title),
  builder: (context, state, mutate) {
    if (state is MutationSuccess<Post, String>) {
      return Column(children: [
        Text('Créé : ${state.data.title}'),
        TextButton(
          // Reconstruit avec un état idle frais au prochain frame
          onPressed: () => setState(() {}),
          child: const Text('Créer un autre'),
        ),
      ]);
    }
    return SubmitButton(
      onTap: state.isPending ? null : () => mutate('Mon post'),
    );
  },
)
Pour un accès programmatique à reset() ou mutate() en dehors du builder, utiliser directement MutationController et s'abonner à son stream dans un StatefulWidget.

Étiquetage DevTools avec metadata

Attacher un contexte métier à chaque MutationEvent pour un filtrage plus facile dans les DevTools :

auth_screen.dart
QoraMutationBuilder<User, LoginCredentials, void>(
  mutator: authApi.login,
  metadata: {'category': 'auth', 'screen': 'login'},
  builder: (context, state, mutate) { ... },
)

Tous les événements émis par ce contrôleur incluront metadata: {'category': 'auth', 'screen': 'login'}, visible dans client.mutationEvents et client.activeMutations.


Mode autonome

QoraMutationBuilder peut être utilisé n'importe où dans l'arbre de widgets, même sans ancêtre QoraScope. Dans ce cas, tracker est null et le contrôleur fonctionne en mode autonome sans câblage DevTools global :

onboarding_screen.dart
// Pas de QoraScope requis
QoraMutationBuilder<void, OnboardingData, void>(
  mutator: onboardingApi.submit,
  builder: (context, state, mutate) { ... },
)

Cycle de vie du contrôleur

Le MutationController interne est :

  • Créé au montage du widget (initState)
  • Recréé quand l'identité de mutator, options ou metadata change (didUpdateWidget)
  • Disposé au démontage du widget (dispose)

Quand le contrôleur est recréé, l'état de mutation est réinitialisé à MutationIdle.


Étapes suivantes

Copyright © 2026