Unverified Commit 235927d5 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Allow callers to pump a root widget with no child (#75576)

This can be useful for offscreen widget trees, where the caller
wants to completely tear down the tree (properly clean up) when
they're done with the tree, to ensure they're not leaving behind
any event listeners that could be registered by child elements
(which could lead to memory leaks and unexpected behavior).
parent 8e87408f
......@@ -1151,6 +1151,7 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje
assert(parent == null);
super.mount(parent, newSlot);
_rebuild();
assert(_child != null);
}
@override
......@@ -1180,7 +1181,6 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje
void _rebuild() {
try {
_child = updateChild(_child, widget.child, _rootChildSlot);
assert(_child != null);
} catch (exception, stack) {
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
......
......@@ -2307,6 +2307,67 @@ abstract class BuildContext {
/// To assign a build owner to a tree, use the
/// [RootRenderObjectElement.assignOwner] method on the root element of the
/// widget tree.
///
/// {@tool dartpad --template=freeform}
/// This example shows how to build an off-screen widget tree used to measure
/// the size of the rendered tree. For some use cases, the simpler [Offstage]
/// widget may be a better alternative to this approach.
///
/// ```dart imports
/// import 'package:flutter/rendering.dart';
/// import 'package:flutter/widgets.dart';
/// ```
///
/// ```dart
/// void main() {
/// WidgetsFlutterBinding.ensureInitialized();
/// print(measureWidget(const SizedBox(width: 640, height: 480)));
/// }
///
/// Size measureWidget(Widget widget) {
/// final PipelineOwner pipelineOwner = PipelineOwner();
/// final MeasurementView rootView = pipelineOwner.rootNode = MeasurementView();
/// final BuildOwner buildOwner = BuildOwner(focusManager: FailingFocusManager());
/// final RenderObjectToWidgetElement<RenderBox> element = RenderObjectToWidgetAdapter<RenderBox>(
/// container: rootView,
/// debugShortDescription: '[root]',
/// child: widget,
/// ).attachToRenderTree(buildOwner);
/// try {
/// rootView.scheduleInitialLayout();
/// pipelineOwner.flushLayout();
/// return rootView.size;
/// } finally {
/// // Clean up.
/// element.update(RenderObjectToWidgetAdapter<RenderBox>(container: rootView));
/// buildOwner.finalizeTree();
/// }
/// }
///
/// // The default FocusManager, when created, modifies some static properties
/// // that we don't want to modify, which is why we use a failing implementation
/// // here.
/// class FailingFocusManager implements FocusManager {
/// @override
/// dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
///
/// @override
/// String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) => 'FailingFocusManager';
/// }
///
/// class MeasurementView extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
/// @override
/// void performLayout() {
/// assert(child != null);
/// child!.layout(const BoxConstraints(), parentUsesSize: true);
/// size = child!.size;
/// }
///
/// @override
/// void debugAssertDoesMeetConstraints() => true;
/// }
/// ```
/// {@end-tool}
class BuildOwner {
/// Creates an object that manages widgets.
BuildOwner({ this.onBuildScheduled, FocusManager? focusManager }) :
......
......@@ -32,7 +32,7 @@ class OffscreenWidgetTree {
final PipelineOwner pipelineOwner = PipelineOwner();
RenderObjectToWidgetElement<RenderBox>? root;
void pumpWidget(Widget app) {
void pumpWidget(Widget? app) {
root = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
......@@ -212,4 +212,45 @@ void main() {
expect(offscreenFocus.hasFocus, isTrue);
});
testWidgets('able to tear down offscreen tree', (WidgetTester tester) async {
final OffscreenWidgetTree tree = OffscreenWidgetTree();
final List<WidgetState> states = <WidgetState>[];
tree.pumpWidget(SizedBox(child: TestStates(states: states)));
expect(states, <WidgetState>[WidgetState.initialized]);
expect(tree.renderView.child, isNotNull);
tree.pumpWidget(null); // The root node should be allowed to have no child.
expect(states, <WidgetState>[WidgetState.initialized, WidgetState.disposed]);
expect(tree.renderView.child, isNull);
});
}
enum WidgetState {
initialized,
disposed,
}
class TestStates extends StatefulWidget {
const TestStates({required this.states});
final List<WidgetState> states;
@override
TestStatesState createState() => TestStatesState();
}
class TestStatesState extends State<TestStates> {
@override
void initState() {
super.initState();
widget.states.add(WidgetState.initialized);
}
@override
void dispose() {
widget.states.add(WidgetState.disposed);
super.dispose();
}
@override
Widget build(BuildContext context) => Container();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment