riverpod-ref-in-provider-lifecycle

Par divinevideo · divine-mobile

Corriger le crash Riverpod "Cannot use Ref or modify other providers inside life-cycles/selectors" dans les corps de provider `@riverpod`. À utiliser quand : (1) Crash lors de la destruction d'un provider avec ce message d'erreur exact, (2) Utilisation de `keepAlive()` avec une destruction basée sur un timer, (3) Des callbacks asynchrones (`.then`, `.catchError`) tentent d'appeler `ref.read()` ou `ref.invalidateSelf()`, (4) L'erreur survient après le déclenchement des callbacks `ref.onCancel`/`ref.onResume`/`ref.onDispose`. Solution : encapsuler les opérations sur `ref` dans un try-catch ou éviter complètement l'utilisation de `ref` dans les callbacks asynchrones.

npx skills add https://github.com/divinevideo/divine-mobile --skill riverpod-ref-in-provider-lifecycle

Utilisation de Riverpod ref dans les callbacks de cycle de vie des providers

Problème

Lors de l'utilisation de ref.keepAlive() avec auto-disposal basé sur timer dans les providers @riverpod, les callbacks asynchrones (.then(), .catchError()) qui s'exécutent pendant ou après la suppression planteront quand ils essaient d'utiliser ref.read(), ref.invalidateSelf(), ou toute autre opération ref.

Contexte / Conditions de déclenchement

Message d'erreur :

'package:riverpod/src/core/ref.dart': Failed assertion: line 216 pos 7:
'_debugCallbackStack == 0': Cannot use Ref or modify other providers inside life-cycles/selectors.

Scénario typique :

  1. Le provider utilise ref.keepAlive() avec un timer dans ref.onCancel() :

    final link = ref.keepAlive();
    ref.onCancel(() {
    Timer(Duration(seconds: 15), () {
     link.close();  // Déclenche la suppression
    });
    });
  2. Le provider a des callbacks asynchrones qui utilisent ref :

    someAsyncOperation().then((_) {
    if (ref.mounted) {  // CETTE VÉRIFICATION N'EST PAS SUFFISANTE !
     ref.invalidateSelf();
    }
    });
  3. Le timer s'exécute pendant que le callback asynchrone est en attente

  4. CRASH - ref.mounted retourne true mais l'utilisation de ref est toujours interdite

Point clé : ref.mounted retourne true pendant les callbacks de cycle de vie, mais l'utilisation des opérations ref est toujours interdite. C'est contre-intuitif mais c'est voulu.

Solution

Option 1 : Envelopper les opérations ref dans un try-catch (Recommandé)

someAsyncOperation().then((_) {
  try {
    ref.read(someProvider.notifier).update(/*...*/);
  } catch (e) {
    Log.debug('Provider probablement supprimé : $e');
  }
});

Option 2 : Éviter complètement les opérations ref dans les callbacks asynchrones

Au lieu de :

// MAUVAIS - utilise ref dans le callback asynchrone
openVineVideoCache.removeCorruptedVideo(videoId).then((_) {
  if (ref.mounted) {
    ref.invalidateSelf();  // CRASH !
  }
});

Faire :

// BON - sans attente sans utilisation de ref
unawaited(
  openVineVideoCache.removeCorruptedVideo(videoId).then((_) {
    Log.info('Cache supprimé');  // Pas d'utilisation de ref
  }),
);
// Laisser l'utilisateur réessayer manuellement ou le provider se recréer au prochain accès

Option 3 : Capturer l'état du provider de manière synchrone d'abord

// Lire l'état AVANT l'opération asynchrone
final notifier = ref.read(someProvider.notifier);

// Utiliser la référence capturée dans le callback (pas ref)
someAsyncOperation().then((_) {
  notifier.doSomething();  // Utilise la référence capturée, pas ref
});

Anti-Pattern : Vérification ref.mounted

// CELA NE FONCTIONNE PAS !
someAsyncOperation().then((_) {
  if (ref.mounted) {  // Retourne true pendant le cycle de vie !
    ref.invalidateSelf();  // Plante quand même !
  }
});

La vérification ref.mounted est insuffisante car :

  1. Le timer s'exécute, link.close() est appelé
  2. Riverpod entre dans le cycle de vie de suppression (callback stack > 0)
  3. La vérification ref.mounted passe (retourne toujours true !)
  4. ref.invalidateSelf() est appelé
  5. CRASH - les opérations ref sont interdites pendant les callbacks de cycle de vie

Vérification

  1. Faites défiler rapidement le contenu qui utilise le provider
  2. Naviguez loin et revenez pendant que les providers sont actifs
  3. Laissez les timers keepAlive s'exécuter naturellement (attendez 15+ secondes après le défilement)
  4. Vérifiez Crashlytics/console pour l'erreur d'assertion de cycle de vie
  5. L'erreur ne devrait plus se produire

Exemple

Avant (provoque un crash) :

@riverpod
VideoPlayerController videoController(Ref ref, String videoId) {
  final link = ref.keepAlive();

  ref.onCancel(() {
    Timer(Duration(seconds: 15), () => link.close());
  });

  final controller = VideoPlayerController.networkUrl(url);

  controller.initialize().catchError((error) {
    // CRASH ! Ce callback peut s'exécuter pendant la suppression
    if (ref.mounted) {
      ref.invalidateSelf();  // Erreur d'assertion !
    }
  });

  return controller;
}

Après (sûr) :

@riverpod
VideoPlayerController videoController(Ref ref, String videoId) {
  final link = ref.keepAlive();

  ref.onCancel(() {
    Timer(Duration(seconds: 15), () => link.close());
  });

  final controller = VideoPlayerController.networkUrl(url);

  controller.initialize().catchError((error) {
    // Sûr - enveloppé dans try-catch
    try {
      ref.read(fallbackProvider.notifier).state = newValue;
    } catch (e) {
      Log.debug('Provider supprimé lors du traitement de l\'erreur : $e');
    }
    // Ne pas utiliser invalidateSelf - laisser le provider se recréer au prochain accès
  });

  return controller;
}

Remarques

  • Ce problème est spécifique aux corps de provider @riverpod, pas au dispose() des widgets
  • La skill connexe riverpod-ref-read-in-dispose couvre les problèmes de cycle de vie des widgets
  • Réfléchissez à savoir si ref.invalidateSelf() est vraiment nécessaire - souvent le provider sera recréé naturellement au prochain accès
  • Pour un nettoyage vraiment critique, utilisez ref.onDispose() qui s'exécute de manière synchrone avant la vérification de la pile de callbacks de cycle de vie
  • Ce bug est particulièrement courant avec les lecteurs vidéo, les chargeurs d'images et autres ressources qui utilisent keepAlive() avec suppression basée sur timer

Skills connexes

  • riverpod-ref-read-in-dispose : Pour les problèmes de ref.read() dans dispose() des widgets
  • flutter-dispose-timer-test-failure : Pour les échecs de tests liés aux timers

Références

Skills similaires