Skip to content

Fix SelectableText crash with inline lambda contextMenuBuilder#184990

Merged
auto-submit[bot] merged 35 commits into
flutter:masterfrom
victorsanni:selectable-text-crash-fix
May 9, 2026
Merged

Fix SelectableText crash with inline lambda contextMenuBuilder#184990
auto-submit[bot] merged 35 commits into
flutter:masterfrom
victorsanni:selectable-text-crash-fix

Conversation

@victorsanni

@victorsanni victorsanni commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Instead of disposing and recreating the OverlayEntry when contextMenuBuilder changes (which triggered the crash during the build phase), we now update the contextMenuBuilder in place and trigger a rebuild of the existing overlay entry (by calling markNeedsBuild in ContextMenuController.show).

To avoid calling markNeedsBuild during the build phase (which is not allowed), we defer the call to showToolbar() in EditableTextState.didUpdateWidget using addPostFrameCallback.

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

@github-actions github-actions Bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. labels Apr 13, 2026
@victorsanni victorsanni added the CICD Run CI/CD label Apr 13, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enables in-place updates for context menus by making the contextMenuBuilder in TextSelectionOverlay mutable and modifying ContextMenuController to update existing overlays instead of recreating them. This change prevents crashes that occur when the menu is disposed of and rebuilt during the build phase. Review feedback suggests improving the robustness of the OverlayEntry builder by avoiding unsafe null-assertions on the static instance and ensuring that the toolbar is explicitly hidden if the context menu builder is removed during an update.

Comment on lines 78 to 82
_menuOverlayEntry = OverlayEntry(
builder: (BuildContext context) {
return capturedThemes.wrap(contextMenuBuilder(context));
return _shownInstance!._capturedThemes!.wrap(_shownInstance!._contextMenuBuilder!(context));
},
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing the static _shownInstance with the null-assertion operator (!) inside the OverlayEntry builder is potentially unsafe. If the controller is removed or the state becomes inconsistent during a rebuild cycle, this could lead to a runtime crash. It is safer to capture the instance in a local variable and provide a fallback widget (like SizedBox.shrink()) if the instance or its builder is null.

    _menuOverlayEntry = OverlayEntry(
      builder: (BuildContext context) {
        final ContextMenuController? instance = _shownInstance;
        if (instance == null || instance._contextMenuBuilder == null) {
          return const SizedBox.shrink();
        }
        return instance._capturedThemes!.wrap(instance._contextMenuBuilder!(context));
      },
    );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems reasonable... It can be reset to null in removeAny. Is there another reason why it will never be null?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the _shownInstance should never be null in the builder then I think we should probably leave an assert.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_showInstance is never null because before inserting the _menuOverlayEntry (which will call OverlayEntry.builder) into the Overlay it is set to this:

    _shownInstance = this;

I added an assert for posterity though.

Comment on lines +3421 to +3431
if (_selectionOverlay!.toolbarIsVisible) {
// We use addPostFrameCallback to avoid calling markNeedsBuild() during
// the build phase. This delays the update to the next frame, which could
// theoretically cause a 1-frame lag if the content changes drastically.
// However, for text selection menus, this is negligible.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_selectionOverlay!.showToolbar();
}
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the contextMenuBuilder is updated to null (or disabled on web), the current implementation calls showToolbar() in a post-frame callback. However, showToolbar() typically returns early if the builder is null, which means an existing toolbar will not be dismissed. If the builder becomes null, hideToolbar() should be called instead to ensure the menu is removed from the UI.

      if (_selectionOverlay!.toolbarIsVisible) {
        if (_selectionOverlay!.contextMenuBuilder == null) {
          _selectionOverlay!.hideToolbar();
        } else {
          // We use addPostFrameCallback to avoid calling markNeedsBuild() during
          // the build phase. This delays the update to the next frame, which could
          // theoretically cause a 1-frame lag if the content changes drastically.
          // However, for text selection menus, this is negligible.
          WidgetsBinding.instance.addPostFrameCallback((_) {
            if (mounted) {
              _selectionOverlay!.showToolbar();
            }
          });
        }
      }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what Gemini is talking about is a new behavior: it says that setting contextMenuBuilder to null should hide the menu, but I don't think that happened before this PR. Maybe Gemini is right, but that's probably not for this PR if I'm understanding correctly.

@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 13, 2026
@victorsanni victorsanni added the CICD Run CI/CD label Apr 13, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 13, 2026
@victorsanni victorsanni added the CICD Run CI/CD label Apr 13, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 13, 2026
@victorsanni victorsanni added the CICD Run CI/CD label Apr 13, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 13, 2026
@victorsanni victorsanni added the CICD Run CI/CD label Apr 13, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 14, 2026
@victorsanni victorsanni added the CICD Run CI/CD label Apr 14, 2026

@justinmc justinmc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@justinmc justinmc added the CICD Run CI/CD label May 4, 2026

@Renzo-Olivares Renzo-Olivares left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@LongCatIsLooong LongCatIsLooong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

// tree, and didUpdateWidget is called before layout.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && (_selectionOverlay?.toolbarIsVisible ?? false)) {
_selectionOverlay!.showToolbar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only way we can tell the toolbar to rebuild (there's no request update API or some such)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I can find, maybe SelectionOverlay.markNeedsBuild? But this is the convention.

@victorsanni victorsanni added the autosubmit Merge PR when tree becomes green via auto submit App label May 7, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 7, 2026
@auto-submit

auto-submit Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

auto label is removed for flutter/flutter/184990, Failed to enqueue flutter/flutter/184990 with HTTP 400: Pull request Required status check "Check Code Freeze" is expected..

@github-actions github-actions Bot removed the CICD Run CI/CD label May 7, 2026
@Renzo-Olivares Renzo-Olivares added the CICD Run CI/CD label May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

5 participants