İçeriğe geç / Skip to content / Zum Inhalt

Flutter Life Cycle: StatefulWidget and AppLifecycleState

Ahmet Balaman

Last updated:

5 min read

FlutterLife CycleStatefulWidgetState ManagementApp Lifecycle
Flutter Life Cycle: StatefulWidget and AppLifecycleState

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. If the difference between the two, or how the widget tree is built, is still unclear, the widget tree and layout post is a good place to start.

Live Demo: Life Cycle Methods

Try StatefulWidget life cycle methods interactively:

💡 If the example above doesn't load, click DartPad to run it in a new tab.

StatefulWidget Life Cycle

A StatefulWidget's life progresses like this:

StatefulWidget life cycle: creation with createState, initState, didChangeDependencies and build; update with setState and build; teardown with deactivate and dispose

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

The pattern below is a hand-written loading flow: fetch the data in initState and push it to the screen with setState. If you want the same result with less code, FutureBuilder handles the loading, error and success states for you; for data that keeps arriving, StreamBuilder is the better fit.

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();
  }
}

One thing to keep in mind: this state disappears together with dispose as soon as the page is popped. For data that several screens share and that has to survive navigation, reach for something like Provider or Riverpod instead of widget state.

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 after an async operation.
  2. Clean up timers, stream subscriptions and controllers in dispose.
  3. Don't forget to call the super methods.
  4. Keep initState minimal and defer heavy work.
  5. Keep build() free of 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 the life cycle methods is fundamental to writing performant, bug-free Flutter applications.


Confused about life cycle?

See you in the next article! 🚀

Frequently Asked Questions

Should I make my API call in initState?

Yes — for a one-off load, initState is the right place. Never put it in build(), which runs on every repaint. Instead of using await directly in initState, call an async method and write the result with setState.

What exactly should I clean up in dispose?

Everything you created yourself: TextEditingController, AnimationController, ScrollController, any Timer and every StreamSubscription. Left alone, they keep running after the widget leaves the tree and leak memory.

How do I avoid "setState() called after dispose()"?

That error means an async operation finished after the widget was already gone. Guard the call with if (!mounted) return; before setState, and where possible cancel the operation or subscription in dispose.

Comments