Skip to content

CupertinoPicker tap-to-select throws "Bad state: No element" when the picker is disposed during the tap animation #192135

Closed
flutter/packages
#13012

Description

@sintypitch

Steps to reproduce

  1. Show a CupertinoPicker (or CupertinoDatePicker / CupertinoTimerPicker, which embed it) in a dismissible container, e.g. showCupertinoModalPopup.
  2. Tap a non-centred item — this starts the tap-to-select scroll animation (_kCupertinoPickerTapToScrollDuration).
  3. Dispose the picker before the animation finishes, i.e. within 300 ms of the tap. A parent that hides the picker on selection does this deterministically (sample below); a Material showModalBottomSheet (200 ms exit) popped right after the tap does too. Tapping the barrier of showCupertinoModalPopup does NOT: its 335 ms exit outlives the animation.

Edited 2026-09-02: the original step 3 and code sample used the showCupertinoModalPopup barrier, which cannot trigger this. See the comment below for the timing analysis and a widget test.

Expected results

The picker is disposed; the pending tap animation is abandoned silently.

Actual results

An unhandled StateError from the framework's own tap handler, reported through PlatformDispatcher.onError:

StateError: Bad state: No element
  at _CupertinoPickerState._handleChildTap (picker.dart:321)
  at FixedExtentScrollController.selectedItem (list_wheel_scroll_view.dart:268)
  at ScrollController.position (scroll_controller.dart:173)
  at List.single (growable_array.dart:362)

No application frames — the async continuation runs after the widget tree that hosted the picker is gone.

Root cause

_CupertinoPickerState._handleChildTap (introduced with tap-to-select in #153386, first shipped in 3.27.0) awaits the scroll animation and then reads the controller without checking that the state is still mounted:

Future<void> _handleChildTap(int index) async {
  _enableHapticFeedback = false;
  await _effectiveController.animateToItem(
    index,
    duration: _kCupertinoPickerTapToScrollDuration,
    curve: _kCupertinoPickerTapToScrollCurve,
  );
  _enableHapticFeedback = true;
  _lastHapticIndex = _effectiveController.selectedItem;   // ← throws
}

If the picker is disposed during the awaited animation, the controller has no attached
positions, so selectedItem → ScrollController.position → _positions.single throws
Bad state: No element. When the picker created its own internal controller the
controller is also already disposed at that point.

Suggested fix: guard the post-await section, e.g.

  if (!mounted) {
    return;
  }

(or additionally _effectiveController.hasClients) before touching
_effectiveController again.

Still present on master as of 2026-09-01
(packages/flutter/lib/src/cupertino/picker.dart).

Code sample

Code sample
// Minimal app for flutter/flutter#192135: deterministic, no human timing.
//
// "Select a value, then dismiss the picker" is a common app pattern. Here the
// parent hides the picker 50 ms after the selection changes. Tapping ANY row that
// is not already centred starts the 300 ms tap-to-select animation, the first
// onSelectedItemChanged fires ~85 ms in, and the picker is disposed ~135 ms in,
// inside the awaited animation in _CupertinoPickerState._handleChildTap.
//
// Debug:   assertion "FixedExtentScrollController.selectedItem cannot be accessed
//          before a scroll view is built with it." at picker.dart:321
// Release: StateError: Bad state: No element (same frame).
import 'dart:async';

import 'package:flutter/cupertino.dart';

void main() => runApp(const CupertinoApp(home: Home()));

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

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  bool _showPicker = true;
  Timer? _hide;

  void _onChanged(int i) {
    _hide ??= Timer(const Duration(milliseconds: 50), () {
      if (mounted) setState(() => _showPicker = false);
    });
  }

  @override
  void dispose() {
    _hide?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      child: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Padding(
              padding: EdgeInsets.all(16),
              child: Text('Tap any row that is NOT the centred one.'),
            ),
            if (_showPicker)
              SizedBox(
                height: 216,
                child: CupertinoPicker(
                  itemExtent: 32,
                  onSelectedItemChanged: _onChanged,
                  children: List<Widget>.generate(20, (i) => Center(child: Text('Item $i'))),
                ),
              )
            else
              CupertinoButton.filled(
                onPressed: () => setState(() {
                  _hide = null;
                  _showPicker = true;
                }),
                child: const Text('Show picker again'),
              ),
          ],
        ),
      ),
    );
  }
}

Repro: run the sample and tap any row that is not the centred one. The parent hides the picker 50 ms after the first onSelectedItemChanged, inside the 300 ms tap animation. Debug builds fail the selectedItem assertion at picker.dart:321; release builds throw Bad state: No element from the same line.

Logs

Logs
StateError: Bad state: No element
  File "growable_array.dart", line 362, in List.single
  File "scroll_controller.dart", line 173, in ScrollController.position
  File "list_wheel_scroll_view.dart", line 268, in FixedExtentScrollController.selectedItem
  File "picker.dart", line 321, in _CupertinoPickerState._handleChildTap

Observed in production (iOS 26.6, iPhone18,3, release AOT build) via Sentry:
20 occurrences in 46 seconds from a single user interacting with a
CupertinoDatePicker inside a modal sheet. handled: no,
mechanism: PlatformDispatcher.onError.

Flutter version

Observed on Flutter 3.44.0 (stable), Dart 3.12.0. Affects 3.27.0 (where
tap-to-select landed) through current master — master's _handleChildTap still has
no mounted guard after the await.

Activity

  1. suryaKommana2662 commented on Sep 2, 2026

    @suryaKommana2662
    Contributor

    @sintypitch I tested the provided sample. The sample builds and runs successfully on my simulator, but I was not able to reproduce the StateError: Bad state: No element after tapping a non-centered picker item and dismissing the modal during the animation on Flutter 3.47.2.

    I noticed that the issue was reported with Flutter 3.44.0. Could you please verify the issue on the latest Flutter stable version and confirm whether it is still reproducible? If it is reproducible, please provide the specific timing or interaction steps that consistently trigger the issue.

  2. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    and removed
    in triagePresently being triaged by the triage team
    on Sep 2, 2026
  3. sintypitch commented on Sep 2, 2026

    @sintypitch
    Author

    Thanks for testing. The steps as written do not reproduce it, and that is a mistake in my report, not a version difference. The code has not changed: _handleChildTap on 3.47.2 and on master is identical to 3.44 (picker.dart#L313-L322), so the race is still there.

    Why the original steps cannot trigger it. The tap-to-select animation is 300 ms (_kCupertinoPickerTapToScrollDuration), and the exit transition of showCupertinoModalPopup is 335 ms. Tapping the barrier therefore always lets the picker outlive the animation, and nothing throws. The bug needs the picker to be disposed less than 300 ms after an off-centre tap. Ordinary ways that happens in apps:

    • the parent rebuilds without the picker once a value is selected (a "select, then hide" pattern),
    • a Material showModalBottomSheet (200 ms exit) popped right after the tap, e.g. tap a row and then the Done button,
    • any route removal that runs without a transition.

    Debug vs release. In a debug build the same line fails as an assertion, FixedExtentScrollController.selectedItem cannot be accessed before a scroll view is built with it., raised from _CupertinoPickerState._handleChildTap. Bad state: No element is what the identical line throws in release, which is where the production report came from.

    Deterministic sample, no timing by hand

    Tap any row that is not the centred one. The parent hides the picker 50 ms after the first onSelectedItemChanged, which lands inside the awaited animation. Reproduced on Flutter 3.44.0 stable, iPhone 17 Pro Max simulator, debug build. I have also updated the code sample in the issue body to this one.

    lib/main.dart
    // Minimal app for flutter/flutter#192135: deterministic, no human timing.
    //
    // "Select a value, then dismiss the picker" is a common app pattern. Here the
    // parent hides the picker 50 ms after the selection changes. Tapping ANY row that
    // is not already centred starts the 300 ms tap-to-select animation, the first
    // onSelectedItemChanged fires ~85 ms in, and the picker is disposed ~135 ms in,
    // inside the awaited animation in _CupertinoPickerState._handleChildTap.
    //
    // Debug:   assertion "FixedExtentScrollController.selectedItem cannot be accessed
    //          before a scroll view is built with it." at picker.dart:321
    // Release: StateError: Bad state: No element (same frame).
    import 'dart:async';
    
    import 'package:flutter/cupertino.dart';
    
    void main() => runApp(const CupertinoApp(home: Home()));
    
    class Home extends StatefulWidget {
      const Home({super.key});
    
      @override
      State<Home> createState() => _HomeState();
    }
    
    class _HomeState extends State<Home> {
      bool _showPicker = true;
      Timer? _hide;
    
      void _onChanged(int i) {
        _hide ??= Timer(const Duration(milliseconds: 50), () {
          if (mounted) setState(() => _showPicker = false);
        });
      }
    
      @override
      void dispose() {
        _hide?.cancel();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return CupertinoPageScaffold(
          child: SafeArea(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Padding(
                  padding: EdgeInsets.all(16),
                  child: Text('Tap any row that is NOT the centred one.'),
                ),
                if (_showPicker)
                  SizedBox(
                    height: 216,
                    child: CupertinoPicker(
                      itemExtent: 32,
                      onSelectedItemChanged: _onChanged,
                      children: List<Widget>.generate(20, (i) => Center(child: Text('Item $i'))),
                    ),
                  )
                else
                  CupertinoButton.filled(
                    onPressed: () => setState(() {
                      _hide = null;
                      _showPicker = true;
                    }),
                    child: const Text('Show picker again'),
                  ),
              ],
            ),
          ),
        );
      }
    }

    Widget test

    Paste into test/ and run flutter test. Scenarios A and C fail with the assertion above, at picker.dart:321. Scenario B passes and is the barrier case you tried. The 16 ms pump cadence matters: pumpAndSettle's 100 ms default step can land the end of the tap animation and the route exit on the same frame and hide the race.

    test/picker_dispose_mid_tap_test.dart
    // Repro for flutter/flutter#192135.
    //
    // Each scenario taps an off-centre CupertinoPicker item, which starts the 300 ms
    // tap-to-select animateToItem, then disposes the picker on a tester.pump-driven
    // clock at a real 16 ms frame cadence. A scenario REPRODUCES when the test fails with
    //
    //   FixedExtentScrollController.selectedItem cannot be accessed before a scroll view is built with it.
    //   #3 _CupertinoPickerState._handleChildTap (package:flutter/src/cupertino/picker.dart:321:45)
    //
    // (debug asserts; in profile/release the same line throws `StateError: Bad state: No element`,
    // which is the production stack in the issue).
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_test/flutter_test.dart';
    
    Widget _picker() => SizedBox(
          height: 216,
          child: CupertinoPicker(
            itemExtent: 32,
            onSelectedItemChanged: (_) {},
            children: List.generate(20, (i) => Center(child: Text('Item $i'))),
          ),
        );
    
    /// Pump [ms] milliseconds in 16 ms frames (pumpAndSettle's 100 ms default step
    /// masks the race: the tap animation and the route exit can land on one frame).
    Future<void> _frames(WidgetTester tester, int ms) async {
      for (var t = 0; t < ms; t += 16) {
        await tester.pump(const Duration(milliseconds: 16));
      }
    }
    
    void main() {
      testWidgets('A. REPRODUCES: parent removes the picker 50 ms into the tap animation',
          (tester) async {
        await tester.pumpWidget(CupertinoApp(home: Center(child: _picker())));
        await tester.tap(find.text('Item 3'));
        await _frames(tester, 50);
        await tester.pumpWidget(const CupertinoApp(home: SizedBox()));
        await _frames(tester, 1000);
      });
    
      testWidgets('B. does not reproduce: showCupertinoModalPopup popped right after the tap '
          '(exit transition 335 ms > 300 ms tap animation, picker outlives the animation)',
          (tester) async {
        late BuildContext ctx;
        await tester.pumpWidget(CupertinoApp(home: Builder(builder: (c) { ctx = c; return const SizedBox(); })));
        showCupertinoModalPopup<void>(context: ctx, builder: (_) => _picker());
        await _frames(tester, 400);
        await tester.tap(find.text('Item 3'));
        await tester.pump();
        Navigator.of(ctx).pop();
        await _frames(tester, 1000);
      });
    
      testWidgets('C. REPRODUCES: Material showModalBottomSheet popped right after the tap '
          '(exit transition 200 ms < 300 ms tap animation)',
          (tester) async {
        late BuildContext ctx;
        await tester.pumpWidget(MaterialApp(home: Scaffold(body: Builder(builder: (c) { ctx = c; return const SizedBox(); }))));
        showModalBottomSheet<void>(context: ctx, builder: (_) => _picker());
        await _frames(tester, 400);
        await tester.tap(find.text('Item 3'));
        await tester.pump();
        Navigator.of(ctx).pop();
        await _frames(tester, 1000);
      });
    }
    flutter doctor -v
    [✓] Flutter (Channel stable, 3.44.0, on macOS 26.2 25C56 darwin-arm64, locale en-BE) [371ms]
        • Flutter version 3.44.0 on channel stable at /Users/anthony/development/flutter
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 559ffa3f75 (4 months ago), 2026-05-15 14:13:13 -0700
        • Engine revision 4c525dac5e
        • Dart version 3.12.0
        • DevTools version 2.57.0
        • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, no-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,228ms]
        • Android SDK at /opt/homebrew/share/android-commandlinetools
        • Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
        • Platform android-36, build-tools 36.0.0
        • ANDROID_HOME = /opt/homebrew/share/android-commandlinetools
        • Java binary at: /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/bin/java
          This JDK is specified in your Flutter configuration.
          To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
        • Java version OpenJDK Runtime Environment Homebrew (build 17.0.19+0)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 26.6) [716ms]
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 17F113
        • CocoaPods version 1.16.2
    
    [✓] Chrome - develop for the web [5ms]
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Connected device (4 available) [6.2s]
        • sdk gphone64 arm64 (mobile)  • emulator-5554             • android-arm64  • Android 16 (API 36) (emulator)
        • Phone 🌮 (wireless) (mobile) • 00008150-001654913A38401C • ios            • iOS 26.6.1 23G83
        • macOS (desktop)              • macos                     • darwin-arm64   • macOS 26.2 25C56 darwin-arm64
        • Chrome (web)                 • chrome                    • web-javascript • Google Chrome 152.0.7977.75
    
    [✓] Network resources [232ms]
        • All expected network resources are available.
    
    • No issues found!
    

    If it helps, I am happy to open a PR with the if (!mounted) return; guard and scenario A as a regression test in picker_test.dart.

  4. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Sep 2, 2026
  5. suryaKommana2662 commented on Sep 2, 2026

    @suryaKommana2662
    Contributor

    @sintypitch Thanks for providing the updated deterministic reproduction and additional details. Could you please also verify the issue on the latest Flutter stable release(3.47.2) using the updated reproduction and confirm whether the StateError: Bad state: No element is still reproducible?

  6. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    and removed
    in triagePresently being triaged by the triage team
    on Sep 2, 2026
  7. sintypitch commented on Sep 2, 2026

    @sintypitch
    Author

    Verified on Flutter 3.47.2 stable with the updated reproduction. Still reproducible, identical behaviour to 3.44.0.

    1. Widget test (flutter test, 3.47.2): scenarios A and C fail from _CupertinoPickerState._handleChildTap at picker.dart:321, scenario B passes.

    00:00 +0: A. REPRODUCES: parent removes the picker 50 ms into the tap animation
    FixedExtentScrollController.selectedItem cannot be accessed before a scroll view is built with it.
    Failed assertion: line 260 pos 7: 'positions.isNotEmpty'
    #3      _CupertinoPickerState._handleChildTap (package:flutter/src/cupertino/picker.dart:321:45)
    00:00 +0 -1: A. REPRODUCES: parent removes the picker 50 ms into the tap animation [E]
    00:00 +0 -1: B. does not reproduce: showCupertinoModalPopup popped right after the tap (exit transition 335 ms > 300 ms tap animation, picker outlives the animation)
    00:00 +1 -1: C. REPRODUCES: Material showModalBottomSheet popped right after the tap (exit transition 200 ms < 300 ms tap animation)
    FixedExtentScrollController.selectedItem cannot be accessed before a scroll view is built with it.
    Failed assertion: line 260 pos 7: 'positions.isNotEmpty'
    #3      _CupertinoPickerState._handleChildTap (package:flutter/src/cupertino/picker.dart:321:45)
    00:00 +1 -2: C. REPRODUCES: Material showModalBottomSheet popped right after the tap (exit transition 200 ms < 300 ms tap animation) [E]
    00:00 +1 -2: Some tests failed.
    

    2. iOS simulator, debug (iPhone 17 Pro Max, iOS 26.5, 3.47.2): the sample fails on the first tap of an off-centre row with the same assertion.

    FixedExtentScrollController.selectedItem cannot be accessed before a scroll view is built with it.
    'package:flutter/src/widgets/list_wheel_scroll_view.dart':
    #3      _CupertinoPickerState._handleChildTap (package:flutter/src/cupertino/picker.dart:321:45)
    

    3. Profile mode, asserts stripped (macOS desktop, flutter drive --profile, 3.47.2): this is the StateError from the production report, same line.

    00:00 +0: tap an off-centre row; the app hides the picker 50 ms after the first selection change
    VMServiceFlutterDriver: Connected to Flutter application.
    Bad state: No element
    #0      List.single (dart:core-patch/growable_array.dart:362)
    #1      ScrollController.position (package:flutter/src/widgets/scroll_controller.dart:173)
    #2      FixedExtentScrollController.selectedItem (package:flutter/src/widgets/list_wheel_scroll_view.dart:269)
    #3      _CupertinoPickerState._handleChildTap (package:flutter/src/cupertino/picker.dart:321)
    <asynchronous suspension>
    00:00 +0: tap an off-centre row; the app hides the picker 50 ms after the first selection change [E]
      Test failed. See exception logs above.
    

    3.47.2 was installed from the release tag, side by side with the 3.44.0 install used yesterday, hence [user-branch]:

    Flutter 3.47.2 • channel [user-branch] • https://github.com/flutter/flutter.git
    Framework • revision d3b14c8769 (7 days ago) • 2026-08-26 16:07:51 -0700
    Engine • hash 1cf1c4773fb941c4c74a7f8bb144a8837596c0f4 (revision a804b26164) (6 days ago) • 2026-08-26 18:46:13.000Z
    Tools • Dart 3.13.2 • DevTools 2.60.0
    

    The offer to send a PR with the mounted guard and scenario A as a regression test stands.

  8. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Sep 2, 2026
  9. added
    P2Important issues not at the top of the work list
    triaged-designTriaged by Design Languages team
    on Sep 8, 2026
  10. theprantadutta commented on Sep 25, 2026

    @theprantadutta
    Contributor

    Thanks for the careful write-up — pinning it to _handleChildTap and the deterministic sample made this quick to confirm. I reproduced the assertion in a widget test against cupertino_ui on flutter/packages main (the code moved there with the decoupling) and put up a fix in flutter/packages#13012: return early after the awaited animation if the state is no longer mounted.

    It's a draft for now only because I'm at the open-PR limit for non-committers; I'll mark it ready as soon as a slot frees.

  11. theprantadutta commented on Oct 2, 2026

    @theprantadutta
    Contributor

    Quick follow-up: a review slot opened up, so flutter/packages#13012 is now out of draft and ready for review.

  12. added a commit that references this issue on Oct 10, 2026
    8a74ca8
  13. github-actions commented on Oct 24, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  14. locked as resolved and limited conversation to collaborators on Oct 24, 2026
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 listf: cupertinop: cupertino_uicupertino_ui package in flutter/packagespackageflutter/packages repository. See also p: labels.team-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions