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 :
-
Le provider utilise
ref.keepAlive()avec un timer dansref.onCancel():final link = ref.keepAlive(); ref.onCancel(() { Timer(Duration(seconds: 15), () { link.close(); // Déclenche la suppression }); }); -
Le provider a des callbacks asynchrones qui utilisent
ref:someAsyncOperation().then((_) { if (ref.mounted) { // CETTE VÉRIFICATION N'EST PAS SUFFISANTE ! ref.invalidateSelf(); } }); -
Le timer s'exécute pendant que le callback asynchrone est en attente
-
CRASH -
ref.mountedretournetruemais l'utilisation derefest 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 :
- Le timer s'exécute,
link.close()est appelé - Riverpod entre dans le cycle de vie de suppression (callback stack > 0)
- La vérification
ref.mountedpasse (retourne toujourstrue!) ref.invalidateSelf()est appelé- CRASH - les opérations ref sont interdites pendant les callbacks de cycle de vie
Vérification
- Faites défiler rapidement le contenu qui utilise le provider
- Naviguez loin et revenez pendant que les providers sont actifs
- Laissez les timers keepAlive s'exécuter naturellement (attendez 15+ secondes après le défilement)
- Vérifiez Crashlytics/console pour l'erreur d'assertion de cycle de vie
- 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-disposecouvre 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 widgetsflutter-dispose-timer-test-failure: Pour les échecs de tests liés aux timers