In dispose function in StatefulWidget in Flutter, should super.dispose() be called before any function or after any function?
#dispose#lifecycle#statefulwidget#memory-management#best-practices
Answer
Quick Answer
must be called LAST — after all your cleanup code.textsuper.dispose()
dartvoid dispose() { // ✅ FIRST - Clean up your resources _controller.dispose(); _timer?.cancel(); _subscription?.cancel(); super.dispose(); // ✅ LAST - Framework cleanup }
Why Call super.dispose() Last?
Reason 1: Resource Cleanup Order
You need to clean up your resources first before the framework cleans up its internal state.
Correct order:
- Cancel timers
- Close streams
- Dispose controllers
- Remove listeners
- Then call (framework cleanup)text
super.dispose()
Reason 2: Access to Widget State
Until
text
super.dispose()dartvoid dispose() { // ✅ Widget state still available _controller.dispose(); _timer?.cancel(); super.dispose(); // ✅ Last - releases widget state // ❌ After super.dispose(), widget state is gone }
Official Documentation
From Flutter's State documentation:
"If you override this method, make sure to call
as the last line."textsuper.dispose()
This is the opposite of
text
initState()text
super.initState()Correct Pattern
✅ Complete Example
dartclass _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin { late AnimationController _animationController; late TextEditingController _textController; late ScrollController _scrollController; Timer? _timer; StreamSubscription? _subscription; void initState() { super.initState(); // ✅ FIRST in initState _animationController = AnimationController( vsync: this, duration: Duration(seconds: 2), ); _textController = TextEditingController(); _scrollController = ScrollController(); _timer = Timer.periodic(Duration(seconds: 1), (timer) { print('Tick ${timer.tick}'); }); _subscription = someStream.listen((data) { print('Received: $data'); }); // Add listeners _animationController.addListener(_onAnimationUpdate); _scrollController.addListener(_onScrollUpdate); } void _onAnimationUpdate() { // Handle animation update } void _onScrollUpdate() { // Handle scroll update } void dispose() { // ✅ Clean up in REVERSE order of initialization // 1. Remove listeners first _animationController.removeListener(_onAnimationUpdate); _scrollController.removeListener(_onScrollUpdate); // 2. Dispose controllers _animationController.dispose(); _textController.dispose(); _scrollController.dispose(); // 3. Cancel timers _timer?.cancel(); // 4. Cancel subscriptions _subscription?.cancel(); // 5. LAST - Framework cleanup super.dispose(); // ✅ LAST } Widget build(BuildContext context) { return Container(); } }
❌ Wrong Example
dartvoid dispose() { super.dispose(); // ❌ WRONG - Called too early // ❌ These might not work properly now _controller.dispose(); _timer?.cancel(); }
Why it's wrong:
- Widget state already released
- Cleanup might fail silently
- Memory leaks possible
- Framework state inconsistencies
Common Use Cases
1. Disposing Controllers
dartvoid dispose() { // Dispose all controllers first _animationController.dispose(); _textEditingController.dispose(); _scrollController.dispose(); _tabController.dispose(); _pageController.dispose(); super.dispose(); // ✅ Last }
2. Canceling Timers
dartvoid dispose() { // Cancel all timers _periodicTimer?.cancel(); _delayedTimer?.cancel(); super.dispose(); // ✅ Last }
3. Closing Streams and Subscriptions
dartvoid dispose() { // Close stream controllers _streamController.close(); // Cancel subscriptions _subscription?.cancel(); _multipleSubscriptions.forEach((sub) => sub.cancel()); super.dispose(); // ✅ Last }
4. Removing Listeners
dartvoid dispose() { // Remove all listeners before disposing _controller.removeListener(_onUpdate); _textController.removeListener(_onTextChange); _focusNode.removeListener(_onFocusChange); // Then dispose _controller.dispose(); _textController.dispose(); _focusNode.dispose(); super.dispose(); // ✅ Last }
5. Disposing Focus Nodes
dartvoid dispose() { // Dispose focus nodes _emailFocusNode.dispose(); _passwordFocusNode.dispose(); super.dispose(); // ✅ Last }
Cleanup Order Best Practice
Recommended Order
dartvoid dispose() { // 1. Remove listeners (they might reference disposed objects) _controller.removeListener(_listener); // 2. Cancel async operations _timer?.cancel(); _subscription?.cancel(); // 3. Close streams _streamController.close(); // 4. Dispose controllers and nodes _animationController.dispose(); _textController.dispose(); _focusNode.dispose(); // 5. Nullify large objects (optional, for memory) _largeDataList = null; // 6. LAST - Framework cleanup super.dispose(); }
What Happens If You Don't Call super.dispose()?
Analyzer Warning
dartvoid dispose() { _controller.dispose(); // Missing super.dispose() } // ⚠️ Analyzer warning: // "Missing call to 'super.dispose()'"
Runtime Issues
Potential problems:
- Memory leaks (framework can't clean up properly)
- Widget not removed from widget tree correctly
- Framework state inconsistencies
- Resources not freed
- Performance degradation over time
initState vs dispose Order Comparison
| Method | super Call Placement | Reason |
|---|---|---|
text | FIRST (before your code) | Framework initializes → then you initialize |
text | LAST (after your code) | You clean up → then framework cleans up |
Side-by-Side Comparison
dartclass _MyWidgetState extends State<MyWidget> { late AnimationController _controller; late Timer _timer; void initState() { super.initState(); // ✅ FIRST - Framework sets up // Your initialization (framework is ready) _controller = AnimationController(vsync: this); _timer = Timer.periodic(Duration(seconds: 1), (_) {}); } void dispose() { // Your cleanup first (while framework state available) _controller.dispose(); _timer.cancel(); super.dispose(); // ✅ LAST - Framework tears down } Widget build(BuildContext context) { return Container(); } }
Best Practices
✅ DO
dartvoid dispose() { // Clean up in logical order _removeListeners(); _cancelTimers(); _disposeControllers(); super.dispose(); // ✅ Last line }
dartvoid dispose() { // Use try-catch for critical cleanup try { _controller.dispose(); } catch (e) { print('Error disposing controller: $e'); } super.dispose(); // ✅ Still call even if errors }
❌ DON'T
dartvoid dispose() { super.dispose(); // ❌ Don't call first _controller.dispose(); }
dartvoid dispose() { _controller.dispose(); // ❌ Don't forget super.dispose() }
dartvoid dispose() { setState(() {}); // ❌ Never call setState in dispose super.dispose(); }
Memory Leak Detection
Check for Memory Leaks
dartclass _MyWidgetState extends State<MyWidget> { late Timer _timer; void initState() { super.initState(); _timer = Timer.periodic(Duration(seconds: 1), (_) { print('Timer tick'); // This will keep running if not canceled! }); } void dispose() { // ✅ Must cancel timer to prevent memory leak _timer.cancel(); super.dispose(); } Widget build(BuildContext context) { return Container(); } }
Without :text_timer.cancel()
- Timer keeps running even after widget is removed
- References to widget kept alive
- Memory leak grows over time
- App performance degrades
Testing Your Cleanup
Use Flutter DevTools
- Open DevTools Memory tab
- Navigate to your screen
- Pop back (trigger dispose)
- Force garbage collection
- Check if widget instances are freed
Debug Print
dartvoid dispose() { print('🗑️ Disposing MyWidget'); _controller.dispose(); _timer?.cancel(); super.dispose(); print('✅ MyWidget disposed'); }
Common Mistakes
Mistake 1: Calling super.dispose() First
dart// ❌ WRONG void dispose() { super.dispose(); // ❌ Too early _controller.dispose(); // Might not work properly } // ✅ CORRECT void dispose() { _controller.dispose(); // ✅ Your cleanup first super.dispose(); // ✅ Framework cleanup last }
Mistake 2: Forgetting to Dispose Controllers
dart// ❌ WRONG - Memory leak! void dispose() { // Forgot to dispose _controller super.dispose(); } // ✅ CORRECT void dispose() { _controller.dispose(); // ✅ Always dispose controllers super.dispose(); }
Mistake 3: Not Canceling Timers
dart// ❌ WRONG - Timer keeps running! void dispose() { _controller.dispose(); // Forgot to cancel _timer super.dispose(); } // ✅ CORRECT void dispose() { _controller.dispose(); _timer?.cancel(); // ✅ Cancel timers super.dispose(); }
When dispose() is Called
Scenarios
dart// 1. Navigator.pop() - Going back Navigator.pop(context); // 2. Navigator.pushReplacement() - Replacing current route Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => NewPage())); // 3. Removed from widget tree if (showWidget) MyWidget() // When showWidget becomes false, dispose() is called // 4. Hot reload (in debug mode) // - dispose() called on old widget // - initState() called on new widget
Summary
Rule: Always call
as the last line in yourtextsuper.dispose()method.textdispose()
Why: Clean up your resources first while widget state is still available, then let the framework clean up.
Opposite: In
, calltextinitState()as the first line (framework initializes before you).textsuper.initState()
Required: Not optional — Flutter will show an analyzer warning if you forget.
Memory: Proper dispose prevents memory leaks, timer leaks, and resource exhaustion.