flutter-deactivate-setstate-during-build

Par divinevideo · divine-mobile

Corrige l'erreur "setState() or markNeedsBuild() called during build" déclenchée depuis `deactivate()`. À utiliser quand : (1) l'erreur survient lors de la destruction d'un widget ou d'une navigation sortante, (2) la stack trace montre `deactivate` -> un notifier -> `ValueListenableBuilder._valueChanged`, (3) les appels à `pause()` ou `stop()` d'un lecteur vidéo/audio dans `deactivate()` provoquent l'erreur. La correction consiste à différer les opérations modifiant l'état via `addPostFrameCallback`.

npx skills add https://github.com/divinevideo/divine-mobile --skill flutter-deactivate-setstate-during-build

Flutter : setState Pendant Build depuis deactivate()

Problème

Lors de la navigation loin d'un écran, appeler des méthodes comme VideoPlayerController.pause() dans deactivate() déclenche setState() or markNeedsBuild() called during build parce que le contrôleur notifie ses listeners de façon synchrone pendant le démontage de l'arborescence des widgets.

Contexte / Conditions de Déclenchement

  • Erreur : setState() or markNeedsBuild() called during build
  • La stack trace inclut :
    • YourWidget.deactivate
    • VideoPlayerController.pause (ou similaire)
    • ChangeNotifier.notifyListeners
    • ValueListenableBuilder._valueChanged
  • Widget utilisant ValueListenableBuilder<VideoPlayerValue> ou similaire
  • Tentative de pause/arrêt de la lecture multimédia lors de la navigation

Solution

Repousser l'opération qui modifie l'état après la frame actuelle :

@override
void deactivate() {
  // Capturer la référence du contrôleur tant que ref/context est valide
  final controller = _getController();

  // Reporter la pause après la frame actuelle
  if (controller != null && controller.value.isPlaying) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (controller.value.isPlaying) {
        controller.pause();
      }
    });
  }

  super.deactivate();
}

Points Clés

  1. Pourquoi deactivate() est problématique : Elle est appelée pendant la phase de démontage de l'arborescence des widgets, qui fait partie du processus de build. Toute notification synchrone aux listeners déclenchera des reconstructions pendant le build.

  2. Pourquoi dispose() n'a pas ce problème : À dispose(), le widget est complètement démonté et les listeners sont généralement déjà supprimés.

  3. Capturer les références tôt : Récupérer les références du contrôleur/état avant le délai asynchrone car ref, context, ou d'autres états du widget peuvent devenir invalides.

  4. Se protéger contre l'exécution double : Vérifier isPlaying à nouveau dans le callback puisque l'état peut avoir changé.

Vérification

  • Naviguer loin de l'écran plusieurs fois
  • Aucune erreur dans la console
  • Le vidéo/audio s'arrête correctement en quittant

Exemple : Implémentation Complète

class _FullscreenVideoScreenState extends ConsumerState<FullscreenVideoScreen> {
  @override
  void deactivate() {
    _schedulePauseCurrentVideo();
    super.deactivate();
  }

  void _schedulePauseCurrentVideo() {
    final controller = _getCurrentController();
    if (controller == null || !controller.value.isInitialized) {
      return;
    }

    // Reporter pour éviter setState pendant le build
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (controller.value.isPlaying) {
        controller.pause();
      }
    });
  }
}

Remarques

  • Ce pattern s'applique à tout ChangeNotifier qui met à jour de façon synchrone
  • Aussi pertinent pour : AnimationController.stop(), mises à jour de TextEditingController
  • Si vous avez besoin que la pause se fasse immédiatement (rare), envisagez d'utiliser controller.pause() sans notifier, bien que cela brise le pattern observer
  • Le même problème peut survenir dans didUpdateWidget lors du disposing des anciens contrôleurs

Patterns Connexes

  • Utiliser deactivate() pour le nettoyage qui a besoin de ref/context (comme se désabonner)
  • Utiliser dispose() pour le nettoyage qui n'en a pas besoin (comme disposer les contrôleurs que vous possédez)
  • Ne jamais appeler setState() dans deactivate() ou dispose()

Skills similaires