Make sure LayoutBuilder rebuild in an inactive route (#154681)
Prompted by https://github.com/flutter/flutter/issues/154060: widgets should always rebuild even when off-screen. The ancestor widget could be trying to pass down information that is not related to the UI state, or trying to pause video playback. Widgets with global keys should also always rebuild to make sure the widget tree is consistent in terms of global keys. ~Also prevents unnecessary repaints: https://github.com/flutter/flutter/issues/106306#issuecomment-1266432242~ This works by adding `_RenderLayoutBuilder` as a relayout boundary in the dirtly layout list so the layout callback always gets a chance to run if marked dirty. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
@@ -2608,7 +2608,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
|
||||
@pragma('vm:notify-debugger-on-exception')
|
||||
void _layoutWithoutResize() {
|
||||
assert(_needsLayout);
|
||||
assert(_relayoutBoundary == this);
|
||||
assert(_relayoutBoundary == this || this is RenderObjectWithLayoutCallbackMixin);
|
||||
RenderObject? debugPreviousActiveLayout;
|
||||
assert(!_debugMutationsLocked);
|
||||
assert(!_doingThisLayoutWithCallback);
|
||||
@@ -4132,6 +4132,77 @@ mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject
|
||||
}
|
||||
}
|
||||
|
||||
/// A mixin for managing [RenderObject] with a [layoutCallback], which will be
|
||||
/// invoked during this [RenderObject]'s layout process if scheduled using
|
||||
/// [scheduleLayoutCallback].
|
||||
///
|
||||
/// A layout callback is typically a callback that mutates the [RenderObject]'s
|
||||
/// render subtree during the [RenderObject]'s layout process. When an ancestor
|
||||
/// [RenderObject] chooses to skip laying out this [RenderObject] in its
|
||||
/// [performLayout] implementation (for example, for performance reasons, an
|
||||
/// [Overlay] may skip laying out an offstage [OverlayEntry] while keeping it in
|
||||
/// the tree), normally the [layoutCallback] will not be invoked because the
|
||||
/// [layout] method will not be called. This can be undesirable when the
|
||||
/// [layoutCallback] involves rebuilding dirty widgets (most notably, the
|
||||
/// [LayoutBuilder] widget). Unlike render subtrees, typically all dirty widgets
|
||||
/// (even off-screen ones) in a widget tree must be rebuilt. This mixin makes
|
||||
/// sure once scheduled, the [layoutCallback] method will be invoked even if it's
|
||||
/// skipped by an ancestor [RenderObject], unless this [RenderObject] has never
|
||||
/// been laid out.
|
||||
///
|
||||
/// Subclasses must not invoke the layout callback directly. Instead, call
|
||||
/// [runLayoutCallback] in the [performLayout] implementation.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [LayoutBuilder] and [SliverLayoutBuilder], which use the mixin.
|
||||
mixin RenderObjectWithLayoutCallbackMixin on RenderObject {
|
||||
// The initial value of this flag must be set to true to prevent the layout
|
||||
// callback from being scheduled when the subtree has never been laid out (in
|
||||
// which case the `constraints` or any other layout information is unknown).
|
||||
bool _needsRebuild = true;
|
||||
|
||||
/// The layout callback to be invoked during [performLayout].
|
||||
///
|
||||
/// This method should not be invoked directly. Instead, call
|
||||
/// [runLayoutCallback] in the [performLayout] implementation. This callback
|
||||
/// will be invoked using [invokeLayoutCallback].
|
||||
@visibleForOverriding
|
||||
void layoutCallback();
|
||||
|
||||
/// Invokes [layoutCallback] with [invokeLayoutCallback].
|
||||
///
|
||||
/// This method must be called in [performLayout], typically as early as
|
||||
/// possible before any layout work is done, to avoid re-dirtying any child
|
||||
/// [RenderObject]s.
|
||||
@mustCallSuper
|
||||
void runLayoutCallback() {
|
||||
assert(debugDoingThisLayout);
|
||||
invokeLayoutCallback((_) => layoutCallback());
|
||||
_needsRebuild = false;
|
||||
}
|
||||
|
||||
/// Informs the framework that the layout callback has been updated and must be
|
||||
/// invoked again when this [RenderObject] is ready for layout, even when an
|
||||
/// ancestor [RenderObject] chooses to skip laying out this render subtree.
|
||||
@mustCallSuper
|
||||
void scheduleLayoutCallback() {
|
||||
if (_needsRebuild) {
|
||||
assert(debugNeedsLayout);
|
||||
return;
|
||||
}
|
||||
_needsRebuild = true;
|
||||
// This ensures that the layout callback will be run even if an ancestor
|
||||
// chooses to not lay out this subtree (for example, obstructed OverlayEntries
|
||||
// with `maintainState` set to true), to maintain the widget tree integrity
|
||||
// (making sure global keys are unique, for example).
|
||||
owner?._nodesNeedingLayout.add(this);
|
||||
// In an active tree, markNeedsLayout is needed to inform the layout boundary
|
||||
// that its child size may change.
|
||||
super.markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/// Parent data to support a doubly-linked list of children.
|
||||
///
|
||||
/// The children can be traversed using [nextSibling] or [previousSibling],
|
||||
|
||||
@@ -92,6 +92,10 @@ abstract class AbstractLayoutBuilder<LayoutInfoType> extends RenderObjectWidget
|
||||
///
|
||||
/// The [builder] function is _not_ called during layout if the parent passes
|
||||
/// the same constraints repeatedly.
|
||||
///
|
||||
/// In the event that an ancestor skips the layout of this subtree so the
|
||||
/// constraints become outdated, the `builder` rebuilds with the last known
|
||||
/// constraints.
|
||||
/// {@endtemplate}
|
||||
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints>
|
||||
extends AbstractLayoutBuilder<ConstraintType> {
|
||||
@@ -133,7 +137,7 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
|
||||
SchedulerPhase.persistentCallbacks => false,
|
||||
};
|
||||
if (!deferMarkNeedsLayout) {
|
||||
renderObject.markNeedsLayout();
|
||||
renderObject.scheduleLayoutCallback();
|
||||
return;
|
||||
}
|
||||
_deferredCallbackScheduled = true;
|
||||
@@ -145,7 +149,7 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
|
||||
// This method is only called when the render tree is stable, if the Element
|
||||
// is deactivated it will never be reincorporated back to the tree.
|
||||
if (mounted) {
|
||||
renderObject.markNeedsLayout();
|
||||
renderObject.scheduleLayoutCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +184,7 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
|
||||
renderObject._updateCallback(_rebuildWithConstraints);
|
||||
if (newWidget.updateShouldRebuild(oldWidget)) {
|
||||
_needsBuild = true;
|
||||
renderObject.markNeedsLayout();
|
||||
renderObject.scheduleLayoutCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +194,7 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
|
||||
// to performRebuild since this call already does what performRebuild does,
|
||||
// So the element is clean as soon as this method returns and does not have
|
||||
// to be added to the dirty list or marked as dirty.
|
||||
renderObject.markNeedsLayout();
|
||||
renderObject.scheduleLayoutCallback();
|
||||
_needsBuild = true;
|
||||
}
|
||||
|
||||
@@ -202,14 +206,14 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
|
||||
// Force the callback to be called, even if the layout constraints are the
|
||||
// same. This is because that callback may depend on the updated widget
|
||||
// configuration, or an inherited widget.
|
||||
renderObject.markNeedsLayout();
|
||||
renderObject.scheduleLayoutCallback();
|
||||
_needsBuild = true;
|
||||
super.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
|
||||
}
|
||||
|
||||
@override
|
||||
void unmount() {
|
||||
renderObject._updateCallback(null);
|
||||
renderObject._callback = null;
|
||||
super.unmount();
|
||||
}
|
||||
|
||||
@@ -295,39 +299,36 @@ class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
|
||||
/// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with
|
||||
/// the the same `LayoutInfoType`.
|
||||
///
|
||||
/// Provides a [rebuildIfNecessary] method that should be called at layout time,
|
||||
/// typically in [RenderObject.performLayout]. The method invokes
|
||||
/// [AbstractLayoutBuilder]'s builder callback if needed.
|
||||
/// Provides a [layoutCallback] implementation which, if needed, invokes
|
||||
/// [AbstractLayoutBuilder]'s builder callback.
|
||||
///
|
||||
/// Implementers must provide a [layoutInfo] implementation that is safe to
|
||||
/// access in [rebuildIfNecessary], which is typically called in [performLayout].
|
||||
/// access in [layoutCallback], which is called in [performLayout].
|
||||
mixin RenderAbstractLayoutBuilderMixin<LayoutInfoType, ChildType extends RenderObject>
|
||||
on RenderObjectWithChildMixin<ChildType> {
|
||||
on RenderObjectWithChildMixin<ChildType>, RenderObjectWithLayoutCallbackMixin {
|
||||
LayoutCallback<Constraints>? _callback;
|
||||
|
||||
/// Change the layout callback.
|
||||
void _updateCallback(LayoutCallback<Constraints>? value) {
|
||||
void _updateCallback(LayoutCallback<Constraints> value) {
|
||||
if (value == _callback) {
|
||||
return;
|
||||
}
|
||||
_callback = value;
|
||||
markNeedsLayout();
|
||||
scheduleLayoutCallback();
|
||||
}
|
||||
|
||||
/// Invoke the builder callback supplied via [AbstractLayoutBuilder] and
|
||||
/// Invokes the builder callback supplied via [AbstractLayoutBuilder] and
|
||||
/// rebuilds the [AbstractLayoutBuilder]'s widget tree, if needed.
|
||||
///
|
||||
/// No work will be done if [layoutInfo] has not changed since the last time
|
||||
/// this method was called, and [AbstractLayoutBuilder.updateShouldRebuild]
|
||||
/// No further work will be done if [layoutInfo] has not changed since the last
|
||||
/// time this method was called, and [AbstractLayoutBuilder.updateShouldRebuild]
|
||||
/// returned `false` when the widget was rebuilt.
|
||||
///
|
||||
/// This method should typically be called as soon as possible in the class's
|
||||
/// [performLayout] implementation, before any layout work is done.
|
||||
@protected
|
||||
void rebuildIfNecessary() {
|
||||
assert(_callback != null);
|
||||
invokeLayoutCallback(_callback!);
|
||||
}
|
||||
@visibleForOverriding
|
||||
@override
|
||||
void layoutCallback() => _callback!(constraints);
|
||||
|
||||
/// The information to invoke the [AbstractLayoutBuilder.builder] callback with.
|
||||
///
|
||||
@@ -381,6 +382,7 @@ class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
|
||||
class _RenderLayoutBuilder extends RenderBox
|
||||
with
|
||||
RenderObjectWithChildMixin<RenderBox>,
|
||||
RenderObjectWithLayoutCallbackMixin,
|
||||
RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> {
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
@@ -433,7 +435,7 @@ class _RenderLayoutBuilder extends RenderBox
|
||||
@override
|
||||
void performLayout() {
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
rebuildIfNecessary();
|
||||
runLayoutCallback();
|
||||
if (child != null) {
|
||||
child!.layout(constraints, parentUsesSize: true);
|
||||
size = constraints.constrain(child!.size);
|
||||
|
||||
@@ -2632,7 +2632,10 @@ class _OverlayChildLayoutBuilder extends AbstractLayoutBuilder<OverlayChildLayou
|
||||
// Additionally, like RenderDeferredLayoutBox, this RenderBox also uses the Stack
|
||||
// layout algorithm so developers can use the Positioned widget.
|
||||
class _RenderLayoutBuilder extends RenderProxyBox
|
||||
with _RenderTheaterMixin, RenderAbstractLayoutBuilderMixin<OverlayChildLayoutInfo, RenderBox> {
|
||||
with
|
||||
_RenderTheaterMixin,
|
||||
RenderObjectWithLayoutCallbackMixin,
|
||||
RenderAbstractLayoutBuilderMixin<OverlayChildLayoutInfo, RenderBox> {
|
||||
@override
|
||||
Iterable<RenderBox> _childrenInPaintOrder() {
|
||||
final RenderBox? child = this.child;
|
||||
@@ -2709,19 +2712,25 @@ class _RenderLayoutBuilder extends RenderProxyBox
|
||||
return OverlayChildLayoutInfo._((overlayPortalSize, paintTransform, size));
|
||||
}
|
||||
|
||||
@override
|
||||
@visibleForOverriding
|
||||
void layoutCallback() {
|
||||
_layoutInfo = _computeNewLayoutInfo();
|
||||
super.layoutCallback();
|
||||
}
|
||||
|
||||
int? _callbackId;
|
||||
@override
|
||||
void performLayout() {
|
||||
// The invokeLayoutCallback allows arbitrary access to the sizes of
|
||||
// RenderBoxes the we know that have finished doing layout.
|
||||
invokeLayoutCallback((_) => _layoutInfo = _computeNewLayoutInfo());
|
||||
rebuildIfNecessary();
|
||||
runLayoutCallback();
|
||||
if (child case final RenderBox child?) {
|
||||
layoutChild(child, constraints);
|
||||
}
|
||||
assert(_callbackId == null);
|
||||
_callbackId ??= SchedulerBinding.instance.scheduleFrameCallback(
|
||||
_frameCallback,
|
||||
scheduleNewFrame: false,
|
||||
);
|
||||
layoutChild(child!, constraints);
|
||||
}
|
||||
|
||||
// This RenderObject is a child of _RenderDeferredLayouts which in turn is a
|
||||
|
||||
@@ -37,6 +37,7 @@ class SliverLayoutBuilder extends ConstrainedLayoutBuilder<SliverConstraints> {
|
||||
class _RenderSliverLayoutBuilder extends RenderSliver
|
||||
with
|
||||
RenderObjectWithChildMixin<RenderSliver>,
|
||||
RenderObjectWithLayoutCallbackMixin,
|
||||
RenderAbstractLayoutBuilderMixin<SliverConstraints, RenderSliver> {
|
||||
@override
|
||||
double childMainAxisPosition(RenderObject child) {
|
||||
@@ -50,7 +51,7 @@ class _RenderSliverLayoutBuilder extends RenderSliver
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
rebuildIfNecessary();
|
||||
runLayoutCallback();
|
||||
child?.layout(constraints, parentUsesSize: true);
|
||||
geometry = child?.geometry ?? SliverGeometry.zero;
|
||||
}
|
||||
|
||||
@@ -872,7 +872,6 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!);
|
||||
await tester.pump();
|
||||
WidgetsBinding.instance.buildOwner!.reassemble(WidgetsBinding.instance.rootElement!);
|
||||
@@ -880,6 +879,111 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'LayoutBuilder in a subtree that skips layout does not rebuild during the initial treewalk',
|
||||
(WidgetTester tester) async {
|
||||
bool rebuilt = false;
|
||||
final LayoutBuilder layoutBuilder = LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
rebuilt = true;
|
||||
return const Placeholder();
|
||||
},
|
||||
);
|
||||
final OverlayEntry overlayEntry1 = OverlayEntry(
|
||||
maintainState: true,
|
||||
builder: (BuildContext context) => layoutBuilder,
|
||||
);
|
||||
// OverlayEntry2 obstructs OverlayEntry1 and forces it to skip layout.
|
||||
final OverlayEntry overlayEntry2 = OverlayEntry(
|
||||
opaque: true,
|
||||
canSizeOverlay: true,
|
||||
builder: (BuildContext context) => Container(),
|
||||
);
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry1
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry2
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
// The UnconstrainedBox makes sure the OverlayEntries are not relayout boundaries.
|
||||
child: UnconstrainedBox(
|
||||
child: Overlay(initialEntries: <OverlayEntry>[overlayEntry1, overlayEntry2]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Element layoutBuilderElement = tester.element(
|
||||
find.byWidget(layoutBuilder, skipOffstage: false),
|
||||
);
|
||||
layoutBuilderElement.markNeedsBuild();
|
||||
await tester.pump();
|
||||
expect(rebuilt, isFalse);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('LayoutBuilder in a subtree that skips layout still rebuilds', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
bool rebuilt = false;
|
||||
final LayoutBuilder layoutBuilder = LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
rebuilt = true;
|
||||
return const Placeholder();
|
||||
},
|
||||
);
|
||||
final OverlayEntry overlayEntry1 = OverlayEntry(
|
||||
maintainState: true,
|
||||
canSizeOverlay: true,
|
||||
builder: (BuildContext context) => layoutBuilder,
|
||||
);
|
||||
// OverlayEntry2 obstructs OverlayEntry1 and forces it to skip layout.
|
||||
final OverlayEntry overlayEntry2 = OverlayEntry(
|
||||
opaque: true,
|
||||
canSizeOverlay: true,
|
||||
builder: (BuildContext context) => const Placeholder(),
|
||||
);
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry1
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
addTearDown(
|
||||
() =>
|
||||
overlayEntry2
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
// The UnconstrainedBox makes sure the OverlayEntries are not relayout boundaries.
|
||||
child: UnconstrainedBox(child: Overlay(initialEntries: <OverlayEntry>[overlayEntry1])),
|
||||
),
|
||||
);
|
||||
tester.state<OverlayState>(find.byType(Overlay)).insert(overlayEntry2);
|
||||
await tester.pump();
|
||||
|
||||
rebuilt = false;
|
||||
final Element layoutBuilderElement = tester.element(
|
||||
find.byWidget(layoutBuilder, skipOffstage: false),
|
||||
);
|
||||
layoutBuilderElement.markNeedsBuild();
|
||||
expect(rebuilt, isFalse);
|
||||
await tester.pump();
|
||||
expect(rebuilt, isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
class _SmartLayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
|
||||
@@ -921,7 +1025,9 @@ class _SmartLayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
|
||||
typedef _OnChildWasPaintedCallback = void Function(Offset extraOffset);
|
||||
|
||||
class _RenderSmartLayoutBuilder extends RenderProxyBox
|
||||
with RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> {
|
||||
with
|
||||
RenderObjectWithLayoutCallbackMixin,
|
||||
RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> {
|
||||
_RenderSmartLayoutBuilder({required double offsetPercentage, required this.onChildWasPainted})
|
||||
: _offsetPercentage = offsetPercentage;
|
||||
|
||||
@@ -946,7 +1052,7 @@ class _RenderSmartLayoutBuilder extends RenderProxyBox
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
rebuildIfNecessary();
|
||||
runLayoutCallback();
|
||||
child?.layout(constraints);
|
||||
}
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ void main() {
|
||||
expect(paintTransform, Matrix4.translationValues(10.0, 20.0, 0.0) * transform);
|
||||
});
|
||||
|
||||
testWidgets('Still works if child is null', (WidgetTester tester) async {
|
||||
testWidgets('Still works if child and overlay child are null', (WidgetTester tester) async {
|
||||
late final OverlayEntry overlayEntry;
|
||||
addTearDown(
|
||||
() =>
|
||||
@@ -307,7 +307,7 @@ void main() {
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context, OverlayChildLayoutInfo layoutInfo) {
|
||||
regularChildSize = layoutInfo.childSize;
|
||||
return const SizedBox();
|
||||
return const _NullLeaf();
|
||||
},
|
||||
child: null,
|
||||
),
|
||||
@@ -363,3 +363,22 @@ void main() {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _NullLeaf extends Widget {
|
||||
const _NullLeaf();
|
||||
@override
|
||||
Element createElement() => _NullElement(this);
|
||||
}
|
||||
|
||||
class _NullElement extends Element {
|
||||
_NullElement(super.widget);
|
||||
|
||||
@override
|
||||
void mount(Element? parent, Object? newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
rebuild(force: true);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get debugDoingBuild => throw UnimplementedError();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user