settle method

Future<void> settle({
  1. Duration step = const Duration(milliseconds: 16),
  2. Duration timeout = const Duration(seconds: 5),
  3. int stableSteps = 4,
})

The async analog of pumpAndSettle for data-bound widgets: yields to the real event loop between steps so pending microtasks, timers, and stream emissions land (a StreamBuilder/QueryBuilder's first value arrives on a later event-loop turn), then flushes builds — repeating until a step does no build work or timeout is hit.

This replaces the hand-rolled for (i in 0..N) { await Future.delayed(d); pump(); } loop every async test would otherwise need: synchronous pump can't observe async emissions because it never turns the event loop.

Returns once the tree has been quiescent for stableSteps consecutive steps. Requiring a short run of idle steps (rather than returning on the first) guards the cold-start case: in-flight async work (a real DB query, an HTTP call) that hasn't produced its first rebuild yet gets a window to land and interrupt the idle streak — so settle() doesn't conclude "done" before the data arrives. Tune up for slower backends.

Throws a StateError on timeout (the tree never settled) — the same signal as Flutter's pumpAndSettle timeout.

Implementation

Future<void> settle({
  Duration step = const Duration(milliseconds: 16),
  Duration timeout = const Duration(seconds: 5),
  int stableSteps = 4,
}) async {
  _assertNotDisposed('settle');
  assert(
    step > Duration.zero,
    'settle needs a positive step; a zero step never advances the clock '
    '(nor elapsed), so timeout is never reached and the loop spins.',
  );
  var elapsed = Duration.zero;
  var steps = 0;
  var stable = 0;
  var hasRendered = false;
  while (elapsed < timeout) {
    // Turn the real event loop so async completions (stream/Future) run
    // their setState before we flush + test for quiescence.
    await Future<void>.delayed(step);
    final (quiescent, rendered) = _settleStep(step, hasRendered: hasRendered);
    hasRendered = hasRendered || rendered;
    stable = quiescent ? stable + 1 : 0;
    if (stable >= stableSteps) {
      _closeFrame();
      return;
    }
    elapsed += step;
    steps++;
  }
  throw StateError(
    'settle() timed out after $timeout ($steps steps): the tree never '
    'reached $stableSteps consecutive quiescent frames. A stream that never '
    'stops emitting, or a continuous animation, will not settle — assert on '
    'a bounded pump instead. ${_scheduler.activeTickerCount} ticker(s) are '
    'active.',
  );
}