Skip to content

Flinging in PageView depends on rendering time, leading to inconsistent behaviour #174904

Open

Description

@Eerey

Steps to reproduce

Run the example code.

Expected results

Heavy Build: disabled and Heavy Build: enabled (shown in the example) should not differ in their behaviour.

The flinging should work, regardless of long build times.

Actual results

Heavy Build: enabled flings back to original page, even when flinging with great velocity. This is undesired.

Code sample

Code sample
import 'dart:math';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PageView Bug',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
      ),
      home: ScrollConfiguration(
        behavior: ScrollConfiguration.of(context).copyWith(
          // This is required to swipe left and right on windows
          dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
        ),
        child: Scaffold(
          appBar: AppBar(
            title: Text(
              "PageView Bug - Fling not working correctly if Page takes more time to build",
            ),
          ),
          body: Column(children: [Expanded(child: Content())]),
        ),
      ),
    );
  }
}

class Content extends StatefulWidget {
  const Content({super.key});

  @override
  State<Content> createState() => _ContentState();
}

class _ContentState extends State<Content> {
  ValueNotifier<bool> heavyVn = ValueNotifier<bool>(true);

  final ValueNotifier<String> startEventVn = ValueNotifier<String>(
    "ScrollStartNotification: -",
  );
  final ValueNotifier<String> userEventVn = ValueNotifier<String>(
    "UserScrollNotification: -",
  );
  final ValueNotifier<String> updateEventVn = ValueNotifier<String>(
    "ScrollUpdateNotification: -",
  );
  final ValueNotifier<String> endEventVn = ValueNotifier<String>(
    "ScrollEndNotification: -",
  );

  void _handleScrollNotification(ScrollNotification notification) {
    if (notification is ScrollStartNotification) {
      startEventVn.value =
          "ScrollStartNotification → Dragging=${notification.dragDetails != null}";
    } else if (notification is UserScrollNotification) {
      userEventVn.value =
          "UserScrollNotification → Direction=${notification.direction}";
    } else if (notification is ScrollUpdateNotification) {
      updateEventVn.value =
          "ScrollUpdateNotification → Δ=${notification.scrollDelta?.toStringAsFixed(2)}";
    } else if (notification is ScrollEndNotification) {
      endEventVn.value = "ScrollEndNotification → ${notification.toString()}";
    }
  }

  void reset() {
    startEventVn.value = "ScrollStartNotification →";
    userEventVn.value = "UserScrollNotification →";
    updateEventVn.value = "ScrollUpdateNotification →";
    endEventVn.value = "ScrollEndNotification →";
  }

  Widget _buildEventText(ValueNotifier<String> vn) {
    return Padding(
      padding: const EdgeInsets.all(4.0),
      child: ValueListenableBuilder<String>(
        valueListenable: vn,
        builder: (context, value, _) => Text(
          value,
          style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<bool>(
      valueListenable: heavyVn,
      builder: (context, enableExpensiveBuildTime, child) => Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _buildEventText(startEventVn),
          _buildEventText(userEventVn),
          _buildEventText(updateEventVn),
          _buildEventText(endEventVn),
          Row(
            children: [
              Text("Heavy Build: ${heavyVn.value ? "enabled" : "disabled"}"),
              Checkbox(
                value: heavyVn.value,
                onChanged: (value) {
                  reset();
                  heavyVn.value = value ?? true;
                },
              ),
            ],
          ),
          const Divider(),
          Expanded(
            child: NotificationListener<ScrollNotification>(
              onNotification: (notification) {
                _handleScrollNotification(notification);
                return true;
              },
              child: FlutterPageView(heavy: enableExpensiveBuildTime),
            ),
          ),
        ],
      ),
    );
  }
}

class FlutterPageView extends StatelessWidget {
  const FlutterPageView({super.key, required this.heavy});

  final bool heavy;

  @override
  Widget build(BuildContext context) {
    // this is purposefully inefficient
    final list = List<int>.generate(
      heavy ? 5000 : 10,
      (int index) => index,
      growable: false,
    );
    return PageView.builder(
      itemBuilder: (context, index) {
        return Container(
          color: Colors.blue[(index * 100) % 1000],
          child: Stack(
            children: [
              for (final item in list)
                Positioned(
                  left: item.toDouble() * 10,
                  top: 120 + sin(item * 0.4) * 120 + item * 2.05,
                  child: Text(
                    index.toString(),
                    style: TextStyle(color: Colors.black, fontSize: item % 164),
                  ),
                ),
            ],
          ),
        );
      },
      itemCount: 20,
    );
  }
}

Screenshots or Video

page_view_corrupted.2025-09-03.22-33-46.mp4

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[√] Flutter (Channel stable, 3.35.2, on Microsoft Windows [Version 10.0.19045.3208], locale de-DE)
[√] Windows Version (10 64-Bit)
[√] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.5.2)  
[√] Android Studio (version 2024.3)
[√] VS Code, 64-bit edition (version 1.103.2)
[√] Connected device (3 available)
[√] Network resources

Activity

  1. Eerey commented on Sep 3, 2025

    @Eerey
    Author

    Just as a note:
    I upgraded from Flutter 3.22.2 to 3.35.2 and this behaviour started to appear on all components that uses a PageView.

    I wanted to implement my own ScrollPhysics to eliminate the problem. However, it appears to be impossible.

    line 941https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/page_view.dart

        final ScrollPhysics physics =
            _ForceImplicitScrollPhysics(allowImplicitScrolling: widget.allowImplicitScrolling).applyTo(
              widget.pageSnapping
                  ? _kPagePhysics.applyTo( // <- I do not want _kPagePhysics
                      widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
                    )
                  : widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
            );
    

    If I understand it correctly, it is impossible to fully control the PageView with customized ScrollPhysics, because the methods of the default _kPagePhysics will always be prioritized over those that are set in the customized widget.physics:

    Documentation of PageScrollPhysics applyTo(ScrollPhysics? ancestor)

    The returned object uses this instance's physics when it has an opinion, and defers to the given ancestor object's physics when it does not.
    

    While testing I found out that Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) is executed twice. PageScrollPhysics first, then my Customized ScrollPhysics.

    This is undesired. I feel like I have to copy the entire PageView class and remove the _kPagePhysics to fully control my PageView. Am I missing something?

  2. darshankawar commented on Sep 4, 2025

    @darshankawar
    Member

    Thanks for the detailed report. Seeing the same behavior as reported on latest sdk versions.

    stable : 3.35.2
    master : 3.36.0-1.0.pre-394
    
  3. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    f: gesturesflutter/packages/flutter/gestures repository.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    perf: speedPerformance issues related to (mostly rendering) speed
    c: performanceRelates to speed or footprint issues (see "perf:" labels)
    and removed
    in triagePresently being triaged by the triage team
    on Sep 4, 2025
  4. atototenten commented on Sep 5, 2025

    @atototenten

    this behavior is everywhere in flutter ,due to its sync. rendering pipeline .

    the item building in scrolling(including page-view) should be implicitly async. ,to disallow affecting/hijacking the other widgets .

    like a well-traded-off react-fiber feature .

    cc @Hixie

  5. Eerey commented on Sep 5, 2025

    @Eerey
    Author

    this behavior is everywhere in flutter ,due to its sync. rendering pipeline .

    Up until now I was using flutter 3.22 and this did in fact not occur to me. This is a new phenomenon. If it worked in the past, it should work in the future.

    the item building in scrolling(including page-view) should be implicitly async. ,to disallow affecting/hijacking the other widgets .

    This cannot always be guaranteed. Also, If a synced build is introduced by widgets outside of the PageView but still visible on the screen, for example a complex Rive-Animation-Widget, the problem still persists. To me it does feel like a cheap performance hack, just to make a well-known gesture work again.

    Just as a note:
    I tested this on touch devices with users and it was perceived as buggy and sluggish. Some users couldn't even fling to the next page.

  6. atototenten commented on Sep 5, 2025

    @atototenten

    If it worked in the past, it should work in the future.

    this is understandable .

    This cannot always be guaranteed.

    but this is not .im not talking about dart's async/await keywords ,but react's fiber concept .

    To me it does feel like a cheap performance hack, just to make a well-known gesture work again.

    this is also false ,and a mis-understanding .this "cheap" hack is a major feature of react ,which flutter lacks ,due to resource constraints ,and maybe politics .

  7. added
    P2Important issues not at the top of the work list
    on Oct 14, 2025
  8. kayson-argyle commented on Dec 8, 2025

    @kayson-argyle

    Any workarounds I can use or fixes in the works? I'm also experiencing this issue.

  9. Eerey commented on Dec 11, 2025

    @Eerey
    Author

    @kayson-argyle Not to my knowledge. It blocks my current migration plan, I have to stick to 3.22.

  10. deckerst commented on Mar 27, 2026

    @deckerst

    I experience the same phenomenon.

    In my case, I see a difference between Flutter versions, as OP mentioned.
    But I also see a difference whether thread merging is enabled (via the DisableMergedPlatformUIThread flag) while staying with Flutter v3.35.7:

    • with merged threads disabled, the PageView gesture works well,
    • with merged threads enabled, the PageView gesture aborts when loading the same page, but it became a "heavy build" because of threading.
  11. Eerey commented on May 12, 2026

    @Eerey
    Author

    Hi there, just as an update I pulled 3.41.9 and this behaviour is still present.

    This issue blocks us from moving from 3.22.2 to the most recent version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: performanceRelates to speed or footprint issues (see "perf:" labels)f: gesturesflutter/packages/flutter/gestures repository.found in release: 3.35Found to occur in 3.35found in release: 3.36Found to occur in 3.36frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onperf: speedPerformance issues related to (mostly rendering) speedteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions