Navigating back while a request is running logs:
setState() called after dispose(): _MyPageState#1234(lifecycle state: defunct, not mounted)
It happens after an await in a method. How do I prevent it properly?
DEV3 Developer Forum
Every post is reviewed first. Usually reviewed within 24 hours.
Navigating back while a request is running logs:
setState() called after dispose(): _MyPageState#1234(lifecycle state: defunct, not mounted)
It happens after an await in a method. How do I prevent it properly?
Cause
The async call finishes after the user left the screen, and the code then calls setState on a State that was already disposed.
Fix 1: guard with mounted
Future<void> _load() async {
final data = await api.fetch();
if (!mounted) return;
setState(() => _data = data);
}
Check mounted after every await that precedes setState or context usage.
Fix 2: cancel what you started
Timers, stream subscriptions and animation controllers must be cancelled in dispose:
late final StreamSubscription _sub;
Timer? _timer;
@override
void initState() {
super.initState();
_sub = stream.listen((v) { if (mounted) setState(() => _v = v); });
_timer = Timer.periodic(const Duration(seconds: 5), (_) => _poll());
}
@override
void dispose() {
_sub.cancel();
_timer?.cancel();
super.dispose();
}
For HTTP calls, Dio supports CancelToken which you cancel in dispose.
Fix 3: do not keep the logic in the widget
Move the loading to a Riverpod provider, Bloc or ChangeNotifier owned above the page. The widget only watches state, so there is nothing to call setState on, and results are not lost when the user comes back.
Pitfalls
TextEditingController, AnimationController) without disposing them.initState must not be async; call an async method from it instead.