Skip to content

SelectableText crashes the app if custom contextMenuBuilder is used, Null check operator used on a null value #155514

Description

@absar

Steps to reproduce

  1. Run the code sample
  2. Press the button, it will open an overlay
  3. Tap the text, it will crash

It happens on both Flutter 3.24.3 and 3.22.3. Works fine on 3.19.6

Expected results

Should show the custom context menu instead of crashing

Actual results

Crashes the app

Code sample

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

void main() => runApp(const CustomContextMenuBuilderIssue());

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

  @override
  State<CustomContextMenuBuilderIssue> createState() => _CustomContextMenuBuilderIssueState();
}

class _CustomContextMenuBuilderIssueState extends State<CustomContextMenuBuilderIssue> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Custom contextMenuBuilder')),
        body: const Center(child: TheButton()),
      ),
    );
  }
}

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

  @override
  State<TheButton> createState() => _TheButtonState();
}

class _TheButtonState extends State<TheButton> with TickerProviderStateMixin {
  late final _animationController =
      AnimationController(duration: const Duration(milliseconds: 300), vsync: this);
  bool _openInProgress = false;
  OverlayEntry? _popup;
  late ThemeData _theme;
  String _value = '0.0';

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _theme = Theme.of(context);
  }

  @override
  Widget build(BuildContext context) {
    _value = 'Tap me to crash';
    return FilledButton.tonal(onPressed: _showPopup, child: const Text('Press me'));
  }

  void _showPopup() async {
    if (_openInProgress) return;
    _openInProgress = true;
    _popup ??= OverlayEntry(builder: _popupBuilder);
    Overlay.of(context).insert(_popup!);
    await _animationController.forward(from: 0);
    setState(() {});
  }

  Widget _popupBuilder(BuildContext context) {
    return PositionedDirectional(
      bottom: 0,
      start: 0,
      end: 0,
      child: SizedBox(
        height: 322.0,
        child: Material(
          color: _theme.colorScheme.secondaryContainer,
          type: MaterialType.card,
          child: ThePopUp(initialValue: _value),
        ),
      ),
    );
  }
}

typedef PopupOnChanged = void Function(double? amount);

class ThePopUp extends StatefulWidget {
  const ThePopUp({super.key, required this.initialValue});

  final String initialValue;

  @override
  State<ThePopUp> createState() => _ThePopUpState();
}

class _ThePopUpState extends State<ThePopUp> {
  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: const BoxConstraints(maxWidth: 400),
      child: ValueDisplay(value: widget.initialValue),
    );
  }
}

class ValueDisplay extends StatelessWidget {
  const ValueDisplay({super.key, required this.value});

  final String? value;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return SizedBox(
      height: 40,
      child: FittedBox(
        fit: BoxFit.scaleDown,
        child: SelectableText(
          value.toString(),
          textAlign: TextAlign.center,
          maxLines: 1,
          style: theme.textTheme.titleLarge,
          contextMenuBuilder: _contextMenuBuilder,
        ),
      ),
    );
  }

  void _pasteFromClipboard() async {
    ContextMenuController.removeAny();
    print('Fake paste');
  }

  Widget _contextMenuBuilder(BuildContext context, EditableTextState state) {
    final List<ContextMenuButtonItem> buttonItems = state.contextMenuButtonItems;
    final int pasteButtonIndex =
        buttonItems.indexWhere((buttonItem) => buttonItem.type == ContextMenuButtonType.paste);

    if (pasteButtonIndex >= 0) {
      final pasteButton = buttonItems[pasteButtonIndex];
      buttonItems[pasteButtonIndex] = pasteButton.copyWith(onPressed: _pasteFromClipboard);
    } else {
      final pasteButton = ContextMenuButtonItem(
        onPressed: _pasteFromClipboard,
        type: ContextMenuButtonType.paste,
      );
      if (buttonItems.length > 1) {
        buttonItems[1] = pasteButton;
      } else {
        buttonItems.add(pasteButton);
      }
    }

    return AdaptiveTextSelectionToolbar.buttonItems(
      anchors: state.contextMenuAnchors,
      buttonItems: buttonItems,
    );
  }
}

Screenshots or Video

No response

Logs

Logs
======== Exception caught by widgets library =======================================================
The following _TypeError was thrown building Overlay-[LabeledGlobalKey<OverlayState>#de574](state: OverlayState#b0685(entries: [OverlayEntry#809eb(opaque: true; maintainState: false), OverlayEntry#de194(opaque: false; maintainState: true), OverlayEntry#e7ffa(opaque: false; maintainState: false)])):
Null check operator used on a null value

The relevant error-causing widget was: 
  MaterialApp MaterialApp:file:///issue_reproducibles/lib/flutter/custom_context_menu_builder_issue.dart:15:12
When the exception was thrown, this was the stack: 
#0      _OverlayEntryWidgetState.initState (package:flutter/src/widgets/overlay.dart:364:44)
#1      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5748:55)
#2      ComponentElement.mount (package:flutter/src/widgets/framework.dart:5593:5)
#3      Element.inflateWidget (package:flutter/src/widgets/framework.dart:4468:16)
#4      MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:7035:36)
#5      Element.updateChild (package:flutter/src/widgets/framework.dart:3963:18)
#6      Element.updateChildren (package:flutter/src/widgets/framework.dart:4150:32)
#7      MultiChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:7060:17)
#8      Element.updateChild (package:flutter/src/widgets/framework.dart:3941:15)
#9      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5642:16)
#10     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5780:11)
#11     Element.rebuild (package:flutter/src/widgets/framework.dart:5333:7)
#12     BuildScope._tryRebuild (package:flutter/src/widgets/framework.dart:2693:15)
#13     BuildScope._flushDirtyElements (package:flutter/src/widgets/framework.dart:2752:11)
#14     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:3048:18)
#15     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1162:21)
#16     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:468:5)
#17     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1397:15)
#18     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1318:9)
#19     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1176:5)
#20     _invoke (dart:ui/hooks.dart:312:13)
#21     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5)
#22     _drawFrame (dart:ui/hooks.dart:283:31)
====================================================================================================

Flutter Doctor output

Doctor output
[√] Flutter (Channel stable, 3.24.3, on Microsoft Windows [Version 10.0.22631.4169], locale en-US)
    • Flutter version 3.24.3 on channel stable at D:\Apps\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 2663184aa7 (11 days ago), 2024-09-11 16:27:48 -0500
    • Engine revision 36335019a8
    • Dart version 3.5.3
    • DevTools version 2.37.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work lista: error messageError messages from the Flutter frameworkc: regressionIt was better in the past than it is nowf: selectionSelectableRegion, SelectionArea, SelectionContainer, Selectable, and related APIsfound in release: 3.24Found to occur in 3.24found in release: 3.26Found to occur in 3.26frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onteam-text-inputOwned by Text Input teamtriaged-text-inputTriaged by Text Input 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