Repository: flutter
Issue: Flutter should offer build order control and/or dirty subtree monitoring
Labels: c: new feature,framework,c: proposal,P3,team-framework,triaged-framework

This ticket describes a use-case I'm trying to implement, why I'm implementing it, and the ways in which Flutter makes it impossible.

I'm implementing a widget called `ContentLayers` that takes an arbitrary `content` child, along with arbitrary `underlay` and `overlay` widgets.

This `ContentLayers` widget is intended to facilitate common use-cases related to text fields, flow-based documents, and page based documents, where decorations need to layout and paint based on the layout of the text field or document content. For example: content selection, caret, user comments, etc.

The way Flutter gets around this problem in the framework is by building up monolithic `RenderObject`s that do all-the-things. This makes them impossible to compose into alternate solutions within the community, which is why `super_editor` created `SuperTextField`.

The way `super_editor` has gotten around this issue up to this point is by inspecting content layout on a followup frame. This is what we call the "next frame problem". It creates a lot of fundamentally unnecessary accounting on our end, and also leads to race conditions. We want to eliminate this hack from our implementations.

The challenge of this `ContentLayers` widget is that the content **MUST** always be built **AND** laid out before running build on the underlays and overlays.

Flutter's standard behavior is a pipeline that runs all builds, then all layouts, then all compositing. This pipeline is so restrictive that even the Flutter framework ships with violations to this order of operations, e.g., `LayoutBuilder`.

Using the same approach as `LayoutBuilder`, I'm able to build the underlays and overlays during the layout phase, which allows me to ensure that the underlays and overlays build with an up-to-date `content` layout. That part is solvable.

The part that's unsolvable is controlling when and how the `content`, `underlays`, and `overlays` subtrees **rebuild**. A combination of Flutter's `PipelineOwner` and `BuildOwner` completely obscure observation of dirty subtrees, and when those dirty subtrees are rebuilt. This opaque and automatic rebuild is fine for the vast majority of Flutter use-cases, but it prevents the completion of the `ContentLayers` widget.

For example, consider the following widget tree:

```
  ContentLayers
    Document
    CaretOverlay
```

The initial build and layout work fine. But now consider what happens when the user types a character into the `Document`. Typing this character will alter the layout of the `Document`, scheduling a `Document` rebuild. It will also change the user's selection, scheduling a `CaretOverlay` rebuild. At this point we have:

```
  ContentLayers (clean)
    Document (needs build) (needs layout) (needs paint)
    CaretOverlay (needs build) (needs layout) (needs paint)
```

Then, Flutter's `BuildOwner` will run build on `Document`, giving us:

```
  ContentLayers (clean)
    Document (needs layout) (needs paint)
    CaretOverlay (needs build) (needs layout) (needs paint)
```

Then, we reach the error. The `BuildOwner` will try to build `CaretOverlay`. But `CaretOverlay` contains pseudo code like the following:

```dart
void build(BuildContext context) {
  final caretPosition = composer.getCaretPosition();

  // This line will blow up because the document hasn't run layout yet.
  final documentLayout = documentKey.currentContext!.findRenderObject() as DocumentLayout;

  final selectionOffset = documentLayout.getOffsetForPosition(caretPosition);

  return Stack(
    children: [
      Positioned(
        left: selectionOffset.dx,
        top: selectionOffset.dy,
        child: Caret(),
    ],
  );
}
```

This will throw an error because the `CaretOverlay` will attempt to access the `RenderObject` layout inside of `Document`. But that `RenderObject` is still marked as `NEEDS LAYOUT`, which triggers an assertion.

Flutter doesn't seem to provide any mechanism that allows us to intercept the knowledge that `Document` marked itself dirty, nor does Flutter provide a mechanism to prevent default rebuild behaviors.

I see two ways that Flutter could make this widget possible:

## Let ancestors monitor descendent dirty changes
If the `ContentLayers` `Element` could listen for build requests anywhere in the `Document` subtree, then the `ContentLayers` could detach the `overlays` and `underlays` the moment that a `Document` requests a rebuild. By detaching the `overlays` and `underlays`, those layers won't be rebuilt. Instead, we'll let `ContentLayers` re-attach the `overlays` and `underlays` in the subsequent layout phase.

Again, the key feature to allow this implementation would be the ability for `ContentLayers` to be notified of any and all dirty markers throughout the `Document` subtree. This is kind of like inherited widgets working in reverse. This would need to be added at the framework level because it could involve any and all widgets. I don't see any way to provide a separate, opt-in artifact that developers could add to their existing widget trees.

## Let developers control rebuild decisions
If the `ContentLayers` `Element` could take full responsibility for triggering rebuilds on its subtrees then this problem would be solved immediately. The `ContentLayersElement` would simply tell the `BuildOwner` to leave those trees alone, and then the `RenderContentLayers` would explicitly run (re)build during layout, and everything would work fine.

## ContentLayers hack implementation
The following source code is the closest that I was able to get to accomplishing the use-case described in this ticket:
https://github.com/superlistapp/super_editor/blob/main/super_editor/lib/src/infrastructure/content_layers.dart