Flutter: setState During Build from deactivate()
SkillMediaFix "setState() or markNeedsBuild() called during build" error triggered from deactivate(). Use when: (1) Error occurs during widget disposal or navigation away, (2) Stack trace shows deactivate -> some notifier -> ValueListenableBuilder._valueChanged, (3) Video/audio player pause() or stop() calls in deactivate() cause the error. The fix is deferring state-modifying operations to addPostFrameCallback.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Flutter: setState During Build from deactivate() skill
What this skill tells your AI
The instructions your AI receives, as published by divinevideo/divine-mobile in .agents/skills/flutter-deactivate-setstate-during-build/SKILL.md and read by ahel’s review.
Problem
When navigating away from a screen, calling methods like VideoPlayerController.pause() in
deactivate() triggers setState() or markNeedsBuild() called during build because the
controller notifies its listeners synchronously during widget tree teardown.
Context / Trigger Conditions
- Error:
setState() or markNeedsBuild() called during build - Stack trace includes:
YourWidget.deactivateVideoPlayerController.pause(or similar)ChangeNotifier.notifyListenersValueListenableBuilder._valueChanged
- Widget using
ValueListenableBuilder<VideoPlayerValue>or similar - Trying to pause/stop media playback when navigating away
Solution
Defer the state-modifying operation to after the current frame:
@override
void deactivate() {
// Capture controller reference while ref/context is still valid
final controller = _getController();
// Defer pause to after current frame
if (controller != null && controller.value.isPlaying) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.value.isPlaying) {
controller.pause();
}
});
}
super.deactivate();
}
Key Insights
-
Why deactivate() is problematic: It's called during the widget tree teardown phase, which is part of the build process. Any synchronous notification to listeners will trigger rebuilds during build.
-
Why dispose() doesn't have this issue: By
dispose(), the widget is fully unmounted and listeners are typically already removed. -
Capture references early: Get controller/state references before the async gap since
ref,context, or other widget state may become invalid. -
Guard against double-execution: Check
isPlayingagain in the callback since state may have changed.
Verification
- Navigate away from the screen multiple times
- No error in console
- Video/audio properly pauses when leaving
Example: Full Implementation
class _FullscreenVideoScreenState extends ConsumerState<FullscreenVideoScreen> {
@override
void deactivate() {
_schedulePauseCurrentVideo();
super.deactivate();
}
void _schedulePauseCurrentVideo() {
final controller = _getCurrentController();
if (controller == null || !controller.value.isInitialized) {
return;
}
// Defer to avoid setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
if (controller.value.isPlaying) {
controller.pause();
}
});
}
}
Notes
- This pattern applies to any
ChangeNotifierthat updates synchronously - Also relevant for:
AnimationController.stop(),TextEditingControllerupdates - If you need the pause to happen immediately (rare), consider using
controller.pause()without notifying, though this breaks the observer pattern - The same issue can occur in
didUpdateWidgetwhen old controllers are disposed
Related Patterns
- Use
deactivate()for cleanup that needsref/context(like unsubscribing) - Use
dispose()for cleanup that doesn't (like disposing controllers you own) - Never call
setState()indeactivate()ordispose()
Signals
- GitHub stars
- 264
- Forks
- 55
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
flutter-deactivate-setstate-during-build- Source
- github.com/divinevideo/divine-mobile