Skip to main content

tldr

copy one file into your project, use it. no setup, no init call.

AppPopup(items: [...], child: Icon(Icons.more_horiz)).

anchored to the child, dividers between rows, destructive labels in the error color, light haptic on select.

live demo

starting the demo

install

copy one file

grab it and drop it in at lib/app_popup.dart.

app_popup.dart576 lines

it imports material and services from the flutter sdk. nothing goes in pubspec.yaml.

use it

there is no init call. the defaults stand on their own.

AppPopup(
  items: [
    AppPopupItem(label: "Replace", onSelected: _replace),
    AppPopupItem(
      label: "Remove",
      isDestructive: true,
      onSelected: _remove,
    ),
  ],
  child: const Icon(Icons.more_horiz),
)

usage

wrap any trigger. the menu opens on tap, anchored just below the child.

AppPopup(
  items: [
    AppPopupItem(label: "Replace exercise", onSelected: onReplace),
    AppPopupItem(
      label: "Remove exercise",
      isDestructive: true,
      onSelected: onRemove,
    ),
  ],
  child: Padding(
    padding: const EdgeInsets.all(8),
    child: Icon(Icons.more_horiz_rounded, color: muted),
  ),
)

disable a row without removing it from the list.

AppPopupItem(
  label: "Move up",
  enabled: canMoveUp,
  onSelected: onMoveUp,
)

nudge the menu with offset, or set a wider panel with minWidth.

AppPopup(
  offset: const Offset(0, 8),
  minWidth: 200,
  items: items,
  child: child,
)

example

class ExerciseRow extends StatelessWidget {
  const ExerciseRow({
    super.key,
    required this.title,
    required this.onReplace,
    required this.onRemove,
  });

  final String title;
  final VoidCallback onReplace;
  final VoidCallback onRemove;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(child: Text(title)),
        AppPopup(
          items: <AppPopupItem>[
            AppPopupItem(label: "Replace", onSelected: onReplace),
            AppPopupItem(
              label: "Remove",
              isDestructive: true,
              onSelected: onRemove,
            ),
          ],
          child: const Padding(
            padding: EdgeInsets.all(8),
            child: Icon(Icons.more_horiz_rounded),
          ),
        ),
      ],
    );
  }
}

reference

everything below is here when you need it. you can ship without reading any of it.