Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Docs] Adding children outside of the cache extent to ListView does not update maxScrollExtent #94925

Open
takassh opened this issue Dec 9, 2021 · 8 comments
Labels
d: api docs Issues with https://api.flutter.dev/ d: stackoverflow Good question for Stack Overflow f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. P2 Important issues not at the top of the work list team-framework Owned by Framework team triaged-framework Triaged by Framework team

Comments

@takassh
Copy link
Contributor

takassh commented Dec 9, 2021

Steps to Reproduce

  1. Execute flutter run on the code sample
  2. Tap the FAB.
  3. You can see the viewport scrolls to right before the last item.

Expected results:

  • scrolls to the last item when the FAB is tapped.

Actual results:

  • scrolls to right before the last item when the FAB is tapped.
2021-12-09.18.45.56.mov
Code sample
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _scrollController = ScrollController();
  int _childCount = 50;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('app bar'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _childCount++;
          });

          WidgetsBinding.instance?.scheduleFrameCallback(
            (_) => WidgetsBinding.instance?.addPostFrameCallback(
              (_) => _scrollController.animateTo(
                _scrollController.position.maxScrollExtent,
                duration: const Duration(milliseconds: 200),
                curve: Curves.easeIn,
              ),
            ),
          );
        },
        child: const Icon(Icons.arrow_downward),
      ),
      body: SafeArea(
        child: RefreshIndicator(
          onRefresh: () {
            return Future.delayed(const Duration(seconds: 1));
          },
          child: ListView.builder(
            controller: _scrollController,
            padding: EdgeInsets.zero,
            itemCount: _childCount,
            itemBuilder: (context, index) {
              return ListTile(
                title: Center(
                  child: Text('ListTile $index'),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}
Logs

Analyzing flutter_pr...                                         
No issues found! (ran in 2.4s)
[✓] Flutter (Channel stable, 2.8.0, on macOS 12.0.1 21A559 darwin-x64, locale ja-JP)
    • Flutter version 2.8.0 at /Users/takashi/fvm/versions/2.8.0
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision cf44000065 (12 hours ago), 2021-12-08 14:06:50 -0800
    • Engine revision 40a99c5951
    • Dart version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at /Users/takashi/Library/Android/sdk
    • Platform android-30, build-tools 30.0.2
    • ANDROID_HOME = /Users/takashi/Library/Android
    • ANDROID_SDK_ROOT = /Users/takashi/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.10.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.2)
    • 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.8+10-b944.6916264)

[✓] VS Code (version 1.62.3)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.29.0

[✓] Connected device (2 available)
    • iPhone 11 Pro (mobile) • 1D9077E4-D995-48E1-B080-F02C395F94EC • ios            • com.apple.CoreSimulator.SimRuntime.iOS-15-0 (simulator)
    • Chrome (web)           • chrome                               • web-javascript • Google Chrome 96.0.4664.93

notice

  • if I tap the FAB during scrolling, it works.
2021-12-09.19.03.46.mov
  • if the screen is in almost bottom area, it works.
2021-12-09.19.03.12.mov

workaround

  • add jumpTo before animateTo and wrap animateTo in addPostFrameCallback
          WidgetsBinding.instance?.scheduleFrameCallback(
            (_) => WidgetsBinding.instance?.addPostFrameCallback(
              (_) {
                _scrollController.jumpTo(
                  _scrollController.offset + Tolerance.defaultTolerance.distance,
                );
                WidgetsBinding.instance?.addPostFrameCallback(
                  (_) => _scrollController.animateTo(
                    _scrollController.position.maxScrollExtent,
                    duration: const Duration(milliseconds: 200),
                    curve: Curves.easeIn,
                  ),
                );
              },
            ),
          );
  • or wrap it in while
 WidgetsBinding.instance?.scheduleFrameCallback(
                    (_) => WidgetsBinding.instance?.addPostFrameCallback(
                      (_) async {
                        while (!nearEqual(
                          _scrollController.position.maxScrollExtent,
                          _scrollController.offset,
                          Tolerance.defaultTolerance.distance,
                        )) {
                          await _scrollController.animateTo(
                            _scrollController.position.maxScrollExtent,
                            duration: const Duration(milliseconds: 200),
                            curve: Curves.linear,
                          );
                        }
                      },
                    ),
                  );
@takassh takassh changed the title AnimatedTo method scrolls to right before the last item. AnimateTo method scrolls to right before the last item. Dec 9, 2021
@maheshmnj maheshmnj added the in triage Presently being triaged by the triage team label Dec 9, 2021
@maheshmnj
Copy link
Member

maheshmnj commented Dec 9, 2021

Hi @takassh, I can reproduce the issue on stable and the master channel, on dynamically updating the list, the list always scrolls to the previous maxExtent (list before item was added), The result is similar when scroll is scheduled withpostFrameCallBack or with some Delay using Future.delayed. I am not sure if this is intended the output is similar with jumpTo. And if the list is scrolling and fab is pressed it does scroll to the bottom of the list (list after a new item was added).

flutter doctor -v
[✓] Flutter (Channel stable, 2.8.0, on macOS 12.0.1 21A559 darwin-arm, locale en-GB)
    • Flutter version 2.8.0 at /Users/mahesh/Documents/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision cf44000065 (15 hours ago), 2021-12-08 14:06:50 -0800
    • Engine revision 40a99c5951
    • Dart version 2.15.0

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/mahesh/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • ANDROID_HOME = /Users/mahesh/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.10.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.2)
    • 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.8+10-b944.6916264)

[✓] IntelliJ IDEA Community Edition (version 2021.2.1)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin version 60.1.4
    • Dart plugin version 212.5080.8

[✓] VS Code (version 1.61.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.29.0

[✓] Connected device (3 available)
    • iPhone 11 (mobile) • FC644E6C-7F02-40E5-9998-1F0CD162FBD8 • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-14-5 (simulator)
    • macOS (desktop)    • macos                                • darwin-arm64   • macOS 12.0.1 21A559 darwin-arm
    • Chrome (web)       • chrome                               • web-javascript • Google Chrome 96.0.4664.93

• No issues found!
[✓] Flutter (Channel master, 2.6.0-12.0.pre.975, on macOS 12.0.1 21A559 darwin-arm, locale en-GB)
    • Flutter version 2.6.0-12.0.pre.975 at /Users/mahesh/Documents/flutter_master
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 9b1bbdbd16 (8 hours ago), 2021-12-09 00:04:07 -0500
    • Engine revision 30b0105c72
    • Dart version 2.16.0 (build 2.16.0-85.0.dev)
    • DevTools version 2.9.1

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/mahesh/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • ANDROID_HOME = /Users/mahesh/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6916264)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.10.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.2)
    • 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.8+10-b944.6916264)

[✓] IntelliJ IDEA Community Edition (version 2021.2.1)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin version 60.1.4
    • Dart plugin version 212.5080.8

[✓] VS Code (version 1.61.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.29.0

[✓] Connected device (3 available)
    • iPhone 11 (mobile) • FC644E6C-7F02-40E5-9998-1F0CD162FBD8 • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-14-5 (simulator)
    • macOS (desktop)    • macos                                • darwin-arm64   • macOS 12.0.1 21A559 darwin-arm
    • Chrome (web)       • chrome                               • web-javascript • Google Chrome 96.0.4664.93

• No issues found!
code sample
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _scrollController = ScrollController();
  int _childCount = 50;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _scrollController.addListener(() {
      print('list scrolled to ${_scrollController.position.pixels}');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('app bar'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          print(
              ' list max extent before = ${_scrollController.position.maxScrollExtent}');
          setState(() {
            _childCount++;
          });
          SchedulerBinding.instance?.addPostFrameCallback((_) {
            print(
                ' list max extent afer update = ${_scrollController.position.maxScrollExtent}');
            _scrollController.animateTo(
              _scrollController.position.maxScrollExtent,
              duration: const Duration(milliseconds: 200),
              curve: Curves.easeIn,
            );
          });
        },
        child: const Icon(Icons.arrow_downward),
      ),
      body: SafeArea(
        child: RefreshIndicator(
          onRefresh: () {
            return Future.delayed(const Duration(seconds: 1));
          },
          child: ListView.builder(
            controller: _scrollController,
            padding: EdgeInsets.zero,
            itemCount: _childCount,
            itemBuilder: (context, index) {
              return ListTile(
                title: Center(
                  child: Text('ListTile $index'),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

cc: @Piinks

@maheshmnj maheshmnj added f: scrolling Viewports, list views, slivers, etc. found in release: 2.6 Found to occur in 2.6 found in release: 2.8 Found to occur in 2.8 framework flutter/packages/flutter repository. See also f: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on and removed in triage Presently being triaged by the triage team labels Dec 9, 2021
@maheshmnj maheshmnj changed the title AnimateTo method scrolls to right before the last item. ListView AnimateTo and jumpTo do not scroll to maxExtent in a dynamic list. Dec 9, 2021
@takassh
Copy link
Contributor Author

takassh commented Dec 9, 2021

@maheshmnj
thank you for reproducing.
I didn't notice I didn't need first scheduleFrameCallback.

@takassh
Copy link
Contributor Author

takassh commented Dec 9, 2021

@maheshmnj
I don't know why but it does scroll to the bottom of the list if I counts five ListTile from the bottom in contrast to six.

2021-12-09.23.30.34.mov

@Piinks
Copy link
Contributor

Piinks commented Dec 10, 2021

Hey @takassh I think this is working as expected.

When you are tapping your button and adding a child, the maxScrollExtent does not update right away, because it does not build that new child. At the time, it is very far down the list and it is not necessary to build it. It would be expensive to build all of the things in a ListView that the user could not see.

As the ListView scrolls, (including while animating), it builds the children just as they are about to appear on screen. This is why you noticed that if you are towards the end of your list, it works as expected. The child is added, and it is within the cache range so it is built in order to be ready to come into view.

I found this case on stack overflow: https://stackoverflow.com/questions/57645089/flutter-listview-get-full-size-of-scrollcontroller-after-adding-item-to-list
It looks like someone has found adding ensureVisible works in this case.

@Piinks Piinks changed the title ListView AnimateTo and jumpTo do not scroll to maxExtent in a dynamic list. Adding children outside of the cache extent to ListView does not update maxScrollExtent Dec 10, 2021
@Piinks Piinks added waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds d: api docs Issues with https://api.flutter.dev/ d: stackoverflow Good question for Stack Overflow documentation labels Dec 10, 2021
@takassh
Copy link
Contributor Author

takassh commented Dec 10, 2021

@Piinks
Thank you for explanation!

The answer of stack overflow works!
However I found it depends on Duration and itemCount.

Please see this sample code and video.

Code sample
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class MyList extends StatefulWidget {
  const MyList({Key? key}) : super(key: key);

  @override
  _MyListState createState() => _MyListState();
}

class _MyListState extends State<MyList> {
  final ScrollController _scrollController = ScrollController();
  final lastKey = GlobalKey();
  final items = List<String>.generate(50, (i) => "Item $i"); // ← changed item length to 50

  void add() {
    setState(() {
      items.add("new Item ${items.length}");
    });
    SchedulerBinding.instance?.addPostFrameCallback((_) => scrollToEnd());
  }

  void scrollToEnd() async {
    await _scrollController.animateTo(
      _scrollController.position.maxScrollExtent,
      duration: const Duration(milliseconds: 200), // ← changed milliseconds to 200
      curve: Curves.easeOut
    );
    Scrollable.ensureVisible(lastKey.currentContext!);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('app bar'),
      ),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: items.length,
        shrinkWrap: true,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('${items[index]}'),
            key: index == items.length - 1 ? lastKey : null,
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          add();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
2021-12-10.11.16.29.mp4
  • video shows
  1. not working (200 ms)
  2. working (350 ms)
  3. somehow working (200 ms)
  4. not working (200 ms)

I think also wrapping ensureVisible in addPostFrameCallback works.

Scrollable.ensureVisible(lastKey.currentContext!);

to

SchedulerBinding.instance?.addPostFrameCallback((_) => Scrollable.ensureVisible(lastKey.currentContext!));

Do you have any advice or idea about this?

@github-actions github-actions bot removed the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Dec 10, 2021
@Piinks
Copy link
Contributor

Piinks commented Dec 10, 2021

Do you have any advice or idea about this?

I do not, stackoverflow may be a better place to ask.

@Piinks Piinks added the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Dec 10, 2021
@takassh
Copy link
Contributor Author

takassh commented Dec 11, 2021

@Piinks
Ok, thank you very much :)

@github-actions github-actions bot removed the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Dec 11, 2021
@maheshmnj
Copy link
Member

Since this is working as intended, considering this issue as a documentation request.

@maheshmnj maheshmnj added passed first triage and removed found in release: 2.6 Found to occur in 2.6 found in release: 2.8 Found to occur in 2.8 has reproducible steps The issue has been confirmed reproducible and is ready to work on labels Dec 13, 2021
@maheshmnj maheshmnj changed the title Adding children outside of the cache extent to ListView does not update maxScrollExtent [Docs] Adding children outside of the cache extent to ListView does not update maxScrollExtent Dec 13, 2021
@goderbauer goderbauer added the P2 Important issues not at the top of the work list label Feb 2, 2022
@flutter-triage-bot flutter-triage-bot bot added team-framework Owned by Framework team triaged-framework Triaged by Framework team labels Jul 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
d: api docs Issues with https://api.flutter.dev/ d: stackoverflow Good question for Stack Overflow f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels. P2 Important issues not at the top of the work list team-framework Owned by Framework team triaged-framework Triaged by Framework team
Projects
None yet
Development

No branches or pull requests

5 participants