Question #445MediumFlutter BasicsImportant

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()
and
text
dispose()
are called when a widget is being removed, but they serve very different purposes and are called at different stages.

  • text
    deactivate()
    — Called when the widget is temporarily removed from the widget tree. The widget might come back.
  • text
    dispose()
    — Called when the widget is permanently removed from the widget tree. The widget will never come back.

Lifecycle Flow

text
Widget 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()
is called when the State object is removed from the widget tree. The key point: the State might be re-inserted into the tree (e.g., when using
text
GlobalKey
to move a widget).

dart
class 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()
is called when the State object will never be used again. This is where you do final cleanup of resources.

dart
class 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:

dart
class 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
deactivate()
text
dispose()
Cancel timersNoYes
Cancel stream subscriptionsNoYes
Dispose controllersNoYes
Remove listenersNoYes
Unregister from ancestor widgetsYesNo
Clean up
text
InheritedWidget
refs
YesNo
Check
text
mounted
needed?
No (still valid)No (about to be invalid)

Comparison Table

Aspect
text
deactivate()
text
dispose()
When calledWidget removed from treeWidget permanently destroyed
Widget can return?Yes (via
text
GlobalKey
)
No — never
Called before dispose?Yes — always called firstYes — always called after deactivate
PurposePause tree-dependent operationsFinal resource cleanup
FrequencyCan be called multiple timesCalled exactly once
super call position
text
super.deactivate()
at END
text
super.dispose()
at END
Common usageRare (most devs never need it)Very common (resource cleanup)
State still accessible?YesNo (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

text
dispose()
— not
text
deactivate()
. You only need
text
deactivate()
in rare cases where your widget interacts with ancestor widgets and needs to unregister when moved. 99% of the time, you only need
text
dispose()
.