Repository: flutter
Issue: NestedScrollView / Material3: _StretchController throws  the error: Build scheduled during frame
Labels: framework,f: material design,f: scrolling,a: error message,has reproducible steps,P2,found in release: 3.0,found in release: 3.1,team-design,triaged-design

<!-- Thank you for using Flutter!

     If you are looking for support, please check out our documentation
     or consider asking a question on Stack Overflow:
      * https://flutter.dev/
      * https://api.flutter.dev/
      * https://stackoverflow.com/questions/tagged/flutter?sort=frequent

     If you have found a bug or if our documentation doesn't have an answer
     to what you're looking for, then fill out the template below. Please read
     our guide to filing a bug first: https://flutter.dev/docs/resources/bug-reports
-->

## Steps to Reproduce

<!-- Please include full steps to reproduce so that we can reproduce the problem. -->


Problem
If the NestedScrollView example from _api.flutter.dev_  is used with material3 the stretch controller throws a _**Build scheduled during frame**_  error. The error appears when the CustomScrollView is stretched  multiple times (pulled down).  I think the errors occurs when the dimension is changed and trigger applyNewDimensions -> BallisticScrollActivity, etc, etc,

Error:
Build scheduled during frame.
While the widget tree was being built, laid out, and painted, a new frame was scheduled to rebuild the widget tree.
This might be because setState() was called from a layout or paint callback.

**Expected results:** <!-- what did you want to see? -->
It is expected that this standard code example runs without errors.

**Actual results:** <!-- what did you see? -->
If the error occurs the CustomScrollView cannot be stretched anymore until a complete rebuild of the widget. The components in the slivers are also not always completely visible after the error, I think the view is than still partly stretched. :)

<details>
<summary>Code sample</summary>

<!--
      Please create a minimal reproducible sample that shows the problem
      and attach it below between the lines with the backticks.

      To create it you can use `flutter create bug` command and update the `main.dart` file.

      Alternatively, you can use https://dartpad.dev/
      which is capable of creating and running small Flutter apps.

      Without this we will unlikely be able to progress on the issue, and because of that
      we regretfully will have to close it.
-->

```
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      title: _title,
      home: const MyStatelessWidget(),
    );
  }
}

class MyStatelessWidget extends StatelessWidget {
  const MyStatelessWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final List<String> tabs = <String>['Tab 1', 'Tab 2'];
    return DefaultTabController(
      length: tabs.length, // This is the number of tabs.
      child: Scaffold(
        body: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            // These are the slivers that show up in the "outer" scroll view.
            return <Widget>[
              SliverOverlapAbsorber(
                // This widget takes the overlapping behavior of the SliverAppBar,
                // and redirects it to the SliverOverlapInjector below. If it is
                // missing, then it is possible for the nested "inner" scroll view
                // below to end up under the SliverAppBar even when the inner
                // scroll view thinks it has not been scrolled.
                // This is not necessary if the "headerSliverBuilder" only builds
                // widgets that do not overlap the next sliver.
                handle:
                    NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                sliver: SliverAppBar(
                  title:
                      const Text('Books'), // This is the title in the app bar.
                  pinned: true,
                  expandedHeight: 150.0,
                  // The "forceElevated" property causes the SliverAppBar to show
                  // a shadow. The "innerBoxIsScrolled" parameter is true when the
                  // inner scroll view is scrolled beyond its "zero" point, i.e.
                  // when it appears to be scrolled below the SliverAppBar.
                  // Without this, there are cases where the shadow would appear
                  // or not appear inappropriately, because the SliverAppBar is
                  // not actually aware of the precise position of the inner
                  // scroll views.
                  forceElevated: innerBoxIsScrolled,
                  bottom: TabBar(
                    // These are the widgets to put in each tab in the tab bar.
                    tabs: tabs.map((String name) => Tab(text: name)).toList(),
                  ),
                ),
              ),
            ];
          },
          body: TabBarView(
            // These are the contents of the tab views, below the tabs.
            children: tabs.map((String name) {
              return SafeArea(
                top: false,
                bottom: false,
                child: Builder(
                  // This Builder is needed to provide a BuildContext that is
                  // "inside" the NestedScrollView, so that
                  // sliverOverlapAbsorberHandleFor() can find the
                  // NestedScrollView.
                  builder: (BuildContext context) {
                    return CustomScrollView(
                      // The "controller" and "primary" members should be left
                      // unset, so that the NestedScrollView can control this
                      // inner scroll view.
                      // If the "controller" property is set, then this scroll
                      // view will not be associated with the NestedScrollView.
                      // The PageStorageKey should be unique to this ScrollView;
                      // it allows the list to remember its scroll position when
                      // the tab view is not on the screen.
                      key: PageStorageKey<String>(name),
                      slivers: <Widget>[
                        SliverOverlapInjector(
                          // This is the flip side of the SliverOverlapAbsorber
                          // above.
                          handle:
                              NestedScrollView.sliverOverlapAbsorberHandleFor(
                                  context),
                        ),
                        SliverPadding(
                          padding: const EdgeInsets.all(8.0),
                          // In this example, the inner scroll view has
                          // fixed-height list items, hence the use of
                          // SliverFixedExtentList. However, one could use any
                          // sliver widget here, e.g. SliverList or SliverGrid.
                          sliver: SliverFixedExtentList(
                            // The items in this example are fixed to 48 pixels
                            // high. This matches the Material Design spec for
                            // ListTile widgets.
                            itemExtent: 48.0,
                            delegate: SliverChildBuilderDelegate(
                              (BuildContext context, int index) {
                                // This builder is called for each child.
                                // In this example, we just number each list item.
                                return ListTile(
                                  title: Text('Item $index'),
                                );
                              },
                              // The childCount of the SliverChildBuilderDelegate
                              // specifies how many children this inner list
                              // has. In this example, each tab has a list of
                              // exactly 30 items, but this is arbitrary.
                              childCount: 30,
                            ),
                          ),
                        ),
                      ],
                    );
                  },
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}

```

</details>

<details>
  <summary>Logs</summary>

<!--
      Run your application with `flutter run --verbose` and attach all the
      log output below between the lines with the backticks. If there is an
      exception, please see if the error message includes enough information
      to explain how to solve the issue.
-->

```
════════ Exception caught by foundation library ════════════════════════════════
The following assertion was thrown while dispatching notifications for _StretchController:
Build scheduled during frame.

While the widget tree was being built, laid out, and painted, a new frame was scheduled to rebuild the widget tree.

This might be because setState() was called from a layout or paint callback. If a change is needed to the widget tree, it should be applied as the tree is being built. Scheduling a change for the subsequent frame instead results in an interface that lags behind by one frame. If this was done to make your build dependent on a size measured at layout time, consider using a LayoutBuilder, CustomSingleChildLayout, or CustomMultiChildLayout. If, on the other hand, the one frame delay is the desired effect, for example because this is an animation, consider scheduling the frame in a post-frame callback using SchedulerBinding.addPostFrameCallback or using an AnimationController to trigger the animation.

When the exception was thrown, this was the stack
#0      WidgetsBinding._handleBuildScheduled.<anonymous closure>
#1      WidgetsBinding._handleBuildScheduled
#2      BuildOwner.scheduleBuildFor
#3      Element.markNeedsBuild
#4      State.setState
#5      _AnimatedState._handleChange
#6      ChangeNotifier.notifyListeners
#7      AnimationLocalListenersMixin.notifyListeners
#8      AnimationController.value=
#9      AnimationController.forward
#10     _StretchController._recede
#11     _StretchController.scrollEnd
#12     _StretchingOverscrollIndicatorState._handleScrollNotification
#13     _NotificationElement.onNotification
#14     _NotificationNode.dispatchNotification
#15     _NotificationNode.dispatchNotification
#16     Element.dispatchNotification
#17     Notification.dispatch
#18     ScrollActivity.dispatchScrollEndNotification
#19     ScrollPosition.didEndScroll
#20     ScrollPosition.beginActivity
#21     _NestedInnerBallisticScrollActivity.applyNewDimensions
#22     ScrollPosition.applyNewDimensions
#23     _NestedScrollPosition.applyNewDimensions
#24     ScrollPosition.applyContentDimensions
#25     RenderViewport.performLayout
#26     RenderObject.layout
#27     RenderProxyBoxMixin.performLayout
#28     RenderObject.layout
#29     RenderProxyBoxMixin.performLayout
#30     RenderObject.layout
#31     RenderProxyBoxMixin.performLayout
#32     RenderObject.layout
#33     RenderProxyBoxMixin.performLayout
#34     RenderObject.layout
#35     RenderProxyBoxMixin.performLayout
#36     RenderObject.layout
#37     RenderProxyBoxMixin.performLayout
#38     RenderObject.layout
#39     RenderProxyBoxMixin.performLayout
#40     RenderObject.layout
#41     RenderProxyBoxMixin.performLayout
#42     _RenderCustomClip.performLayout
#43     RenderObject.layout
#44     RenderPadding.performLayout
#45     RenderObject.layout
#46     RenderProxyBoxMixin.performLayout
#47     RenderObject.layout
#48     RenderProxyBoxMixin.performLayout
#49     RenderObject.layout
#50     RenderSliverFixedExtentBoxAdaptor.performLayout
#51     RenderObject.layout
#52     RenderSliverEdgeInsetsPadding.performLayout
#53     _RenderSliverFractionalPadding.performLayout
#54     RenderObject.layout
#55     RenderViewportBase.layoutChildSequence
#56     RenderViewport._attemptLayout
#57     RenderViewport.performLayout
#58     RenderObject.layout
#59     RenderProxyBoxMixin.performLayout
#60     RenderObject.layout
#61     RenderProxyBoxMixin.performLayout
#62     RenderObject.layout
#63     RenderProxyBoxMixin.performLayout
#64     RenderObject.layout
#65     RenderProxyBoxMixin.performLayout
#66     RenderObject.layout
#67     RenderProxyBoxMixin.performLayout
#68     RenderObject.layout
#69     RenderProxyBoxMixin.performLayout
#70     RenderObject.layout
#71     RenderProxyBoxMixin.performLayout
#72     RenderObject.layout
#73     RenderProxyBoxMixin.performLayout
#74     _RenderCustomClip.performLayout
#75     RenderObject.layout
#76     RenderSliverFillRemainingWithScrollable.performLayout
#77     RenderObject.layout
#78     RenderViewportBase.layoutChildSequence
#79     RenderViewport._attemptLayout
#80     RenderViewport.performLayout
#81     RenderObject._layoutWithoutResize
#82     PipelineOwner.flushLayout
#83     RendererBinding.drawFrame
#84     WidgetsBinding.drawFrame
#85     RendererBinding._handlePersistentFrameCallback
#86     SchedulerBinding._invokeFrameCallback
#87     SchedulerBinding.handleDrawFrame
#88     SchedulerBinding._handleDrawFrame
#92     _invoke (dart:ui/hooks.dart:151:10)
#93     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:308:5)
#94     _drawFrame (dart:ui/hooks.dart:115:31)
(elided 3 frames from dart:async)
The _StretchController sending notification was: _StretchController()
════════════════════════════════════════════════════════════════════════════════
```

<!--
     Run `flutter analyze` and attach any output of that command below.
     If there are any analysis errors, try resolving them before filing this issue.
-->

```
Analyzing flutter_application_1...                                      

   info • Unused import: 'package:flutter/material.dart' • lib/test.dart:3:8 • unused_import
```

<!-- Finally, paste the output of running `flutter doctor -v` here. -->

```
[✓] Flutter (Channel stable, 3.0.1, on openSUSE Leap 15.3 5.3.18-150300.59.68-default,
    locale en_US.UTF-8)
    • Flutter version 3.0.1 at /home/joan/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision fb57da5f94 (7 days ago), 2022-05-19 15:50:29 -0700
    • Engine revision caaafc5604
    • Dart version 2.17.1
    • DevTools version 2.12.2

[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
    • Android SDK at /home/joan/Android/Sdk
    • Platform android-32, build-tools 32.1.0-rc1
    • Java binary at: /home/joan/App/android-studio/jre/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Chrome - develop for the web
    • Chrome at google-chrome

[✓] Linux toolchain - develop for Linux desktop
    • clang version 11.0.1
    • cmake version 3.17.0
    • ninja version 1.10.0
    • pkg-config version 0.29.2

[✓] Android Studio (version 2021.2)
    • Android Studio at /home/joan/App/android-studio
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin version 212.5744
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.57.0)
    • VS Code at /usr/share/code
    • Flutter extension version 3.25.0

[✓] Connected device (3 available)
    • sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64    • Android 12 (API
      32) (emulator)
    • Linux (desktop)              • linux         • linux-x64      • openSUSE Leap 15.3
      5.3.18-150300.59.68-default
    • Chrome (web)                 • chrome        • web-javascript • Google Chrome
      102.0.5005.61

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
```

</details>


<!--
      Consider also attaching screenshots and/or videos to better
      illustrate the issue.

      You can upload them directly on GitHub.
      Beware that video file size is limited to 10MB.
-->