Question #433MediumFlutter BasicsImportant

Real-world StatefulWidget lifecycle scenarios: 1) Inside widget if setState is called, which lifecycle method is called - didUpdateWidget or didChangeDependencies? 2) App is running and device theme changes (light/dark mode), which lifecycle method is called? 3) Device orientation changes, which lifecycle method? 4) Stream data updated, which lifecycle method? 5) State management (BLOC/Riverpod) state changes, which lifecycle method?

#lifecycle#setState#theme#orientation#stream#state-management#bloc#riverpod

Answer

Overview

Understanding which StatefulWidget lifecycle methods are called in real-world scenarios is crucial for effective state management, performance optimization, and avoiding common bugs. This question covers 5 practical scenarios that every Flutter developer encounters.


Quick Reference Table

ScenarioLifecycle Method CalledReason
1. setState() called
text
build()
only
setState schedules a rebuild
2. Theme changed (light/dark)
text
didChangeDependencies()
text
build()
Theme is an InheritedWidget
3. Orientation changed
text
didChangeDependencies()
text
build()
MediaQuery changes (InheritedWidget)
4. Stream data updatedNone automaticallyMust call
text
setState()
in listener →
text
build()
5. State management update (BLOC/Riverpod)
text
build()
(via framework)
Widget rebuilds when watching/listening to state

Scenario 1: setState() Called Inside Widget

Question

If setState is called inside the widget, will

text
didUpdateWidget
or
text
didChangeDependencies
be called?

Answer

Neither! Only

text
build()
is called.

Explanation

dart
class CounterWidget extends StatefulWidget {
  
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  
  void initState() {
    super.initState();
    print('1. initState called');
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies called');
  }

  
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('3. didUpdateWidget called');
  }

  
  Widget build(BuildContext context) {
    print('4. build called');
    return Column(
      children: [
        Text('Counter: $_counter'),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _counter++;
              print('setState called');
            });
          },
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Output when button is pressed:

text
setState called
4. build called

Why?

  • text
    setState()
    directly schedules a rebuild by calling
    text
    build()
  • text
    didUpdateWidget()
    is only called when the parent passes a new widget configuration
  • text
    didChangeDependencies()
    is only called when InheritedWidget dependencies change
  • Internal state changes (via setState) don't trigger either method

Scenario 2: Device Theme Changed (Light ↔ Dark Mode)

Question

Application is running. If device theme changes from light to dark mode (or vice versa), which lifecycle method is called?

Answer

text
didChangeDependencies()
followed by
text
build()

Explanation

dart
class ThemeAwareWidget extends StatefulWidget {
  
  _ThemeAwareWidgetState createState() => _ThemeAwareWidgetState();
}

class _ThemeAwareWidgetState extends State<ThemeAwareWidget> {
  late Brightness _currentBrightness;

  
  void initState() {
    super.initState();
    print('1. initState called');
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies called');

    // Theme is accessed here
    final theme = Theme.of(context);
    _currentBrightness = theme.brightness;
    print('   Current theme: ${_currentBrightness == Brightness.dark ? "Dark" : "Light"}');
  }

  
  Widget build(BuildContext context) {
    print('3. build called');
    final theme = Theme.of(context);

    return Container(
      color: theme.brightness == Brightness.dark
          ? Colors.black
          : Colors.white,
      child: Text(
        'Current theme: ${theme.brightness == Brightness.dark ? "Dark" : "Light"}',
        style: TextStyle(
          color: theme.brightness == Brightness.dark
              ? Colors.white
              : Colors.black,
        ),
      ),
    );
  }
}

Output when theme changes (light → dark):

text
2. didChangeDependencies called
   Current theme: Dark
3. build called

Why?

  • text
    Theme
    is provided by
    text
    Theme
    widget, which is an InheritedWidget
  • When system theme changes, Flutter rebuilds the
    text
    MaterialApp
    with new ThemeData
  • This updates the Theme InheritedWidget
  • All widgets that access
    text
    Theme.of(context)
    have their
    text
    didChangeDependencies()
    called
  • Then
    text
    build()
    is called to rebuild with new theme

Scenario 3: Device Orientation Changed

Question

If device orientation changes (portrait ↔ landscape), which lifecycle method is called?

Answer

text
didChangeDependencies()
followed by
text
build()

Explanation

dart
class OrientationAwareWidget extends StatefulWidget {
  
  _OrientationAwareWidgetState createState() => _OrientationAwareWidgetState();
}

class _OrientationAwareWidgetState extends State<OrientationAwareWidget> {
  Orientation? _previousOrientation;

  
  void initState() {
    super.initState();
    print('1. initState called');
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies called');

    final orientation = MediaQuery.of(context).orientation;

    if (_previousOrientation != null && _previousOrientation != orientation) {
      print('   Orientation changed: $_previousOrientation$orientation');
    }

    _previousOrientation = orientation;
  }

  
  Widget build(BuildContext context) {
    print('3. build called');
    final orientation = MediaQuery.of(context).orientation;
    final size = MediaQuery.of(context).size;

    return orientation == Orientation.portrait
        ? _buildPortraitLayout(size)
        : _buildLandscapeLayout(size);
  }

  Widget _buildPortraitLayout(Size size) {
    return Column(
      children: [
        Text('Portrait Mode'),
        Text('Width: ${size.width}, Height: ${size.height}'),
      ],
    );
  }

  Widget _buildLandscapeLayout(Size size) {
    return Row(
      children: [
        Text('Landscape Mode'),
        Text('Width: ${size.width}, Height: ${size.height}'),
      ],
    );
  }
}

Output when rotating device (portrait → landscape):

text
2. didChangeDependencies called
   Orientation changed: Orientation.portrait → Orientation.landscape
3. build called

Why?

  • text
    MediaQuery
    is an InheritedWidget that provides device information
  • When orientation changes, screen dimensions change
  • Flutter updates MediaQuery with new orientation and size
  • All widgets using
    text
    MediaQuery.of(context)
    have their
    text
    didChangeDependencies()
    called
  • Then
    text
    build()
    is called to rebuild with new layout

Important: State persists across orientation changes (unlike Android Activities)


Scenario 4: Stream Data Updated

Question

If you're using a Stream and data is updated in the stream listener, which lifecycle method is called?

Answer

None automatically — You must manually call

text
setState()
in the listener, which triggers
text
build()

Explanation

dart
import 'dart:async';

class StreamListenerWidget extends StatefulWidget {
  
  _StreamListenerWidgetState createState() => _StreamListenerWidgetState();
}

class _StreamListenerWidgetState extends State<StreamListenerWidget> {
  final _controller = StreamController<int>();
  late StreamSubscription<int> _subscription;
  int _currentValue = 0;

  
  void initState() {
    super.initState();
    print('1. initState called');

    // Subscribe to stream
    _subscription = _controller.stream.listen((data) {
      print('   Stream received data: $data');

      // ✅ MUST call setState to rebuild UI
      setState(() {
        _currentValue = data;
        print('   setState called in listener');
      });
    });

    // Simulate stream data
    _simulateStreamData();
  }

  void _simulateStreamData() async {
    await Future.delayed(Duration(seconds: 2));
    _controller.add(10);

    await Future.delayed(Duration(seconds: 2));
    _controller.add(20);
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies called');
  }

  
  Widget build(BuildContext context) {
    print('3. build called');
    return Text('Stream value: $_currentValue');
  }

  
  void dispose() {
    _subscription.cancel();
    _controller.close();
    super.dispose();
  }
}

Output:

text
1. initState called
2. didChangeDependencies called
3. build called

// After 2 seconds:
   Stream received data: 10
   setState called in listener
3. build called

// After 4 seconds:
   Stream received data: 20
   setState called in listener
3. build called

Why?

  • Streams are not part of the widget lifecycle
  • When stream emits data, the listener callback is executed
  • The callback must explicitly call
    text
    setState()
    to trigger a rebuild
  • Without
    text
    setState()
    , the UI won't update even though
    text
    _currentValue
    changed

❌ Common Mistake

dart
// ❌ WRONG - UI won't update!
_subscription = _controller.stream.listen((data) {
  _currentValue = data; // Changed but no setState!
  // build() is NOT called!
});

✅ Correct Approach

dart
// ✅ CORRECT - UI updates
_subscription = _controller.stream.listen((data) {
  setState(() {
    _currentValue = data;
  }); // Triggers build()
});

Scenario 5: State Management (BLOC/Riverpod) Changes

Question

If you're using state management like BLOC or Riverpod and state changes, which lifecycle method is called?

Answer

text
build()
is called automatically (no other lifecycle methods)

Explanation

State management frameworks trigger rebuilds through the Flutter framework, not through StatefulWidget lifecycle methods.

Example 1: BLOC

dart
import 'package:flutter_bloc/flutter_bloc.dart';

class CounterBloc extends Cubit<int> {
  CounterBloc() : super(0);

  void increment() => emit(state + 1);
}

class BlocCounterWidget extends StatefulWidget {
  
  _BlocCounterWidgetState createState() => _BlocCounterWidgetState();
}

class _BlocCounterWidgetState extends State<BlocCounterWidget> {
  
  void initState() {
    super.initState();
    print('1. initState called');
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies called');
  }

  
  void didUpdateWidget(BlocCounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('3. didUpdateWidget called');
  }

  
  Widget build(BuildContext context) {
    print('4. build called');

    return BlocBuilder<CounterBloc, int>(
      builder: (context, count) {
        print('   BlocBuilder rebuilding with state: $count');
        return Column(
          children: [
            Text('Count: $count'),
            ElevatedButton(
              onPressed: () {
                context.read<CounterBloc>().increment();
                print('   Bloc increment called');
              },
              child: Text('Increment'),
            ),
          ],
        );
      },
    );
  }
}

Output when increment button is pressed:

text
   Bloc increment called
4. build called
   BlocBuilder rebuilding with state: 1

Why?

  • BlocBuilder listens to the BLOC's state stream
  • When state changes, BlocBuilder internally calls
    text
    setState()
    on its own State
  • This triggers a rebuild of the parent widget's
    text
    build()
    method
  • text
    didChangeDependencies()
    and
    text
    didUpdateWidget()
    are NOT called
  • BLOC manages the rebuild mechanism internally

Example 2: Riverpod

dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

final counterProvider = StateProvider<int>((ref) => 0);

class RiverpodCounterWidget extends ConsumerStatefulWidget {
  
  _RiverpodCounterWidgetState createState() => _RiverpodCounterWidgetState();
}

class _RiverpodCounterWidgetState extends ConsumerState<RiverpodCounterWidget> {
  
  void initState() {
    super.initState();
    print('1. initState called');
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies called');
  }

  
  void didUpdateWidget(RiverpodCounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('3. didUpdateWidget called');
  }

  
  Widget build(BuildContext context) {
    print('4. build called');

    final count = ref.watch(counterProvider);
    print('   Current count from Riverpod: $count');

    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () {
            ref.read(counterProvider.notifier).state++;
            print('   Riverpod state incremented');
          },
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Output when increment button is pressed:

text
   Riverpod state incremented
4. build called
   Current count from Riverpod: 1

Why?

  • text
    ref.watch(counterProvider)
    sets up a listener for state changes
  • When state changes, Riverpod internally triggers a rebuild
  • Only
    text
    build()
    is called (not didChangeDependencies or didUpdateWidget)
  • Riverpod uses InheritedWidget under the hood but manages rebuilds efficiently

Complete Comparison Table

ScenarioinitStatedidChangeDependenciesdidUpdateWidgetbuildWhy?
setState() calledsetState directly schedules rebuild
Theme changedTheme is InheritedWidget
Orientation changedMediaQuery is InheritedWidget
Stream data updated✅**Only if setState called in listener
BLOC state changedBLOC triggers rebuild internally
Riverpod state changedRiverpod triggers rebuild via ref.watch

When Each Lifecycle Method IS Called

initState()

  • ✅ Widget first created
  • ❌ Never called again (unless widget is disposed and recreated)

didChangeDependencies()

  • ✅ After initState()
  • ✅ When InheritedWidget dependencies change (Theme, MediaQuery, Locale, etc.)
  • ❌ NOT called for setState
  • ❌ NOT called for stream updates
  • ❌ NOT called for state management updates

didUpdateWidget()

  • ✅ When parent rebuilds and passes new widget configuration
  • ❌ NOT called for setState
  • ❌ NOT called for theme/orientation changes
  • ❌ NOT called for state management updates

build()

  • ✅ After initState() and didChangeDependencies()
  • ✅ After setState()
  • ✅ After didUpdateWidget()
  • ✅ After theme/orientation changes (via didChangeDependencies)
  • ✅ When parent rebuilds
  • ✅ When state management triggers rebuild

Best Practices

1. Use didChangeDependencies for InheritedWidget Changes

dart
Good:

void didChangeDependencies() {
  super.didChangeDependencies();

  final theme = Theme.of(context);
  final mediaQuery = MediaQuery.of(context);

  if (mediaQuery.orientation == Orientation.landscape) {
    // Handle landscape-specific logic
  }
}

2. Always Call setState for Manual Updates

dart
Good:
_streamSubscription = stream.listen((data) {
  setState(() {
    _data = data;
  });
});

Bad:
_streamSubscription = stream.listen((data) {
  _data = data; // UI won't update!
});

3. Use State Management for Complex State

dart
Good - Use BLOC for complex state:
class UserProfileWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocBuilder<UserBloc, UserState>(
      builder: (context, state) {
        return state.when(
          loading: () => CircularProgressIndicator(),
          loaded: (user) => Text(user.name),
          error: (error) => Text('Error: $error'),
        );
      },
    );
  }
}

Bad - Manual setState for complex async logic:
class UserProfileWidget extends StatefulWidget { ... }
class _UserProfileWidgetState extends State<UserProfileWidget> {
  User? _user;
  bool _isLoading = false;
  String? _error;

  // Lots of manual setState calls, error-prone
}

4. Detect Orientation Changes Correctly

dart
Good:

void didChangeDependencies() {
  super.didChangeDependencies();

  final newOrientation = MediaQuery.of(context).orientation;
  if (_previousOrientation != newOrientation) {
    // Orientation actually changed
    _handleOrientationChange(newOrientation);
  }
  _previousOrientation = newOrientation;
}

Bad - Using build():

Widget build(BuildContext context) {
  final orientation = MediaQuery.of(context).orientation;
  // ❌ This runs on EVERY build, not just orientation changes!
  _handleOrientationChange(orientation);
  return ...
}

Common Mistakes

Mistake 1: Expecting didChangeDependencies on setState

dart
// ❌ WRONG ASSUMPTION
setState(() {
  _counter++;
});
// User expects didChangeDependencies to be called - it's NOT!

Mistake 2: Not Calling setState in Stream Listener

dart
// ❌ WRONG
_subscription = stream.listen((data) {
  _value = data; // Changed but no rebuild!
});

// ✅ CORRECT
_subscription = stream.listen((data) {
  setState(() {
    _value = data;
  });
});

Mistake 3: Using build() for Side Effects

dart
// ❌ WRONG - build() is called frequently!

Widget build(BuildContext context) {
  _logAnalytics(); // Called multiple times per second during animations!
  return ...
}

// ✅ CORRECT - Use didChangeDependencies for one-time effects

void didChangeDependencies() {
  super.didChangeDependencies();
  _logAnalytics(); // Called only when dependencies change
}

Summary

setState() → Only

text
build()
is called

Theme/Orientation Change

text
didChangeDependencies()
text
build()
(InheritedWidget changed)

Stream Update → Manual

text
setState()
required →
text
build()

State Management (BLOC/Riverpod) → Framework triggers

text
build()
automatically

didUpdateWidget() → Only when parent passes new widget config


Resources