Repository: flutter
Issue: [Hero]overflow error when Popping a route and animating back to the route in a listview
Labels: framework,f: material design,has reproducible steps,found in release: 2.8,found in release: 2.9,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. -->

1. Execute `flutter run` on the code sample <!-- (see "Code sample" section below) -->
2. Click on any card displayed in the `ListView` whose id is >= 4.
3. Pop the route.
4. Observe the overflow error on the animation back.

**Expected results:**
*I'm not sure if Flutter automatically handles a case where the `Hero` widget child is a `Column` and its contents bigger than the viewport or not.*

For now, I expect there to be no overflow error.

**Actual results:**
Overflow error when the widget animates back into the list and its contents is bigger than the viewport.

<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.
-->

```dart
import 'package:flutter/material.dart';

void main() => runApp(const BugApp());

class BugApp extends StatelessWidget {
  const BugApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: ListView.separated(
            separatorBuilder: (_, __) => const SizedBox(height: 20),
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 45),
            itemCount: 9,
            itemBuilder: (_, i) => BugAppCard(i + 1),
          ),
        ),
      ),
    );
  }
}

class BugAppCard extends StatelessWidget {
  final int i;
  const BugAppCard(this.i, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Widget _current = BugThumbnail(i,
        borderRadius: const BorderRadius.all(Radius.circular(30)));

    _current = Column(children: [_current, BugInfo(i)]);

    _current = BugHero(i, child: _current);

    _current = GestureDetector(
      child: _current,
      onTap: () => Navigator.of(context).push(MaterialPageRoute(
        builder: (context) => BugPageFull(i),
      )),
    );

    return _current;
  }
}

class BugPageFull extends StatelessWidget {
  final int i;
  const BugPageFull(this.i, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const _fakeContent = SizedBox(height: 120);
    Widget _current = BugThumbnail(i);

    _current = ListView(children: [_current, BugInfo(i), _fakeContent]);

    _current = BugHero(i, child: _current);

    return Scaffold(
      backgroundColor: BugInfo.bgColor,
      body: _current,
    );
  }
}

class BugThumbnail extends StatelessWidget {
  final int i;
  final BorderRadiusGeometry? borderRadius;
  const BugThumbnail(
    this.i, {
    Key? key,
    this.borderRadius,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text('$i', style: Theme.of(context).textTheme.headline1),
      ),
      width: double.infinity,
      height: MediaQuery.of(context).size.height - 120 + i * 20,
      decoration: BoxDecoration(
        borderRadius: borderRadius,
        color: Colors.teal[i * 100]!,
      ),
    );
  }
}

class BugInfo extends StatelessWidget {
  static final Color bgColor = Colors.blueGrey.shade900;

  final int i;
  const BugInfo(this.i, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(15),
        color: bgColor,
      ),
      padding: const EdgeInsets.fromLTRB(30, 20, 30, 20),
      child: Text(
        'ID: $i',
        style: Theme.of(context)
            .textTheme
            .bodyText2!
            .copyWith(color: Colors.white),
      ),
    );
  }
}

class BugHero extends Hero {
  const BugHero(
    int i, {
    required Widget child,
    HeroFlightShuttleBuilder? flightShuttleBuilder,
    Key? key,
  }) : super(
          key: key,
          child: child,
          tag: 'tag-$i',
          flightShuttleBuilder: flightShuttleBuilder,
        );
}
```

</details>

<details>
  <summary>Logs</summary>

<!-- Finally, paste the output of running `flutter doctor -v` here. -->

```
$ flutter doctor -v

[✓] Flutter (Channel stable, 2.8.1, on macOS 11.6.2 20G314 darwin-x64, locale en-GB)
    • Flutter version 2.8.1 at /usr/local/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 77d935af4d (10 days ago), 2021-12-16 08:37:33 -0800
    • Engine revision 890a5fca2e
    • Dart version 2.15.1

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/jellynoone/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)
    • All Android licenses accepted.

[!] Xcode - develop for iOS and macOS
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
        sudo xcodebuild -runFirstLaunch
    • CocoaPods version 1.10.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2020.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)

[✓] VS Code (version 1.64.0-insider)
    • VS Code at /Applications/Visual Studio Code - Insiders.app/Contents
    • Flutter extension version 3.29.0

[✓] Connected device (3 available)
    • Pixel (mobile)  • 192.168.0.111:5555 • android-arm64  • Android 10 (API 29)
    • macOS (desktop) • macos              • darwin-x64     • macOS 11.6.2 20G314 darwin-x64
    • Chrome (web)    • chrome             • web-javascript • Google Chrome 96.0.4664.110

! Doctor found issues in 1 category.
```

</details>


**Additional:**
I've found two workarounds. Replace the codes snippets bellow with the relevant parts from the initial example.

*Workaround 1:*
<details>
Wrap only the thumbnail in a `Hero` widget not the entire `Column` (in the home route) and `ListView` (on the `BugPageFull`).

Downside: The animation isn't as nice. (To me)

Relevant changes to the initial example:
```dart
class BugAppCard extends StatelessWidget {
  final int i;
  const BugAppCard(this.i, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Widget _current = BugThumbnail(i,
        borderRadius: const BorderRadius.all(Radius.circular(30)));

    _current = BugHero(i, child: _current); // wrap thumbnail not Column

    _current = Column(children: [_current, BugInfo(i)]);

    _current = GestureDetector(
      child: _current,
      onTap: () => Navigator.of(context).push(MaterialPageRoute(
        builder: (context) => BugPageFull(i),
      )),
    );

    return _current;
  }
}

class BugPageFull extends StatelessWidget {
  final int i;
  const BugPageFull(this.i, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    const _fakeContent = SizedBox(height: 120);
    Widget _current = BugThumbnail(i);

    _current = BugHero(i, child: _current);  // wrap thumbnail not ListView 
    // however this change isn't necessary this is not the error-causing widget

    _current = ListView(children: [_current, BugInfo(i), _fakeContent]);

    return Scaffold(
      backgroundColor: BugInfo.bgColor,
      body: _current,
    );
  }
}
```

</details>

*Workaround 2:*
<details>
Use the `Hero.flightShuttleBuilder` property to wrap the `Column` in a `SingleChildScrollView`.

Downside: *Feels* like a hack.

Relevant changes to the initial example:
```dart
class BugAppCard extends StatelessWidget {
  final int i;
  const BugAppCard(this.i, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Widget _current = BugThumbnail(i,
        borderRadius: const BorderRadius.all(Radius.circular(30)));

    _current = Column(children: [_current, BugInfo(i)]);

    // save reference to the widget using just _current would at the time of the closure call refer to the GestureDetector
    final ref = _current;
    _current = BugHero(
      i,
      child: _current,
      flightShuttleBuilder: (flightContext, animation, flightDirection,
          fromHeroContext, toHeroContext) {
        final Hero toHero = toHeroContext.widget as Hero;
        final child = toHero.child;

        // only affect this widget leave others unchanged
        if (child != ref) return child;

        return SingleChildScrollView(child: child);
      },
    );

    _current = GestureDetector(
      child: _current,
      onTap: () => Navigator.of(context).push(MaterialPageRoute(
        builder: (context) => BugPageFull(i),
      )),
    );

    return _current;
  }
}
```
</details>