pumpAndSettle method
Pumps in step increments until a frame does no build work (the tree
has settled) or timeout elapses. Throws when timeout is hit while
the tree still rebuilds every step.
Mirrors WidgetTester.pumpAndSettle, and like it keys off "is another
frame's worth of work pending" (here: did flushBuild rebuild anything)
rather than "are any tickers registered" — so a focused TextInput's
perpetual cursor-blink ticker no longer hangs settle; it returns in the
gaps between blinks. A genuinely continuous animation (one that
rebuilds on every step) is what trips the timeout — pump a bounded
duration for those instead of settling.
NOTE: synchronous, so it cannot observe async work (stream/Future
emissions). For data-bound widgets (StreamBuilder/QueryBuilder) use
the async settle.
Implementation
void pumpAndSettle({
Duration step = const Duration(milliseconds: 16),
Duration timeout = const Duration(seconds: 10),
}) {
_assertNotDisposed('pumpAndSettle');
assert(
step > Duration.zero,
'pumpAndSettle needs a positive step; a zero step never advances the '
'clock, so timeout is never reached.',
);
var elapsed = Duration.zero;
var hasRendered = false;
while (elapsed < timeout) {
final (quiescent, rendered) = _settleStep(step, hasRendered: hasRendered);
hasRendered = hasRendered || rendered;
if (quiescent) {
_closeFrame();
return;
}
elapsed += step;
}
throw StateError(
'pumpAndSettle timed out after $timeout: the tree never reached a '
'quiescent frame — something rebuilds on every step. '
'${_scheduler.activeTickerCount} ticker(s) are active; if a continuous '
'animation is intentional, pump a bounded duration instead of settling. '
'(A perpetual-but-idle ticker such as a cursor blink does NOT cause '
'this — settle returns in the gaps between its frames.)',
);
}