Dart Tutorial

Flutter Lesson 9 of 83 3 min read

The StatefulWidget Lifecycle in Flutter

Learn the lifecycle of a StatefulWidget: initState, didChangeDependencies, build, didUpdateWidget and dispose, with what belongs in each.

On this page

A State object goes through a fixed sequence of steps from creation to removal. Each step has a method you can override.

The methods, in order #

MethodCalledUse it to
initState()Once, when the state is createdSet initial values, create controllers, start loading data
didChangeDependencies()After initState, and whenever an inherited widget it depends on changesRead values that need context, such as the theme
build()Whenever the widget needs drawingDescribe the UI. Nothing else
didUpdateWidget(old)When the parent rebuilds with new parametersReact to a changed parameter
dispose()Once, when the state is removed for goodClean up: controllers, timers, subscriptions

A complete example #

import 'dart:async';
import 'package:flutter/material.dart';

class Clock extends StatefulWidget {
  const Clock({super.key});

  @override
  State<Clock> createState() => _ClockState();
}

class _ClockState extends State<Clock> {
  late Timer _timer;
  DateTime _now = DateTime.now();

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
      setState(() => _now = DateTime.now());
    });
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    String two(int n) => n.toString().padLeft(2, '0');
    return Text(
      '${two(_now.hour)}:${two(_now.minute)}:${two(_now.second)}',
      style: Theme.of(context).textTheme.displaySmall,
    );
  }
}

void main() {
  runApp(const MaterialApp(home: Scaffold(body: Center(child: Clock()))));
}

Two rules shown here:

  • Call super.initState() first in initState.
  • Call super.dispose() last in dispose.

Whatever you create, dispose #

These all hold resources and must be cleaned up in dispose:

  • TextEditingController, ScrollController, FocusNode
  • AnimationController
  • Timer
  • StreamSubscription

Forgetting causes memory leaks and “setState called after dispose” errors.

initState cannot use context for everything #

In initState the widget is not fully in the tree yet, so calls like Theme.of(context) and MediaQuery.of(context) are not allowed. Put them in didChangeDependencies or in build.

initState also cannot be async. To load data, call an async method from it.

@override
void initState() {
  super.initState();
  _load();
}

Future<void> _load() async {
  final user = await fetchUser();
  if (!mounted) return;
  setState(() => _user = user);
}

Reacting to changed parameters #

@override
void didUpdateWidget(covariant UserCard oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.userId != widget.userId) {
    _load(); // the parent now wants a different user
  }
}

The app’s own lifecycle #

To know when the whole app goes to the background or returns, use AppLifecycleListener.

late final AppLifecycleListener _listener;

@override
void initState() {
  super.initState();
  _listener = AppLifecycleListener(
    onResume: () => debugPrint('App is visible again'),
    onPause: () => debugPrint('App went to the background'),
  );
}

@override
void dispose() {
  _listener.dispose();
  super.dispose();
}

Typical uses: pause a video, save a draft, refresh data on return.

Try it yourself #

Write a Stopwatch widget with Start, Stop and Reset buttons that shows elapsed seconds. Create the timer when Start is pressed and make sure it is cancelled in dispose.

Practise in the playground Updated by Santosh Adhikari