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.deactivateVideoPlayerController.pause(ou similaire)ChangeNotifier.notifyListenersValueListenableBuilder._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
-
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.
-
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. -
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. -
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
ChangeNotifierqui met à jour de façon synchrone - Aussi pertinent pour :
AnimationController.stop(), mises à jour deTextEditingController - 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
didUpdateWidgetlors du disposing des anciens contrôleurs
Patterns Connexes
- Utiliser
deactivate()pour le nettoyage qui a besoin deref/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()dansdeactivate()oudispose()