Repository navigation
Description
Steps to reproduce
- Show a
CupertinoPicker(orCupertinoDatePicker/CupertinoTimerPicker, which embed it) in a dismissible container, e.g.showCupertinoModalPopup. - Tap a non-centred item — this starts the tap-to-select scroll animation (
_kCupertinoPickerTapToScrollDuration). - 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 ofshowCupertinoModalPopupdoes 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
suryaKommana2662 commentedon Sep 2, 2026
@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.sintypitch commentedon Sep 2, 2026
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:
_handleChildTapon 3.47.2 and onmasteris 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 ofshowCupertinoModalPopupis 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:showModalBottomSheet(200 ms exit) popped right after the tap, e.g. tap a row and then the Done button,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 elementis 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
Widget test
Paste into
test/and runflutter test. Scenarios A and C fail with the assertion above, atpicker.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
flutter doctor -v
If it helps, I am happy to open a PR with the
if (!mounted) return;guard and scenario A as a regression test inpicker_test.dart.suryaKommana2662 commentedon Sep 2, 2026
@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 elementis still reproducible?sintypitch commentedon Sep 2, 2026
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._handleChildTapatpicker.dart:321, scenario B passes.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.
3. Profile mode, asserts stripped (macOS desktop,
flutter drive --profile, 3.47.2): this is theStateErrorfrom the production report, same line.3.47.2 was installed from the release tag, side by side with the 3.44.0 install used yesterday, hence
[user-branch]:The offer to send a PR with the
mountedguard and scenario A as a regression test stands.theprantadutta commentedon Sep 25, 2026
Thanks for the careful write-up — pinning it to
_handleChildTapand the deterministic sample made this quick to confirm. I reproduced the assertion in a widget test againstcupertino_uion flutter/packagesmain(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.
theprantadutta commentedon Oct 2, 2026
Quick follow-up: a review slot opened up, so flutter/packages#13012 is now out of draft and ready for review.
[cupertino_ui] Fix CupertinoPicker throwing when disposed during the …
github-actions commentedon Oct 24, 2026
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 -vand a minimal reproduction of the issue.