Flutter Best Practices

Chapter 2 · Practice 5 of 5

Dispose every controller, subscription and focus node you create

From Widgets and build methods

Why it matters

Controllers such as TextEditingController, AnimationController, ScrollController and FocusNode, plus StreamSubscriptions, hold resources and listeners. If a State creates one, that State must dispose of it in dispose().

Forgetting this leaks memory and keeps tickers or streams running after the screen is gone. A common symptom is "setState() called after dispose()" errors, which usually means a stream or future callback outlived its widget.

For async work started by a widget, check mounted before using context or calling setState() after an await.

Do

  • Pair every controller created in initState() with a call in dispose()
  • Check mounted after an await before touching context

Avoid

  • Creating controllers inside build()

Example

Dart
Future<void> _save() async {
  await repository.save(_form);
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('Saved')),
  );
}