Skip to content

Unwanted animation in ListView.builder scroll position restoration #167799

@bbjay

Description

@bbjay

Steps to reproduce

  1. Scroll to the end of a ListView with a PageStorageKey
  2. Remove one or more items from the list
  3. Navigate away from the ListView
  4. Navigate back to the ListView so that it gets rebuilt

Expected results

The rebuilt ListView immediately appears with its last scroll position restored.

Actual results

The rebuilt ListView appears with an animation to the last scroll position (BallisticScrollActivity).

Workarounds
A workaround I've found is to call ScrollPosition.pointerScroll() after each list item removal (implemented for List Two in the sample code).
But this is harder to achieve if the list update happens in an async fashion and can't be awaited (eg. when using a database update stream).

This workaround reveals the underlying issue: ListView.builder does not update its scroll position when itemCount is reduced
and the position becomes greater than the actual maxScrollExtent. It is only updated when the position stored in its PageStorageKey is restored which then becomes visible with a BallisticScrollActivity.

Another workaround is to detect this situation in a ScrollController and move the position without animation:

Workaround with CustomScrollController
class CustomScrollController extends ScrollController {
  bool _firstChangeWithDimensionsSeen = false;

  @override
  ScrollControllerCallback? get onAttach => (position) {
    position.addListener(() => _onPositionChange(position));
  };

  void _onPositionChange(ScrollPosition position) {
    if (!_firstChangeWithDimensionsSeen && position.haveDimensions) {
      // detect restoration of a scroll position that would result in an unwanted animation
      if (position.pixels > position.maxScrollExtent) {
        position.jumpTo(position.maxScrollExtent);
      }
      _firstChangeWithDimensionsSeen = true;
    }
  }
}

However, this issue still feels like something that should be handled at the framework level and not by developers themselves.

Code sample

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

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

final globalState = {'one': Iterable<int>.generate(22).toList(), 'two': Iterable<int>.generate(22).toList()};

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Scroll position issue')),
        body: DefaultTabController(
          length: 2,
          child: Column(
            children: [
              TabBar(tabs: [Tab(child: Text('List One')), Tab(child: Text('List Two'))]),
              Expanded(child: TabBarView(children: [ItemList(title: 'one'), ItemList(title: 'two')])),
            ],
          ),
        ),
      ),
    );
  }
}

class ItemList extends StatefulWidget {
  final String title;
  const ItemList({super.key, required this.title});

  @override
  State<ItemList> createState() => _ItemListState();
}

class _ItemListState extends State<ItemList> {
  late final List<int> items;

  @override
  void initState() {
    items = globalState[widget.title]!;
    super.initState();
  }

  @override
  Widget build(BuildContext outerContext) {
    return ListView.builder(
      key: PageStorageKey(widget.title),
      primary: true,
      itemCount: items.length,
      itemBuilder:
          (context, index) => ListTile(
            title: Text(items[index].toString()),
            subtitle: Text('tap me to remove ' * 3),
            onTap:
                () => setState(() {
                  items.removeAt(index);
                  if (widget.title == 'one') return;
                  // Workaround active on list two
                  PrimaryScrollController.of(outerContext).position
                    ..pointerScroll(1)
                    ..pointerScroll(-1);
                }),
          ),
    );
  }
}

Screenshots or Video

Screenshots / Video demonstration
Scroll.position.issue.mp4

Logs

No response

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.29.3, on macOS 14.3.1 23D60 darwin-arm64, locale de-CH) [310ms]
    • Flutter version 3.29.3 on channel stable at /Users/user/fvm/versions/3.29.3
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ea121f8859 (2 weeks ago), 2025-04-11 19:10:07 +0000
    • Engine revision cf56914b32
    • Dart version 3.7.2
    • DevTools version 2.42.3

[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [349ms]
    • Android SDK at /Users/user/Library/Android/sdk
    ✗ cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/to/macos-android-setup for more details.

[✓] Xcode - develop for iOS and macOS (Xcode 15.3) [863ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15E204a
    • CocoaPods version 1.16.2

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

[✓] Android Studio (version 2024.1) [9ms]
    • 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 17.0.11+0-17.0.11b1207.24-11852314)

[✓] VS Code (version 1.97.2) [7ms]
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.108.0

[✓] Network resources [1’432ms]
    • All expected network resources are available.

! Doctor found issues in 1 category.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectf: scrollingViewports, list views, slivers, etc.found in release: 3.29Found to occur in 3.29found in release: 3.32Found to occur in 3.32frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework teamworkaround availableThere is a workaround available to overcome the issue

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions