build method
Runs init once, inside the build error boundary of ComponentElement.performRebuild, and only then builds the subtree.
This is the first point at which the element is connected to its
ancestors and the subtree has not been built yet: Element.mount has
assigned the parent and the inherited map, and buildChild has not run.
Implementation
@nonVirtual
@override
Widget build() {
if (_initPhase == _InitPhase.pending) {
assert(() {
_debugInitializingElement = this;
return true;
}());
try {
init();
} on Object catch (error, stackTrace) {
// Terminal, not retried: a hook that failed halfway may already hold
// something, and running it again would take a second copy of it
// while the first stays out of reach. The boundary above turns this
// into an `ErrorWidget`, and `unmount` still calls `dispose`.
_initPhase = _InitPhase.failed;
_initFailure = (error, stackTrace);
// The only report this failure gets. The boundary above raises it
// again as an `ErrorWidget`, but that is what the subtree shows, not
// what the observer hears: without this an observer watching scopes
// saw one that neither began nor ended, whichever family it belonged
// to. A family with a phase of its own reports its own failures and
// would double this -- except that its phase is started only for an
// `init()` that returned (`_didInit` in `performRebuild`), so a hook
// that threw leaves it never started and with nothing to say.
notifyObserver(
(observer) => observer.onError(
this,
ScopePhase.initialization,
error,
stackTrace,
),
);
rethrow;
} finally {
assert(() {
_debugInitializingElement = null;
return true;
}());
}
_initPhase = _InitPhase.done;
if (!reportsOwnLifecycle) {
notifyObserver((observer) => observer.onInit(this));
}
} else if (_initFailure case (final error, final stackTrace)) {
// There is no scope to build on. Raising the original failure again --
// with its own stack trace -- keeps the boundary showing what actually
// went wrong, rather than a second, derived error.
Error.throwWithStackTrace(error, stackTrace);
}
// A notify-only rebuild hands back what the last real build made.
// [ComponentElement.performRebuild] calls this method whatever else
// happens -- only `updateChild` below is ours to skip -- so without the
// cache `buildChild()` ran on every notification and everything it
// returned was thrown away unlooked at. For a scope notified once a frame
// that is the whole widget graph of its subtree, built and dropped, once
// a frame.
//
// The empty cache is not a case any more: `performRebuild` will not call a
// rebuild notify-only without one. The pattern stays because it is how a
// nullable field is read without an assertion, and because reading the
// same fact twice costs nothing -- not because there is a path here where
// the flag is set and the cache is empty.
if (_rebuildIsNotifyOnly) {
if (_builtChild case final builtChild?) {
return builtChild;
}
}
try {
return _builtChild = buildChild();
} on Object catch (error, stackTrace) {
// The one point every build of every family passes through: the five
// `buildOn*` of a state-carrying family, `ScopeWidgetBase.build` and
// `ScopeModel.build` are all reached from here and nowhere else.
//
// Reported *and* re-thrown, unlike a teardown with no caller left to
// hear it. A build has one, and a load-bearing one: the error boundary
// of [ComponentElement.performRebuild] above, which puts an
// `ErrorWidget` in the subtree's place. That is what the subtree shows;
// this is what the observer hears -- the same split that put a report
// on the `init()` hook higher up this method.
notifyObserver(
(observer) =>
observer.onError(this, ScopePhase.build, error, stackTrace),
);
rethrow;
}
}