Basic State Management
Irbisa · cheatsheetSeptember 13, 2026

Basic State Management

setState, InheritedWidget, ValueNotifier

Junior Developer20 itemscompressed for a skim
  1. 01

    How does setState work and what are its performance implications?

    Easy

    setState() marks the State as dirty and schedules rebuild of build() for the next frame.

    class CartPage extends StatefulWidget {
      const CartPage({super.key});
      @override State<CartPage> createState() => _CartPageState();
    }
    
    class _CartPageState extends State<CartPage> {
    …
  2. 02

    What is InheritedWidget and how does it work?

    Medium

    InheritedWidget is the fundamental mechanism for efficient state propagation down the widget tree.

    // Define InheritedWidget
    class AppTheme extends InheritedWidget {
      final Color primary;
      final double fontSize;
    
      const AppTheme({
    …
  3. 03

    When should you use StatelessWidget vs StatefulWidget?

    Easy

    Default to StatelessWidget.

    // ✅ Stateless — pure props in
    class PriceTag extends StatelessWidget {
      final int cents;
      const PriceTag(this.cents, {super.key});
      @override Widget build(BuildContext context) =>
          Text('\$${(cents / 100).toStringAsFixed(2)}');
    …
  4. 04

    What is ValueNotifier and when should you prefer it over setState?

    Medium

    ValueNotifier<T> is a tiny ChangeNotifier with a single value.

    class FavoriteButton extends StatefulWidget {
      const FavoriteButton({super.key});
      @override State<FavoriteButton> createState() => _FavoriteButtonState();
    }
    
    class _FavoriteButtonState extends State<FavoriteButton> {
    …
  5. 05

    What does it mean to "lift state up" in Flutter?

    Easy

    Lifting state up means moving a piece of state to the lowest common ancestor of the widgets that read or write it, then passing it down via constructor parameters and passing change callbacks back up.

    // ❌ Before — each tile owns its selection, parent cannot know which is selected
    class _Tile extends StatefulWidget {
      final String label;
      const _Tile(this.label);
      @override State<_Tile> createState() => _TileState();
    }
    …
  6. 06

    What is the mounted check and why is it required after an async gap?

    Medium

    Across an await, the widget owning the State may have been disposed.

    class ProfilePage extends StatefulWidget {
      const ProfilePage({super.key});
      @override State<ProfilePage> createState() => _ProfilePageState();
    }
    
    class _ProfilePageState extends State<ProfilePage> {
    …
  7. 07

    What is the difference between initState and didChangeDependencies?

    Medium

    Both run before the first build, but they answer different questions.

    class _PostFeedState extends State<PostFeed>
        with SingleTickerProviderStateMixin {         // supplies the vsync below
      late final AnimationController _shimmer;       // controller owns lifetime
      StreamSubscription? _sub;
      List<Post> _posts = const [];
    …
  8. 08

    What is InheritedNotifier and when does it beat a plain InheritedWidget?

    Medium

    InheritedNotifier<T extends Listenable> is a built-in InheritedWidget that wraps a Listenable (typically ChangeNotifier or ValueNotifier).

    // 1) The notifier holds the state
    class CounterModel extends ChangeNotifier {
      int _value = 0;
      int get value => _value;
      void inc() { _value++; notifyListeners(); }
    }
    …
  9. 09

    What problem do widget Keys solve, and when do you need GlobalKey vs ValueKey?

    Hard

    Keys preserve State across rebuilds when widgets get reordered or swapped.

    // ❌ Without keys — toggling Sort reorders tiles, State (e.g. selection) follows the slot, not the item
    List<Widget> tilesNoKeys(List<Person> people) =>
        [for (final p in people) PersonTile(p)];
    
    // ✅ ValueKey by id — State sticks to the right item across reorder
    List<Widget> tilesKeyed(List<Person> people) =>
    …
  10. 10

    What causes the "setState() or markNeedsBuild() called during build" error, and how do you fix it?

    Medium

    Flutter is in the middle of walking the tree while build runs, so marking anything dirty inside that window would leave the frame half built.

    // ❌ setState() or markNeedsBuild() called during build
    @override
    Widget build(BuildContext context) {
      if (widget.items.isEmpty) {
        setState(() => _showEmpty = true);       // dirty during build
      }
    …
  11. 11

    A checkbox inside a dialog never updates when you call setState. Why, and what is the fix?

    Medium

    The dialog lives on its own route with its own context, so the page's setState rebuilds the page and never touches the dialog's content.

    // ❌ the outer setState rebuilds the page, not the dialog
    showDialog(
      context: context,
      builder: (_) => AlertDialog(
        content: Checkbox(
          value: _agreed,
    …
  12. 12

    What has to be released in a State's dispose(), and what goes wrong when you forget?

    Easy

    Anything the State created that outlives a single build has to be released there, because it holds a reference back to the State.

    class _EditorState extends State<Editor> with SingleTickerProviderStateMixin {
      final _text = TextEditingController();
      final _scroll = ScrollController();
      final _focus = FocusNode();
      late final _anim = AnimationController(
        vsync: this,
    …
  13. 13

    Why does the callback you hand to setState have to be synchronous, and what happens if you make it async?

    Easy

    setState runs the callback immediately and synchronously and then marks the element dirty, so an async callback returns a Future the framework throws away and your fields are assigned long after the rebuild they were supposed to cause.

    // ❌ debug: "setState() callback argument returned a Future"
    void _load() {
      setState(() async {                     // Future<void> Function() slips
        _user = await api.fetchUser();        // into a VoidCallback slot
      });                                     // dirty now, assigned much later
    }
    …
  14. 14

    A user double-taps Save, two orders get created — how do you stop the second tap with local state?

    Easy

    One in-flight boolean on the State, set before the await and cleared in a finally, and the same field drives onPressed: null so the button is visibly dead while the work runs.

    class _SaveButtonState extends State<SaveButton> {
      bool _saving = false;
    
      Future<void> _save() async {
        if (_saving) return;               // the second tap lands here
        setState(() => _saving = true);    // same synchronous slice — no race
    …
  15. 15

    A parent passes a new title down, but the child still shows the old one — why doesn't initState run again?

    Medium

    The State object is reused across parent rebuilds — Flutter swaps only the widget field on the same State — so anything you copied out of widget in initState is a snapshot that nothing ever refreshes.

    // ❌ snapshots taken once — the parent's new values never arrive
    class _EditorState extends State<Editor> {
      late String _title = widget.title;                         // stale forever
      late final _text = TextEditingController(text: widget.body);
    }
    …
  16. 16

    Your custom InheritedWidget holds the new value but nothing under it rebuilds — where do you look first?

    Medium

    Dependents rebuild only when the InheritedElement is updated with a new widget instance whose updateShouldNotify returns true, and only for contexts that actually registered a dependency — so one of those three links is broken.

    class CartScope extends InheritedWidget {
      const CartScope({super.key, required this.items, required super.child});
      final List<Item> items;
    
      // ✅ registers this context as a dependent
      static CartScope of(BuildContext context) =>
    …
  17. 17

    A form rebuilds the whole page on every keystroke because onChanged calls setState — where should that text actually live?

    Medium

    In the TextEditingController: it is a ValueNotifier<TextEditingValue> that the TextField already listens to, so the field repaints itself on every character and nothing above it has to rebuild at all.

    // ❌ every keystroke rebuilds the whole page, and _text duplicates the truth
    TextField(onChanged: (v) => setState(() => _text = v));
    
    // ✅ the controller is the source of truth; only the button listens to it
    class _ComposerState extends State<Composer> {
      final _controller = TextEditingController();
    …
  18. 18

    An expensive chart inside a ValueListenableBuilder rebuilds on every tick — what is the builder's child parameter for?

    Medium

    child is built once, outside the builder, and handed back to it unchanged — and because the widget instance is identical between rebuilds, the element short-circuits and that subtree is never rebuilt.

    // ❌ a fresh ExpensiveChart instance on every tick → the subtree rebuilds
    ValueListenableBuilder<int>(
      valueListenable: _ticks,
      builder: (context, ticks, _) => Column(children: [
        Text('$ticks'),
        ExpensiveChart(series: widget.series),   // rebuilt 60x a second for nothing
    …
  19. 19

    Everything under your InheritedWidget rebuilds when any single field changes — how do you make a widget depend on one field only?

    Hard

    Use InheritedModel<T>: dependents declare an aspect when they read, and updateShouldNotifyDependent is asked per dependent whether the aspects that particular widget cares about actually changed.

    enum PrefAspect { currency, locale }
    
    class Prefs extends InheritedModel<PrefAspect> {
      const Prefs({
        super.key,
        required this.currency,
    …
  20. 20

    Android kills your backgrounded app and the user comes back to an empty form on the first screen — what does Flutter offer for that?

    Hard

    RestorationMixin with restorable properties: Flutter serializes a bucket of UI state that the engine hands back after an OS-initiated kill, so the user returns to the same route, scroll offset and half-typed form.

    void main() => runApp(const MaterialApp(
          restorationScopeId: 'app',      // without a scope, nothing below restores
          home: SurveyPage(),
        ));
    
    class _SurveyPageState extends State<SurveyPage> with RestorationMixin {
    …