---
title: 'State Management Approaches'
source: 'https://academia.sh/en/courses/flutter/state-management-approaches'
course: Flutter
language: en
updated: '2026-10-21T05:02:52+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Tells the provider, event-based, and reactive state management approaches apart'
    - 'Measures how a shared counter changes the request of a badge'
    - 'Shows that the badge stays chosen under the unbounded constraint a row gives to its non-flexible child'
    - 'Explains why the model does not show overflow when the children add up to more than the row'
---

# State Management Approaches

Shared state changes what a widget requests, not who determines its width. The lesson measures the width of a badge fed by a counter and shows that the badge stays chosen under the row's unbounded constraint.

The previous lesson measured that widget families that vary by platform ask for different widths on an action button, yet produce the same result in a fixed slot. This lesson moves the source of the request to the state of the app. A counter, a session record, or a cart total is a value that several widgets read together, and such a value can change a widget's request. The course question stays the same: **who determines this widget's width?** The lesson looks for the answer on a badge fed by a shared counter.

## Three Ways to Share State

When the state of an app does not fit inside a single widget, the state is kept in a shared place near the top of the tree and the widgets below read it. This sharing comes in three forms. Library names are not written in this lesson; each one is referred to by its function.

In the **provider** approach, the value sits at a node high in the tree. The widgets below read the value from the context, and when the value changes, only the widgets that read it are rebuilt. Shared state libraries use this pattern.

In the **event-based** approach, a widget does not change the state directly; it sends an event. A handler receives the event and produces the new state. Event-based state libraries gather the path of change into a single point in this way.

In the **reactive** approach, the state is an observable value. Listeners subscribe to the value and are notified on every change; the value can be a stream or a notifier object.

All three approaches arrive at the same place: the state changes, the `build` method of the affected widget produces a new widget subtree from the new state, and the framework runs layout again.

## The Core

Every lesson uses the same core. `measure` walks the tree with a tight constraint at the screen width, produces a record for each node, and gives the sizing in the record by a rule: if the constraint is tight, **forced**; if the request stays inside the range, **chosen**; if the request falls outside the range, or there is no request and there is no upper bound, **clamped**; if there is no request and the upper bound is finite, **stretched**. The four names are written in ASCII in the output: `forced`, `chosen`, `clamped`, `stretched`. The `parent:` line is the total of forced, clamped, and stretched; it counts the records whose width is not the widget's own request.

```dart
// sizing.dart — classifies who decides a widget's width
const sizings = ['forced', 'chosen', 'clamped', 'stretched'];

class Limit {
  final int min;
  final int? max; // null: unbounded
  const Limit(this.min, this.max);

  bool get tight => max == min;
  Limit loosen() => Limit(0, max);
  Limit deflate(int by) => Limit(_floor(min - by), max == null ? null : _floor(max! - by));
  String bounds() => '$min..${max ?? 'inf'}';
}

int _floor(int value) => value < 0 ? 0 : value;

class Part {
  final String kind;
  final String name;
  final int? width; // null: as wide as it can get
  final List<Part> children;
  final int factor;
  final bool tightFlex;
  const Part(this.kind, this.name, this.width, this.children,
      {this.factor = 0, this.tightFlex = true});
}

Part leaf(String name, int width) => Part('leaf', name, width, const []);
Part fill(String name) => Part('leaf', name, null, const []);
Part pad(String name, int padding, Part child) => Part('pad', name, padding, [child]);
Part center(String name, Part child) => Part('center', name, null, [child]);
Part fixed(String name, int width, Part child) => Part('fixed', name, width, [child]);
Part column(String name, List<Part> children) => Part('column', name, null, children);
Part row(String name, List<Part> children) => Part('row', name, null, children);
Part flex(String name, int factor, Part child, {bool tight = true}) =>
    Part('flex', name, null, [child], factor: factor, tightFlex: tight);

class Sizing {
  final String sizing;
  final String path;
  final int width;
  final Limit limit;
  const Sizing(this.sizing, this.path, this.width, this.limit);
}

Sizing classify(String path, int? desire, Limit limit) {
  if (limit.tight) return Sizing('forced', path, limit.min, limit);
  if (desire == null) {
    return limit.max == null
        ? Sizing('clamped', path, limit.min, limit)
        : Sizing('stretched', path, limit.max!, limit);
  }
  if (desire < limit.min) return Sizing('clamped', path, limit.min, limit);
  if (limit.max != null && desire > limit.max!) {
    return Sizing('clamped', path, limit.max!, limit);
  }
  return Sizing('chosen', path, desire, limit);
}

int layout(Part part, Limit limit, List<Sizing> log, [String path = '']) {
  final here = '$path/${part.name}';
  final below = <Sizing>[];
  if (part.kind == 'fixed') {
    final own = classify(here, part.width, limit);
    layout(part.children.first, Limit(own.width, own.width), below, here);
    log..add(own)..addAll(below);
    return own.width;
  }
  int? desire;
  switch (part.kind) {
    case 'leaf':
      desire = part.width;
    case 'pad':
      final space = 2 * part.width!;
      desire = layout(part.children.first, limit.deflate(space), below, here) + space;
    case 'center':
      final inner = layout(part.children.first, limit.loosen(), below, here);
      desire = limit.max == null ? inner : null;
    case 'flex':
      desire = layout(part.children.first, limit, below, here);
    case 'column':
      var widest = 0;
      for (final child in part.children) {
        final width = layout(child, limit.loosen(), below, here);
        if (width > widest) widest = width;
      }
      desire = widest;
    case 'row':
      final logs = [for (final _ in part.children) <Sizing>[]];
      final bounded = limit.max != null;
      var used = 0;
      var factors = 0;
      for (var i = 0; i < part.children.length; i++) {
        final child = part.children[i];
        if (child.kind == 'flex' && bounded) {
          factors += child.factor;
        } else {
          used += layout(child, const Limit(0, null), logs[i], here);
        }
      }
      final left = bounded ? _floor(limit.max! - used) : 0;
      var given = 0;
      var seen = 0;
      for (var i = 0; i < part.children.length; i++) {
        final child = part.children[i];
        if (child.kind != 'flex' || !bounded) continue;
        seen += child.factor;
        final share = (seen == factors ? left : left * seen ~/ factors) - given;
        given += share;
        final room = child.tightFlex ? Limit(share, share) : Limit(0, share);
        used += layout(child, room, logs[i], here);
      }
      for (final entries in logs) {
        below.addAll(entries);
      }
      desire = bounded ? null : used;
  }
  final own = classify(here, desire, limit);
  log..add(own)..addAll(below);
  return own.width;
}

List<Sizing> measure(Part root, int screen) {
  final log = <Sizing>[];
  layout(root, Limit(screen, screen), log);
  return log;
}

void printSizes(List<Sizing> log) {
  for (final entry in log) {
    print('${entry.sizing}: ${entry.path} width=${entry.width} constraint=${entry.limit.bounds()}');
  }
}

void countSizings(List<Sizing> log) {
  for (final name in sizings) {
    print('$name: ${log.where((entry) => entry.sizing == name).length}');
  }
  print('parent: ${log.where((entry) => entry.sizing != 'chosen').length}');
}
```

## The Framework Side

The snippet below shows the framework side of the provider pattern. The snippet is not measured; the model does not run the framework's code.

```dart
// Platform code: provider widget
class CounterScope extends InheritedNotifier<ValueNotifier<int>> {
  const CounterScope({super.key, required ValueNotifier<int> counter, required super.child})
      : super(notifier: counter);

  static int of(BuildContext context) {
    final scope = context.dependOnInheritedWidgetOfExactType<CounterScope>();
    return scope!.notifier!.value;
  }
}
```

In the snippet, the counter value sits in a notifier object at the top of the tree. A widget that calls `of` becomes dependent on that value; when the value changes, the framework rebuilds only these dependent widgets. Whichever approach is chosen, the input of the layout phase does not change: the constraint comes from above, and the widget's request is now computed from the state.

## From the Counter to the Badge

The state of the model is a shared counter. A helper file holds the counter: it stores the value, calls the listeners, and prints nothing to the output. This is the smallest form of the reactive pattern.

```dart
// store.dart — shared counter store; notifies listeners on every change
class CounterStore {
  int count;
  final List<void Function(int)> _listeners = [];
  CounterStore(this.count);

  void listen(void Function(int) listener) => _listeners.add(listener);

  void update(int value) {
    count = value;
    for (final listener in _listeners) {
      listener(count);
    }
  }
}
```

The request of the badge depends on the number of digits in the counter: each digit adds 12 units, and the badge starts with a fixed gap of 18 units. `buildHeader` produces the tree from the store: a column inside the screen, and inside the column a row that carries the title and the badge. Widths are units of the model.

```dart
// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(9);
  final log = measure(buildHeader(store), 360);
  printSizes(log);
  countSizings(log);
}
```

```text
forced: /Screen width=360 constraint=360..360
forced: /Screen/Body width=328 constraint=328..328
stretched: /Screen/Body/Header width=328 constraint=0..328
chosen: /Screen/Body/Header/Title width=120 constraint=0..inf
chosen: /Screen/Body/Header/Badge width=30 constraint=0..inf
forced: 2
chosen: 2
clamped: 0
stretched: 1
parent: 3
```

When the counter is 9, the badge has one digit and asks for 30 units. The row receives a bounded constraint, so it does not report a request; but it gives its two non-flexible children an unbounded constraint. `Title` asks for 120 and `Badge` asks for 30, and since both stay inside the unbounded range, both are **chosen**. The title and the badge determine the width themselves; the parent is counted only in the screen, column, and row records.

## When the Counter Changes

Now add a listener that subscribes to the counter. On every change, the listener rebuilds the tree, measures it again, and prints only the record of the badge. As the counter moves from 9 to 1000, 10 and 99 will give the same width, while 100 and 1000 move to different digit counts.

```dart
// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(9);
  store.listen((value) {
    final log = measure(buildHeader(store), 360);
    final badge = log.firstWhere((entry) => entry.path.endsWith('/Badge'));
    print('count=$value ${badge.sizing} width=${badge.width} constraint=${badge.limit.bounds()}');
  });
  for (final value in [9, 10, 99, 100, 1000]) {
    store.update(value);
  }
}
```

```text
count=9 chosen width=30 constraint=0..inf
count=10 chosen width=42 constraint=0..inf
count=99 chosen width=42 constraint=0..inf
count=100 chosen width=54 constraint=0..inf
count=1000 chosen width=66 constraint=0..inf
```

The badge is 30 units wide at 9, 42 up to 99, 54 at 100, and 66 at 1000. In every line the sizing stays the same: `chosen`. The width grew because the badge's request grew; but the badge still determines its width. Since the constraint is `0..inf`, no value can push the request outside the range. The rule does not see the state itself, only the part of the state that shows up in the request: 10 and 99 are different numbers, but because they give the same digit count, they produce the same record.

The result is the same with an event-based library and with a reactive library. The approaches change how the state is carried; what enters the layout phase is still the constraint and the request.

## The Bounding Measurement

Because the row gives its non-flexible children an unbounded constraint, nothing stops the badge from growing. The total of the children can exceed the row's own range. To see this, narrow the screen to 200 units and ask for 160 units for the title. The counter is at 1000, and the badge is 66 units. The model does not show overflow: the language of the record is sizing, and each record looks only at its own constraint. The lesson computes the total of the children on a separate line and prints it.

```dart
// main.dart
import 'sizing.dart';
import 'store.dart';

Part buildHeader(CounterStore store, {int title = 120}) {
  final digits = '${store.count}'.length;
  return pad('Screen', 16, column('Body', [
    row('Header', [
      leaf('Title', title),
      leaf('Badge', 18 + 12 * digits),
    ]),
  ]));
}

void main() {
  final store = CounterStore(1000);
  final log = measure(buildHeader(store, title: 160), 200);
  printSizes(log);
  final header = log.firstWhere((entry) => entry.path == '/Screen/Body/Header');
  var total = 0;
  for (final entry in log.where((entry) => entry.path.startsWith('/Screen/Body/Header/'))) {
    total += entry.width;
  }
  print('children total: $total row: ${header.width}');
}
```

```text
forced: /Screen width=200 constraint=200..200
forced: /Screen/Body width=168 constraint=168..168
stretched: /Screen/Body/Header width=168 constraint=0..168
chosen: /Screen/Body/Header/Title width=160 constraint=0..inf
chosen: /Screen/Body/Header/Badge width=66 constraint=0..inf
children total: 226 row: 168
```

The focus node is `/Screen/Body/Header/Badge`. Its record is `chosen`, its width is 66, and its constraint is `0..inf`: the screen has narrowed, the title has grown, and the row has shrunk to 168 units, but the badge still chose its own request. The last line belongs to the sum, not to the measurement: the two children together ask for 226 units, while the row's range is 168 units. The overflow is here; in the real framework, because the children of a row are measured with an unbounded constraint, this total turns into an overflow warning. The rule does not see this: it records neither the warning of the overflow, nor its stripe on the screen, nor which child will be cut.

**AS1.** The model measures only the width. The height of the badge, the wrapping of the text onto a new line, and how many times a state change causes a rebuild are not in the model. The model builds this sizing's rule; the model is not the framework itself. Computing the width from the digit count is an assumption of the lesson; a real badge takes its width from the measurements of the font.

The way to prevent this kind of overflow is not in state management but in layout: the badge should be wrapped in a flexible child, or the title should be placed in an area that can be cut. State approaches change the request; whether the request fits is decided by the constraint.

## Summary

- This lesson's sizing: chosen. The focus node is `/Screen/Body/Header/Badge`: the badge, which asks for 66 units under the row's unbounded constraint, chose its own request.
- The provider, event-based, and reactive approaches change how state is shared; the constraint and the request that enter the layout phase stay the same.
- As the counter went from 9 to 1000, the badge's width grew from 30 to 66, and the sizing stayed `chosen` at every step; numbers that give the same digit count gave the same record.
- The row gives its non-flexible children an unbounded constraint; the total of the children (226) can exceed the row's range (168), and the model does not show this.
- The rule does not see a value that does not enter the request: differences in the state that do not show up in the width do not change the record.

## Next Step

In this lesson, a value that comes from the state changed the width, but the constraint was always unbounded or loose. The next lesson, `Navigation and Routing`, asks who gives a page its width: whichever route is opened, it measures what the navigator imposes on the page.
