riverpod-ref-listen-build-only

Par divinevideo · divine-mobile

Corrige l'erreur d'assertion "ref.listen can only be used within the build method of a ConsumerWidget" dans Flutter Riverpod 3.x. À utiliser quand : (1) un `ConsumerStatefulWidget` plante avec "Failed assertion: line 492 pos 7: 'debugDoingBuild'" dans flutter_riverpod consumer.dart, (2) `ref.listen` est appelé dans `initState`, `addPostFrameCallback`, ou toute méthode de cycle de vie autre que `build()`, (3) tous les tests de widget échouent avec cette assertion depuis un scheduler callback. S'applique à flutter_riverpod 3.0+ avec `ConsumerStatefulWidget`.

npx skills add https://github.com/divinevideo/divine-mobile --skill riverpod-ref-listen-build-only

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 ConsumerStatefulWidget avec flutter_riverpod: ^3.0.0
  • Appel de ref.listen(provider, callback) à l'intérieur de initState() 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 pumpWidget ou pumpAndSettle dans 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 :

  1. ref.listen dans build() est géré automatiquement par Riverpod (aucun disposal manuel nécessaire)
  2. Il se réenregistre à chaque build mais Riverpod gère la déduplication
  3. Utilisez un flag de garde (_actionAttempted) pour éviter que les side effects se déclenchent plusieurs fois
  4. Conservez la vérification immédiate dans le post-frame callback de initState pour quand les données sont déjà disponibles
  5. ref.read fonctionne bien dans initState/callbacks ; seuls ref.listen et ref.watch sont 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.watch est aussi restreint à build() uniquement
  • ref.read peut ê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(), utilisez ref.listenManual() qui retourne une ProviderSubscription qui doit être manuellement disposée dans dispose()
  • 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

Références

Skills similaires