Skip to content

Commit

Permalink
Stand-alone widget tree with multiple render trees to enable multi-vi…
Browse files Browse the repository at this point in the history
…ew rendering (#125003)

This change enables Flutter to generate multiple Scenes to be rendered into separate FlutterViews from a single widget tree. Each Scene is described by a separate render tree, which are all associated with the single widget tree.

This PR implements the framework-side mechanisms to describe the content to be rendered into multiple views. Separate engine-side changes are necessary to provide these views to the framework and to draw the framework-generated Scene into them.

## Summary of changes

The details of this change are described in [flutter.dev/go/multiple-views](https://flutter.dev/go/multiple-views). Below is a high-level summary organized by layers.

### Rendering layer changes

* The `RendererBinding` no longer owns a single `renderView`. In fact, it doesn't OWN any `RenderView`s at all anymore. Instead, it offers an API (`addRenderView`/`removeRenderView`) to add and remove `RenderView`s that then will be MANAGED by the binding. The `RenderView` itself is now owned by a higher-level abstraction (e.g. the `RawView` Element of the widgets layer, see below), who is also in charge of adding it to the binding. When added, the binding will interact with the `RenderView` to produce a frame (e.g. by calling `compositeFrame` on it) and to perform hit tests for incoming pointer events. Multiple `RenderView`s can be added to the binding (typically one per `FlutterView`) to produce multiple Scenes.
* Instead of owning a single `pipelineOwner`, the `RendererBinding` now owns the root of the `PipelineOwner` tree (exposed as `rootPipelineOwner` on the binding). Each `PipelineOwner` in that tree (except for the root) typically manages its own render tree typically rooted in one of the `RenderView`s mentioned in the previous bullet. During frame production, the binding will instruct each `PipelineOwner` of that tree to flush layout, paint, semantics etc. A higher-level abstraction (e.g. the widgets layer, see below) is in charge of adding `PipelineOwner`s to this tree.
* Backwards compatibility: The old `renderView` and `pipelineOwner` properties of the `RendererBinding` are retained, but marked as deprecated. Care has been taken to keep their original behavior for the deprecation period, i.e. if you just call `runApp`, the render tree bootstrapped by this call is rooted in the deprecated `RendererBinding.renderView` and managed by the deprecated `RendererBinding.pipelineOwner`.

### Widgets layer changes

* The `WidgetsBinding` no longer attaches the widget tree to an existing render tree. Instead, it bootstraps a stand-alone widget tree that is not backed by a render tree. For this, `RenderObjectToWidgetAdapter` has been replaced by `RootWidget`.
* Multiple render trees can be bootstrapped and attached to the widget tree with the help of the `View` widget, which internally is backed by a `RawView` widget. Configured with a `FlutterView` to render into, the `RawView` creates a new `PipelineOwner` and a new `RenderView` for the new render tree. It adds the new `RenderView` to the `RendererBinding` and its `PipelineOwner` to the pipeline owner tree.
* The `View` widget can only appear in certain well-defined locations in the widget tree since it bootstraps a new render tree and does not insert a `RenderObject` into an ancestor. However, almost all Elements expect that their children insert `RenderObject`s, otherwise they will not function properly. To produce a good error message when the `View` widget is used in an illegal location, the `debugMustInsertRenderObjectIntoSlot` method has been added to Element, where a child can ask whether a given slot must insert a RenderObject into its ancestor or not. In practice, the `View` widget can be used as a child of the `RootWidget`, inside the `view` slot of the `ViewAnchor` (see below) and inside a `ViewCollection` (see below). In those locations, the `View` widget may be wrapped in other non-RenderObjectWidgets (e.g. InheritedWidgets).
* The new `ViewAnchor` can be used to create a side-view inside a parent `View`. The `child` of the `ViewAnchor` widget renders into the parent `View` as usual, but the `view` slot can take on another `View` widget, which has access to all inherited widgets above the `ViewAnchor`. Metaphorically speaking, the view is anchored to the location of the `ViewAnchor` in the widget tree.
* The new `ViewCollection` widget allows for multiple sibling views as it takes a list of `View`s as children. It can be used in all the places that accept a `View` widget.

## Google3

As of July 5, 2023 this change passed a TAP global presubmit (TGP) in google3: tap/OCL:544707016:BASE:545809771:1688597935864:e43dd651

## Note to reviewers

This change is big (sorry). I suggest focusing the initial review on the changes inside of `packages/flutter` first. The majority of the changes describe above are implemented in (listed in suggested review order):

* `rendering/binding.dart`
* `widgets/binding.dart`
* `widgets/view.dart`
* `widgets/framework.dart`

All other changes included in the PR are basically the fallout of what's implemented in those files. Also note that a lot of the lines added in this PR are documentation and tests.

I am also very happy to walk reviewers through the code in person or via video call, if that is helpful.

I appreciate any feedback.

## Feedback to address before submitting ("TODO")
  • Loading branch information
goderbauer authored Jul 17, 2023
1 parent 7a42ed7 commit 6f09064
Show file tree
Hide file tree
Showing 57 changed files with 4,595 additions and 534 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Future<void> main() async {
size: const Size(355.0, 635.0),
view: tester.view,
);
final RenderView renderView = WidgetsBinding.instance.renderView;
final RenderView renderView = WidgetsBinding.instance.renderViews.single;
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark;

watch.start();
Expand Down
2 changes: 1 addition & 1 deletion dev/bots/test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ Future<void> _runWebTreeshakeTest() async {
final String javaScript = mainDartJs.readAsStringSync();

// Check that we're not looking at minified JS. Otherwise this test would result in false positive.
expect(javaScript.contains('RenderObjectToWidgetElement'), true);
expect(javaScript.contains('RootElement'), true);

const String word = 'debugFillProperties';
int count = 0;
Expand Down
4 changes: 2 additions & 2 deletions dev/integration_tests/flutter_gallery/test/smoke_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async {
// Verify that the dumps are pretty.
final String routeName = demo.routeName;
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep());
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? '');
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderViews.single.toStringDeep());
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderViews.single.debugLayer?.toStringDeep() ?? '');
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());

// Scroll the demo around a bit more.
Expand Down
3 changes: 2 additions & 1 deletion examples/layers/rendering/custom_coordinate_systems.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// system. Most of the guts of this examples are in src/sector_layout.dart.

import 'package:flutter/rendering.dart';
import 'src/binding.dart';
import 'src/sector_layout.dart';

RenderBox buildSectorExample() {
Expand All @@ -21,5 +22,5 @@ RenderBox buildSectorExample() {
}

void main() {
RenderingFlutterBinding(root: buildSectorExample()).scheduleFrame();
ViewRenderingFlutterBinding(root: buildSectorExample()).scheduleFrame();
}
3 changes: 2 additions & 1 deletion examples/layers/rendering/flex_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import 'package:flutter/rendering.dart';

import 'src/binding.dart';
import 'src/solid_color_box.dart';

void main() {
Expand Down Expand Up @@ -86,5 +87,5 @@ void main() {
child: RenderPadding(child: table, padding: const EdgeInsets.symmetric(vertical: 50.0)),
);

RenderingFlutterBinding(root: root).scheduleFrame();
ViewRenderingFlutterBinding(root: root).scheduleFrame();
}
6 changes: 4 additions & 2 deletions examples/layers/rendering/hello_world.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

import 'package:flutter/rendering.dart';

import 'src/binding.dart';

void main() {
// We use RenderingFlutterBinding to attach the render tree to the window.
RenderingFlutterBinding(
// We use ViewRenderingFlutterBinding to attach the render tree to the window.
ViewRenderingFlutterBinding(
// The root of our render tree is a RenderPositionedBox, which centers its
// child both vertically and horizontally.
root: RenderPositionedBox(
Expand Down
4 changes: 3 additions & 1 deletion examples/layers/rendering/spinning_square.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

import 'src/binding.dart';

class NonStopVSync implements TickerProvider {
const NonStopVSync();
@override
Expand Down Expand Up @@ -42,7 +44,7 @@ void main() {
child: spin,
);
// and attach it to the window.
RenderingFlutterBinding(root: root);
ViewRenderingFlutterBinding(root: root);

// To make the square spin, we use an animation that repeats every 1800
// milliseconds.
Expand Down
69 changes: 69 additions & 0 deletions examples/layers/rendering/src/binding.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui';

import 'package:flutter/rendering.dart';

/// An extension of [RenderingFlutterBinding] that owns and manages a
/// [renderView].
///
/// Unlike [RenderingFlutterBinding], this binding also creates and owns a
/// [renderView] to simplify bootstrapping for apps that have a dedicated main
/// view.
class ViewRenderingFlutterBinding extends RenderingFlutterBinding {
/// Creates a binding for the rendering layer.
///
/// The `root` render box is attached directly to the [renderView] and is
/// given constraints that require it to fill the window. The [renderView]
/// itself is attached to the [rootPipelineOwner].
///
/// This binding does not automatically schedule any frames. Callers are
/// responsible for deciding when to first call [scheduleFrame].
ViewRenderingFlutterBinding({ RenderBox? root }) : _root = root;

@override
void initInstances() {
super.initInstances();
// TODO(goderbauer): Create window if embedder doesn't provide an implicit view.
assert(PlatformDispatcher.instance.implicitView != null);
_renderView = initRenderView(PlatformDispatcher.instance.implicitView!);
_renderView.child = _root;
_root = null;
}

RenderBox? _root;

@override
RenderView get renderView => _renderView;
late RenderView _renderView;

/// Creates a [RenderView] object to be the root of the
/// [RenderObject] rendering tree, and initializes it so that it
/// will be rendered when the next frame is requested.
///
/// Called automatically when the binding is created.
RenderView initRenderView(FlutterView view) {
final RenderView renderView = RenderView(view: view);
rootPipelineOwner.rootNode = renderView;
addRenderView(renderView);
renderView.prepareInitialFrame();
return renderView;
}

@override
PipelineOwner createRootPipelineOwner() {
return PipelineOwner(
onSemanticsOwnerCreated: () {
renderView.scheduleInitialSemantics();
},
onSemanticsUpdate: (SemanticsUpdate update) {
renderView.updateSemantics(update);
},
onSemanticsOwnerDisposed: () {
renderView.clearSemantics();
},
);
}
}
4 changes: 3 additions & 1 deletion examples/layers/rendering/touch_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import 'package:flutter/material.dart'; // Imported just for its color palette.
import 'package:flutter/rendering.dart';

import 'src/binding.dart';

// Material design colors. :p
List<Color> _kColors = <Color>[
Colors.teal,
Expand Down Expand Up @@ -133,5 +135,5 @@ void main() {
..left = 20.0;

// Finally, we attach the render tree we've built to the screen.
RenderingFlutterBinding(root: stack).scheduleFrame();
ViewRenderingFlutterBinding(root: stack).scheduleFrame();
}
18 changes: 14 additions & 4 deletions examples/layers/widgets/spinning_mixed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
ElevatedButton(
child: Row(
child: const Row(
children: <Widget>[
Image.network('https://flutter.dev/images/favicon.png'),
const Text('PRESS ME'),
FlutterLogo(),
Text('PRESS ME'),
],
),
onPressed: () {
Expand Down Expand Up @@ -102,6 +102,16 @@ void main() {
transformBox = RenderTransform(child: flexRoot, transform: Matrix4.identity(), alignment: Alignment.center);
final RenderPadding root = RenderPadding(padding: const EdgeInsets.all(80.0), child: transformBox);

binding.renderView.child = root;
// TODO(goderbauer): Create a window if embedder doesn't provide an implicit view to draw into.
assert(binding.platformDispatcher.implicitView != null);
final RenderView view = RenderView(
view: binding.platformDispatcher.implicitView!,
child: root,
);
final PipelineOwner pipelineOwner = PipelineOwner()..rootNode = view;
binding.rootPipelineOwner.adoptChild(pipelineOwner);
binding.addRenderView(view);
view.prepareInitialFrame();

binding.addPersistentFrameCallback(rotate);
}
Loading

0 comments on commit 6f09064

Please sign in to comment.