Explain clearly about difference between deactivate vs dispose in lifecycle of stateful widgets in flutter with some proper examples
#flutter#lifecycle#stateful-widget#deactivate#dispose#state#global-key
Answer
Overview
Both
text
deactivate()text
dispose()- — Called when the widget is temporarily removed from the widget tree. The widget might come back.text
deactivate() - — Called when the widget is permanently removed from the widget tree. The widget will never come back.text
dispose()
Lifecycle Flow
textWidget in tree (active) │ ▼ deactivate() ← Widget removed from tree (temporary) │ ├──→ Widget re-inserted? → didChangeDependencies() → build() │ (e.g., moved with GlobalKey) │ ▼ dispose() ← Widget permanently destroyed (final cleanup) │ ▼ mounted = false ← State object is dead, never reused
deactivate() — Temporary Removal
text
deactivate()text
GlobalKeydartclass MyWidget extends StatefulWidget { const MyWidget({super.key}); State<MyWidget> createState() => _MyWidgetState(); } class _MyWidgetState extends State<MyWidget> { void deactivate() { // ✅ Called when widget is removed from tree // Widget MIGHT be re-inserted somewhere else print('deactivate — removed from tree (maybe temporarily)'); // Good for: // - Removing references from parent/ancestor widgets // - Unregistering from InheritedWidget subscriptions // - Pausing operations that depend on tree position super.deactivate(); // Call super at the END } Widget build(BuildContext context) => Text('Hello'); }
When Does deactivate() Fire?
dart// 1. Widget removed from tree by parent rebuild class Parent extends StatefulWidget { _ParentState createState() => _ParentState(); } class _ParentState extends State<Parent> { bool showChild = true; Widget build(BuildContext context) { return Column( children: [ if (showChild) MyWidget(), // Removing this triggers deactivate() ElevatedButton( onPressed: () => setState(() => showChild = false), child: Text('Remove Child'), ), ], ); } } // Press button → MyWidget.deactivate() → MyWidget.dispose()
dart// 2. Widget MOVED using GlobalKey (deactivate WITHOUT dispose) final key = GlobalKey(); // Before: MyWidget is in Column A Column(children: [MyWidget(key: key)]) // After: MyWidget moved to Column B Row(children: [MyWidget(key: key)]) // Flow: deactivate() → re-insert → didChangeDependencies() → build() // dispose() is NOT called — widget was moved, not destroyed
dispose() — Permanent Destruction
text
dispose()dartclass TimerWidget extends StatefulWidget { const TimerWidget({super.key}); State<TimerWidget> createState() => _TimerWidgetState(); } class _TimerWidgetState extends State<TimerWidget> { late final TextEditingController _textController; late final ScrollController _scrollController; late final AnimationController _animController; Timer? _timer; StreamSubscription? _subscription; void initState() { super.initState(); _textController = TextEditingController(); _scrollController = ScrollController(); _timer = Timer.periodic(Duration(seconds: 1), (_) => _tick()); _subscription = myStream.listen((data) => _handleData(data)); } void _tick() => print('tick'); void _handleData(dynamic data) => print(data); void dispose() { // ✅ Clean up ALL resources here — widget will NEVER come back _textController.dispose(); // Free text controller _scrollController.dispose(); // Free scroll controller _animController.dispose(); // Free animation controller _timer?.cancel(); // Stop timer _subscription?.cancel(); // Cancel stream subscription print('dispose — permanently destroyed, resources freed'); super.dispose(); // Call super at the END } Widget build(BuildContext context) => TextField(controller: _textController); }
Key Difference: GlobalKey Example
This is the best example to understand the difference — when a widget is moved instead of destroyed:
dartclass GlobalKeyExample extends StatefulWidget { _GlobalKeyExampleState createState() => _GlobalKeyExampleState(); } class _GlobalKeyExampleState extends State<GlobalKeyExample> { final _childKey = GlobalKey(); bool isLeft = true; Widget build(BuildContext context) { return Column( children: [ Row( children: [ // Widget moves between left and right if (isLeft) Expanded(child: CounterWidget(key: _childKey)), if (!isLeft) Expanded(child: SizedBox()), if (!isLeft) Expanded(child: CounterWidget(key: _childKey)), if (isLeft) Expanded(child: SizedBox()), ], ), ElevatedButton( onPressed: () => setState(() => isLeft = !isLeft), child: Text('Move Widget'), ), ], ); } } class CounterWidget extends StatefulWidget { const CounterWidget({super.key}); _CounterWidgetState createState() => _CounterWidgetState(); } class _CounterWidgetState extends State<CounterWidget> { int count = 0; void deactivate() { print('deactivate — removed from current position'); super.deactivate(); } void dispose() { print('dispose — permanently destroyed'); super.dispose(); } Widget build(BuildContext context) { return ElevatedButton( onPressed: () => setState(() => count++), child: Text('Count: $count'), ); } } // Press "Move Widget" button: // Output: "deactivate — removed from current position" // ❌ dispose() is NOT called! // Widget moves to new position with state PRESERVED (count stays same)
What to Do in Each Method
| Action | text | text |
|---|---|---|
| Cancel timers | No | Yes |
| Cancel stream subscriptions | No | Yes |
| Dispose controllers | No | Yes |
| Remove listeners | No | Yes |
| Unregister from ancestor widgets | Yes | No |
| Clean up text | Yes | No |
| Check text | No (still valid) | No (about to be invalid) |
Comparison Table
| Aspect | text | text |
|---|---|---|
| When called | Widget removed from tree | Widget permanently destroyed |
| Widget can return? | Yes (via text | No — never |
| Called before dispose? | Yes — always called first | Yes — always called after deactivate |
| Purpose | Pause tree-dependent operations | Final resource cleanup |
| Frequency | Can be called multiple times | Called exactly once |
| super call position | text | text |
| Common usage | Rare (most devs never need it) | Very common (resource cleanup) |
| State still accessible? | Yes | No (after super.dispose) |
Common Mistakes
dart// ❌ Wrong — disposing resources in deactivate void deactivate() { _controller.dispose(); // ❌ Widget might come back! super.deactivate(); } // ✅ Correct — dispose resources only in dispose() void dispose() { _controller.dispose(); // ✅ Widget is gone forever super.dispose(); } // ❌ Wrong — calling super first in dispose void dispose() { super.dispose(); // ❌ State invalidated before cleanup _controller.dispose(); // May crash — state already dead } // ✅ Correct — call super.dispose() LAST void dispose() { _controller.dispose(); // ✅ Clean up first super.dispose(); // ✅ Then invalidate state }
Rule of Thumb: Put resource cleanup (controllers, timers, streams, listeners) in
— nottextdispose(). You only needtextdeactivate()in rare cases where your widget interacts with ancestor widgets and needs to unregister when moved. 99% of the time, you only needtextdeactivate().textdispose()