ref.listen de Riverpod doit être dans build() pour ConsumerStatefulWidget
Problème
Dans Riverpod 3.x, appeler ref.listen à l'intérieur de initState(), addPostFrameCallback, ou
toute autre méthode que build() provoque une erreur d'assertion :
ref.listen can only be used within the build method of a ConsumerWidget
'package:flutter_riverpod/src/core/consumer.dart':
Failed assertion: line 492 pos 7: 'debugDoingBuild'
Cela se manifeste généralement lorsqu'on essaie de surveiller réactivement une valeur de provider qui peut changer après le premier rendu du widget (par exemple, en attente de la fin d'une opération asynchrone).
Contexte / Conditions de déclenchement
- Utilisation de
ConsumerStatefulWidgetavecflutter_riverpod: ^3.0.0 - Appel de
ref.listen(provider, callback)à l'intérieur deinitState()ou d'un post-frame callback - Tous les widget tests échouent avec l'erreur d'assertion de
ConsumerStatefulElement.listen - L'erreur se produit lors de
pumpWidgetoupumpAndSettledans les tests - La stack trace montre
SchedulerBinding._invokeFrameCallback->ConsumerStatefulElement.listen
Solution
Déplacez ref.listen dans la méthode build(). Utilisez un flag de garde pour vous assurer que les side effects
ne se déclenchent qu'une seule fois.
Avant (cassé) :
class _MyScreenState extends ConsumerState<MyScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// CELA VA CRASH dans Riverpod 3.x
ref.listen(
myProvider.select((s) => s.someValue),
(previous, next) {
if (previous == null && next != null) {
_doSomething(next);
}
},
);
});
}
}
Après (corrigé) :
class _MyScreenState extends ConsumerState<MyScreen> {
bool _actionAttempted = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
// Vérification immédiate des données déjà disponibles
final value = ref.read(myProvider).someValue;
if (value != null) _tryDoSomething(value);
});
}
void _tryDoSomething(SomeType? value) {
if (value == null || _actionAttempted) return;
_actionAttempted = true;
_doSomething(value);
}
@override
Widget build(BuildContext context) {
// ref.listen est sûr ici - Riverpod gère automatiquement la subscription
ref.listen(
myProvider.select((s) => s.someValue),
(previous, next) {
if (previous == null && next != null) {
_tryDoSomething(next);
}
},
);
return // ... widget tree
}
}
Points clés :
ref.listendansbuild()est géré automatiquement par Riverpod (aucun disposal manuel nécessaire)- Il se réenregistre à chaque build mais Riverpod gère la déduplication
- Utilisez un flag de garde (
_actionAttempted) pour éviter que les side effects se déclenchent plusieurs fois - Conservez la vérification immédiate dans le post-frame callback de
initStatepour quand les données sont déjà disponibles ref.readfonctionne bien dansinitState/callbacks ; seulsref.listenetref.watchsont restreints
Vérification
- Tous les widget tests passent sans erreurs d'assertion
- Le listener se déclenche correctement quand la valeur surveillée change
- Les side effects ne se déclenchent qu'une seule fois (vérifiez avec un compteur ou un log)
Notes
ref.watchest aussi restreint àbuild()uniquementref.readpeut être utilisé n'importe où (initState, callbacks, dispose, etc.)- Dans les anciennes versions de Riverpod (< 3.0),
ref.listenétait moins restreint - Si vous avez besoin d'un listener en dehors de
build(), utilisezref.listenManual()qui retourne uneProviderSubscriptionqui doit être manuellement disposée dansdispose() - Cette contrainte existe parce que Riverpod a besoin du contexte de l'element du widget pour gérer correctement le cycle de vie de la subscription