Erreur de test Flutter : Timer non supprimé lors de dispose
Problème
Les tests de widgets Flutter échouent avec l'erreur « A Timer is still pending even after the widget tree was disposed » quand dispose() contient du code de nettoyage basé sur Future.
Contexte / Conditions déclencheurs
- Erreur de test :
A Timer is still pending even after the widget tree was disposed - La méthode
dispose()du widget contient :Future(() => ...)Future.delayed(...)Timer(...)ouTimer.periodic(...)- Riverpod
ref.read(provider.notifier).someMethod()enveloppé dans Future
- Les tests réussissent individuellement mais échouent s'ils sont exécutés ensemble
- L'erreur apparaît à la fin du test, pas lors de l'interaction avec le widget
Solution
Étape 1 : Identifier le code problématique
Cherchez des patterns comme celui-ci dans votre widget :
// MAUVAIS : Crée un timer en attente que le framework de test détecte
@override
void dispose() {
final notifier = _overlayNotifier;
if (notifier != null) {
Future(() => notifier.setSettingsOpen(false)); // <- Problème !
}
super.dispose();
}
Étape 2 : Déplacer le nettoyage dans deactivate()
Utilisez deactivate() au lieu de dispose() et supprimez le wrapper Future :
// BON : S'exécute de manière synchrone avant le retrait du widget
@override
void deactivate() {
_overlayNotifier?.setSettingsOpen(false); // Appel direct, pas de Future
super.deactivate();
}
Étape 3 : Comprendre la différence du cycle de vie
deactivate(): Appelé quand le widget est retiré de l'arbre, mais State peut être réinsérédispose(): Appelé quand State ne sera plus jamais construit, nettoyage permanent
Pour la plupart des nettoyages de notifications/providers, deactivate() est approprié.
Vérification
- Exécutez le test spécifique qui échouait
- Exécutez la suite de tests complète
- Vérifiez qu'il n'y a pas d'erreurs « Timer is still pending »
Exemple
Avant (cause l'échec du test) :
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
OverlayNotifier? _overlayNotifier;
@override
void dispose() {
final notifier = _overlayNotifier;
if (notifier != null) {
// Ce Future crée un timer en attente !
Future(() => notifier.setSettingsOpen(false));
}
super.dispose();
}
}
Après (les tests passent) :
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
OverlayNotifier? _overlayNotifier;
@override
void deactivate() {
// Appel synchrone, aucun timer créé
_overlayNotifier?.setSettingsOpen(false);
super.deactivate();
}
}
Remarques
- Le pattern original
Future(() => ...)est souvent utilisé pour différer les modifications de providers jusqu'après la phase de build actuelle, en évitant les erreurs « Cannot modify provider during build » - Si vous avez besoin d'une exécution différée, envisagez
WidgetsBinding.instance.addPostFrameCallbackà la place, bien que cela crée aussi des timers qui peuvent faire échouer les tests - Pour Riverpod, modifier les providers dans
deactivate()est généralement sûr car l'arbre de widgets est en cours de destruction, pas de construction - Ce problème survient souvent quand plusieurs tests s'exécutent séquentiellement, car le framework de test vérifie les timers en attente entre les tests