Skip to content

ReorderableList: gap flips on every pointer move when the dragged item is taller than the item above it #194436

Closed

Description

@valentynapolienova

Steps to reproduce

  1. Build a ReorderableListView (or SliverReorderableList) whose items have different heights, for example [68, 85, 126, 97, 54, 106, 110, 119].
  2. Long-press the item of height 106 (index 5) and drag it up, slowly, over the item of height 54 (index 4) above it.
  3. Keep the finger moving slowly while the top of the dragged item is in the upper half of item 4 (a band of about 26 px here).

The widget test below reproduces it deterministically.

Expected results

Item 4 slides down once and stays down while the finger rests or creeps inside that band. Each new position of the finger gives the same gap position as the previous check.

Actual results

Item 4 flips between its resting position and its pushed-down position on every pointer move, however small (0.5 px in the test). On a device this looks like the rows below the dragged item flickering, and any app reacting to the gap moving (for example with a haptic) fires repeatedly.

The gap calculation in SliverReorderableListState._dragUpdateItems is not idempotent in this situation:

  • In state "gap above item 4" (item 4 shifted down by the drag extent), nothing in the loop matches except the dragged item's own slot, whose branch if (item.index == _dragIndex!) { if (itemMiddle <= proxyItemEnd && proxyItemEnd <= itemEnd) newIndex = _dragIndex!; } sets the gap back to the dragged index. Item 4 moves back up.
  • In state "gap in own slot", item 4 is at rest and its branch itemStart <= proxyItemStart && proxyItemStart <= itemMiddle moves the gap above item 4 again.

Both branches match whenever the dragged item is taller than the item above it: the proxy's top is in that item's upper half while the proxy's bottom is still in the lower half of its own slot. The band is [max(top_k, top_k + h - g/2), top_k + h/2] for a dragged extent g and an upper neighbour of height h, so it never occurs with equal-height items, which is probably why it went unnoticed.

The own-slot branch was added in #151026 (for #150843). Since #172380, _dragEnd already treats _insertIndex == item.index + 1 as "returned to the start", and _handleReorderItem already normalises newIndex > oldIndex, so that branch seems redundant now. Skipping the dragged item in the loop for the forward direction, as the reverse branch already does (if (item.index == _dragIndex! || !item.mounted) continue;), fixes the flip, and the existing regression tests for #150843, #88331 and #110949 still pass with that change (checked on 3.44.1).

Still present on master as of today.

Code sample

Widget test (uses only `package:flutter`)
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('ReorderableListView gap flips on every move over a shorter item',
      (tester) async {
    const heights = <double>[68, 85, 126, 97, 54, 106, 110, 119];
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: ReorderableListView.builder(
            itemCount: heights.length,
            itemBuilder: (context, i) => SizedBox(
              key: ValueKey(i),
              height: heights[i],
              child: Text('Row $i'),
            ),
            onReorder: (a, b) {},
          ),
        ),
      ),
    );
    final row4 = find.byKey(const ValueKey(4));

    // Pick up row 5 (106 px) and move it up into the top half of row 4 (54 px).
    final gesture = await tester.startGesture(
      tester.getCenter(find.byKey(const ValueKey(5))),
    );
    await tester.pump(kLongPressTimeout + const Duration(milliseconds: 50));
    await tester.pumpAndSettle();
    await gesture.moveBy(const Offset(0, -48.5));
    await tester.pumpAndSettle();
    final pushedDown = tester.getTopLeft(row4).dy; // 482: row 4 moved aside.

    // Creep upwards half a pixel at a time: row 4 should stay where it is.
    for (var move = 0; move < 8; move++) {
      await gesture.moveBy(const Offset(0, -0.5));
      await tester.pumpAndSettle();
      expect(tester.getTopLeft(row4).dy, pushedDown, reason: 'move $move');
    }

    await gesture.up();
    await tester.pumpAndSettle();
  });
}

Screenshots or Video

Not available, see the logged positions below.

Logs

Row 4's top edge after each 0.5 px move, printed from the test above (resting top is 376, pushed-down top is 482):

at rest:       row4 top = 376.0
after -48.5:   row4 top = 482.0
after each 0.5 px move: [376.0, 482.0, 376.0, 482.0, 376.0, 482.0, 376.0, 482.0]

The test fails on its first iteration:

Expected: <482.0>
  Actual: <376.0>
move 0

Flutter Doctor output

flutter doctor -v
[✓] Flutter (Channel stable, 3.44.1, on macOS 26.6.2 25G83 darwin-arm64, locale en-UA)
    • Flutter version 3.44.1 on channel stable
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 924134a44c (2026-05-29 12:13:22 -0400)
    • Engine revision c416acfeb8
    • Dart version 3.12.1
    • DevTools version 2.57.0

[!] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
    • Platform android-37.0, build-tools 36.1.0
    • Java version OpenJDK Runtime Environment (build 21+35-2513)
    ! Some Android licenses not accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 27.0)
    • Build 27A266a
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web

[✓] Connected device (3 available)
    • iPhone (mobile) • ios • iOS 27.0.1 24A446
    • macOS (desktop) • darwin-arm64 • macOS 26.6.2 25G83
    • Chrome (web) • Google Chrome 155.0.8059.41

[✓] Network resources

Activity

  1. suryaKommana2662 commented on Oct 15, 2026

    @suryaKommana2662
    Contributor

    @valentynapolienova Thanks for report. I was able to reproduce the issue locally using the provided widget test. The test fails on the first 0.5-pixel movement: Row 4's top position changes from the pushed-down position (482.0) back to its original position (376.0), matching the reported behavior. routing to the team-framework for further investigation.

    suryakommana@SuryaKommanas-MacBook-Pro counter % flutter test test/widget_test.dart     
    00:03 +0: ReorderableListView gap flips on every move over a shorter item                                                                                             
    ══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
    The following TestFailure was thrown running a test:
    Expected: <482.0>
      Actual: <376.0>
    move 0
    
    When the exception was thrown, this was the stack:
    #4      main.<anonymous closure> (file:///Users/suryakommana/StudioProjects/counter/test/widget_test.dart:40:11)
    <asynchronous suspension>
    #5      testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:192:15)
    <asynchronous suspension>
    #6      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1953:5)
    <asynchronous suspension>
    <asynchronous suspension>
    (elided one frame from package:stack_trace)
    
    This was caught by the test expectation on the following line:
      file:///Users/suryakommana/StudioProjects/counter/test/widget_test.dart line 40
    The test description was:
      ReorderableListView gap flips on every move over a shorter item
    ════════════════════════════════════════════════════════════════════════════════════════════════════
    00:03 +0 -1: ReorderableListView gap flips on every move over a shorter item [E]                                                                                      
      Test failed. See exception logs above.
      The test description was: ReorderableListView gap flips on every move over a shorter item
      
    
    To run this test again: /Users/suryakommana/develop/flutter/bin/cache/dart-sdk/bin/dart test /Users/suryakommana/StudioProjects/counter/test/widget_test.dart -p vm --plain-name 'ReorderableListView gap flips on every move over a shorter item'
    00:03 +0 -1: Some tests failed.                                                                                                                                       

    Flutter Doctor Output

    [✓] Flutter (Channel stable, 3.47.7, on macOS 27.0 26A428 darwin-arm64, locale en-IN) [339ms]
        • Flutter version 3.47.7 on channel stable at /Users/suryakommana/develop/flutter
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision abaf9c5237 (7 days ago), 2026-10-08 09:51:21 -0700
        • Engine revision deb287481e
        • Dart version 3.13.5
        • DevTools version 2.60.0
        • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
          enable-record-use, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1,530ms]
        • Android SDK at /Users/suryakommana/Library/Android/sdk
        • Emulator version 37.1.11.0 (build_id 15917651) (CL:N/A)
        • Platform android-37.0, build-tools 36.0.0
        • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
          This is the JDK bundled with the latest Android Studio installation on this machine.
          To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
        • Java version OpenJDK Runtime Environment (build 25.0.2+-15348964-b329.117)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 27.0) [772ms]
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 27A266a
        • CocoaPods version 1.17.0
    
    [✓] Chrome - develop for the web [6ms]
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Connected device (5 available) [6.1s]
        • sdk gphone16k arm64 (mobile)          • emulator-5554                        • android-arm64  • Android 17 (API 37) (emulator)
        • iPhone 18 Pro (mobile)                • D80F2104-E911-4251-BEDD-E67D9B7EBF58 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-27-0 (simulator)
        • iPad Pro 13-inch (M5) (16GB) (mobile) • 818164C9-D191-4401-BDB3-24008DE30CEF • ios            • com.apple.CoreSimulator.SimRuntime.iOS-27-0 (simulator)
        • macOS (desktop)                       • macos                                • darwin-arm64   • macOS 27.0 26A428 darwin-arm64
        • Chrome (web)                          • chrome                               • web-javascript • Google Chrome 155.0.8059.79
    
    [✓] Network resources [470ms]
        • All expected network resources are available.
    
    • No issues found!
  2. added
    p: material_uimaterial_ui package in flutter/packages
    packageflutter/packages repository. See also p: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-designOwned by Design Languages team
    a: tests"flutter test", flutter_test, or one of our tests
    frameworkflutter/packages/flutter repository. See also f: labels.
    and removed
    in triagePresently being triaged by the triage team
    p: material_uimaterial_ui package in flutter/packages
    packageflutter/packages repository. See also p: labels.
    team-designOwned by Design Languages team
    on Oct 15, 2026
  3. added
    P2Important issues not at the top of the work list
    f: scrollingViewports, list views, slivers, etc.
    P1High-priority issues at the top of the work list
    and removed
    P2Important issues not at the top of the work list
    on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

P1High-priority issues at the top of the work lista: tests"flutter test", flutter_test, or one of our testsf: scrollingViewports, list views, slivers, etc.frameworkflutter/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 team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions