flutter-dispose-timer-test-failure

Par divinevideo · divine-mobile

Corrige l'échec de test Flutter « A Timer is still pending even after the widget tree was disposed ». À utiliser quand : (1) les widget tests échouent avec une erreur de timer en attente, (2) `dispose()` contient des appels `Future()`, `Future.delayed()` ou `Timer`, (3) des modifications de provider Riverpod sont effectuées dans `dispose()`. La solution consiste à utiliser `deactivate()` à la place de `dispose()` pour le nettoyage susceptible de déclencher du travail asynchrone.

npx skills add https://github.com/divinevideo/divine-mobile --skill flutter-dispose-timer-test-failure

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(...) ou Timer.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

  1. Exécutez le test spécifique qui échouait
  2. Exécutez la suite de tests complète
  3. 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

Références

Skills similaires