QoraMutationBuilder
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
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 :
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
| Prop | Type | Défaut | Description |
|---|---|---|---|
mutator | Future<TData> Function(TVariables) | requis | Fonction d'écriture asynchrone. Nommée mutator en miroir de fetcher. |
builder | Widget Function(context, MutationState, mutate) | requis | Construit l'arbre de widgets depuis l'état courant. |
options | MutationOptions<TData, TVariables, TContext>? | null | Hooks, retry, file hors-ligne et config de réponse optimiste. |
metadata | Map<String, Object?>? | null | Paires clé-valeur transmises à chaque MutationEvent. |
Le callback builder
Widget Function(
BuildContext context,
MutationState<TData, TVariables> state,
Future<TData?> Function(TVariables variables) mutate,
)
state: leMutationStatescellé courant. Effectuer une correspondance de motifs ou utiliser les helpers.isPending,.isSuccess,.isError.mutate(variables): déclenche la mutation. RetourneTDataen cas de succès ounullen cas d'échec. Les erreurs ne se propagent jamais à l'appelant ; elles apparaissent dans l'étatMutationFailure.
Hooks de cycle de vie
Utiliser options pour effectuer des effets de bord autour de la mutation :
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),
);
},
)
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 :
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 :
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 :
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 :
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'),
);
},
)
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 :
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 :
// 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,optionsoumetadatachange (didUpdateWidget) - Disposé au démontage du widget (
dispose)
Quand le contrôleur est recréé, l'état de mutation est réinitialisé à MutationIdle.
Étapes suivantes
- Guide réseau adaptatif : File hors-ligne,
isOptimisticet relecture à la reconnexion en détail - Guide des mutations : Guide conceptuel complet avec tous les patterns
- Mises à jour optimistes : Exploration approfondie du rollback
- Référence API Mutation : Documentation API complète