Ahmet Balaman LogoAhmet Balaman

Flutter Life Cycle - StatefulWidget and AppLifecycleState Management

personAhmet Balaman
calendar_today
FlutterLife CycleStatefulWidgetState ManagementApp Lifecycle

Flutter Life Cycle

The first time I needed to make an API call, I wondered "Where should I write this?" In the build() method? No, because it would be called every time the screen refreshes. This is where the life cycle comes into play.

In Flutter, there are two types of widgets: StatelessWidget and StatefulWidget. StatelessWidget doesn't have a special life cycle (because it doesn't change), but StatefulWidget has a very rich life cycle.

Live Demo: Life Cycle Methods

Try StatefulWidget life cycle methods interactively:

StatefulWidget Life Cycle

A StatefulWidget's life progresses like this:

createState() → initState() → didChangeDependencies() → build()
                                         ↓
                    setState() ←→ build() ←→ didUpdateWidget()
                                         ↓
                                    dispose()

1. createState()

Called when the widget is first created. Creates the State object.

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() {
    print('createState executed');
    return _MyWidgetState();
  }
}

When called: When the widget is created, only once.

What it's for: Returning the State class. Usually doesn't need customization.

2. initState()

Called immediately after the State object is created. Initial setup is done here.

class _MyWidgetState extends State<MyWidget> {
  int counter = 0;
  late Timer timer;
  
  @override
  void initState() {
    super.initState(); // Must call this!
    
    print('initState executed');
    
    // Initial setup
    counter = 0;
    _loadData();
    _startTimer();
  }
  
  void _loadData() async {
    // API call
    final data = await fetchDataFromAPI();
    setState(() {
      // Save data to state
    });
  }
  
  void _startTimer() {
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      setState(() {
        counter++;
      });
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Text('Counter: $counter');
  }
}

When called: When the widget is first created, only once.

What it's for:

  • Assigning initial values to variables
  • Making API calls
  • Creating streams/controllers
  • Starting timers
  • Adding event listeners

Important: Don't forget to call super.initState()!

3. didChangeDependencies()

Called when the widget's dependencies change.

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  
  print('didChangeDependencies executed');
  
  // Did theme change?
  final theme = Theme.of(context);
  
  // Did MediaQuery change? (screen size, orientation)
  final screenSize = MediaQuery.of(context).size;
}

When called:

  • Right after initState()
  • When InheritedWidget changes (Theme, MediaQuery, etc.)

What it's for:

  • Operations dependent on context
  • Catching Theme, Locale changes

Note: Can't use context in initState(), but can use it here.

4. build()

Creates the widget's view.

@override
Widget build(BuildContext context) {
  print('build executed');
  
  return Scaffold(
    appBar: AppBar(title: Text('Page')),
    body: Center(
      child: Text('Counter: $counter'),
    ),
  );
}

When called:

  • After didChangeDependencies()
  • Every time setState() is called
  • After didUpdateWidget()
  • When parent widget rebuilds

What it's for: Creating UI.

Important:

  • Don't make API calls here!
  • Don't start timers here!
  • Can be called frequently, watch performance.

5. didUpdateWidget()

Called when parent widget changes and sends a new configuration.

class ParentWidget extends StatefulWidget {
  final String title;
  
  ParentWidget({required this.title});
  
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  @override
  void didUpdateWidget(ParentWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    
    print('didUpdateWidget executed');
    
    // Compare old and new widget
    if (oldWidget.title != widget.title) {
      print('Title changed: ${oldWidget.title}${widget.title}');
      // Make necessary updates
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Text(widget.title);
  }
}

When called: When parent widget changes and rebuilds.

What it's for:

  • Comparing old and new widgets
  • Updating based on changed parameters

6. setState()

Used to update state and rebuild the widget.

class _CounterState extends State<Counter> {
  int count = 0;
  
  void _increment() {
    setState(() {
      count++; // State change
    });
    // At this point, build() is called again
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          child: Text('Increment'),
          onPressed: _increment,
        ),
      ],
    );
  }
}

When called: You call it manually.

What it's for: Notifying Flutter of state changes.

Important:

  • Be careful with async operations
  • Avoid unnecessary setState calls
  • Don't call after widget is disposed

7. dispose()

Called before the widget is removed. Cleanup is done here.

class _MyWidgetState extends State<MyWidget> {
  late Timer timer;
  late StreamSubscription subscription;
  TextEditingController controller = TextEditingController();
  
  @override
  void initState() {
    super.initState();
    
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      // ...
    });
    
    subscription = someStream.listen((data) {
      // ...
    });
  }
  
  @override
  void dispose() {
    print('dispose executed - cleaning up');
    
    // Cancel timer
    timer.cancel();
    
    // Close stream
    subscription.cancel();
    
    // Dispose controller
    controller.dispose();
    
    super.dispose(); // Must call this last!
  }
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

When called: Before the widget is removed from the widget tree.

What it's for:

  • Stopping timers
  • Closing streams
  • Disposing controllers
  • Removing event listeners
  • Preventing memory leaks

Important: super.dispose() must be called last!

Practical Example: API Call with Life Cycle

class UserProfilePage extends StatefulWidget {
  final int userId;
  
  UserProfilePage({required this.userId});
  
  @override
  _UserProfilePageState createState() => _UserProfilePageState();
}

class _UserProfilePageState extends State<UserProfilePage> {
  bool isLoading = true;
  String? userName;
  String? errorMessage;
  
  @override
  void initState() {
    super.initState();
    print('1. initState - Making API call');
    _loadUserData();
  }
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies - Context ready');
  }
  
  @override
  void didUpdateWidget(UserProfilePage oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('3. didUpdateWidget - Did userId change?');
    
    // Reload if userId changed
    if (oldWidget.userId != widget.userId) {
      setState(() {
        isLoading = true;
        errorMessage = null;
      });
      _loadUserData();
    }
  }
  
  Future<void> _loadUserData() async {
    try {
      print('API call started: userId=${widget.userId}');
      
      // Simulated API call
      await Future.delayed(Duration(seconds: 2));
      
      // Check if widget is still mounted
      if (!mounted) return;
      
      setState(() {
        userName = 'User ${widget.userId}';
        isLoading = false;
      });
      
      print('API call completed');
    } catch (e) {
      if (!mounted) return;
      
      setState(() {
        errorMessage = 'Error: $e';
        isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    print('4. build - Creating UI');
    
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: Center(
        child: isLoading
            ? CircularProgressIndicator()
            : errorMessage != null
                ? Text(errorMessage!)
                : Text('Hello, $userName!'),
      ),
    );
  }
  
  @override
  void dispose() {
    print('5. dispose - Cleaning up widget');
    super.dispose();
  }
}

Application Life Cycle (App Life Cycle)

Different from widget life cycle, the application itself also has a life cycle.

AppLifecycleState

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }
  
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    
    switch (state) {
      case AppLifecycleState.resumed:
        print('App active - User in app');
        // Update data, start animations
        break;
        
      case AppLifecycleState.inactive:
        print('App inactive - Temporary state');
        // Phone call, notification pull down
        break;
        
      case AppLifecycleState.paused:
        print('App in background');
        // Save data, stop operations
        break;
        
      case AppLifecycleState.detached:
        print('App terminating');
        // Final cleanup
        break;
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: HomePage());
  }
}

State Descriptions

  • resumed: App is visible and user is interacting
  • inactive: App is visible but no interaction (phone call)
  • paused: App is in background, not visible
  • detached: App is terminating

Practical Use: Video Player

class VideoPlayerPage extends StatefulWidget {
  @override
  _VideoPlayerPageState createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage>
    with WidgetsBindingObserver {
  late VideoPlayerController videoController;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    
    videoController = VideoPlayerController.network('video_url');
    videoController.initialize();
    videoController.play();
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      // App moved to background - pause video
      videoController.pause();
    } else if (state == AppLifecycleState.resumed) {
      // App active - resume video
      videoController.play();
    }
  }
  
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    videoController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: AspectRatio(
          aspectRatio: videoController.value.aspectRatio,
          child: VideoPlayer(videoController),
        ),
      ),
    );
  }
}

deactivate() Method

Rarely used, called when widget is temporarily removed from the tree.

@override
void deactivate() {
  super.deactivate();
  print('Widget deactivated');
  // Temporary cleanup operations
}

When called:

  • During page changes with Navigator
  • When widget is repositioned

Common Mistakes

1. Using Context in initState

// ❌ Wrong
@override
void initState() {
  super.initState();
  final theme = Theme.of(context); // ERROR!
}

// ✅ Correct
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  final theme = Theme.of(context); // Correct
}

2. setState After dispose

// ❌ Wrong
Future<void> loadData() async {
  final data = await api.fetchData();
  setState(() {
    this.data = data; // Widget might be disposed!
  });
}

// ✅ Correct
Future<void> loadData() async {
  final data = await api.fetchData();
  if (mounted) { // mounted check
    setState(() {
      this.data = data;
    });
  }
}

3. Side Effects in build()

// ❌ Wrong
@override
Widget build(BuildContext context) {
  fetchData(); // Called on every build!
  return Text('...');
}

// ✅ Correct
@override
void initState() {
  super.initState();
  fetchData(); // Called once
}

4. Forgetting super Call

// ❌ Wrong
@override
void initState() {
  // No super.initState()!
  loadData();
}

// ✅ Correct
@override
void initState() {
  super.initState(); // Must call this!
  loadData();
}

Best Practices

  1. Always check mounted (for async operations)
  2. Clean up in dispose (timers, streams, controllers)
  3. Don't forget to call super methods
  4. Keep initState minimal (use Future.delayed for heavy operations)
  5. Keep build() pure (no side effects)

Summary

StatefulWidget Life Cycle:

  1. createState() - Create State
  2. initState() - Initial setup
  3. didChangeDependencies() - Dependency change
  4. build() - Create UI
  5. didUpdateWidget() - Parent change
  6. setState() - Update state
  7. dispose() - Cleanup

App Life Cycle:

  • resumed - Active
  • inactive - Temporarily inactive
  • paused - Background
  • detached - Terminating

Understanding life cycle methods is fundamental to writing performant and bug-free Flutter applications!


Confused about life cycle?

See you in the next article! 🚀