Skip to content

Instantly share code, notes, and snippets.

@lsaudon
Last active January 24, 2025 11:23
Show Gist options
  • Select an option

  • Save lsaudon/0091b57302c3700651ad6ccf87dbfcf4 to your computer and use it in GitHub Desktop.

Select an option

Save lsaudon/0091b57302c3700651ad6ccf87dbfcf4 to your computer and use it in GitHub Desktop.
Simplest Dropdown Menu in Flutter
import 'package:flutter/material.dart';
class DropdownButtonRaw<T> extends StatefulWidget {
const DropdownButtonRaw({
super.key,
required this.items,
required this.value,
required this.onChanged,
});
final Map<T, Widget> items;
final T value;
final ValueChanged<T> onChanged;
@override
State<DropdownButtonRaw<T>> createState() => _DropdownButtonRawState<T>();
}
class _DropdownButtonRawState<T> extends State<DropdownButtonRaw<T>> {
final _rootKey = GlobalKey();
final _controller = OverlayPortalController();
@override
Widget build(final BuildContext context) {
final items = widget.items;
final item = items[widget.value];
return InkWell(
key: _rootKey,
onTap: _controller.show,
child: OverlayPortal(
controller: _controller,
overlayChildBuilder: (final _) => _DropdownMenu<T>(
rootKey: _rootKey,
controller: _controller,
children: items.entries
.map(
(final e) => _DropdownMenuItem(
controller: _controller,
item: e,
onChanged: widget.onChanged,
),
)
.toList(),
),
child: item,
),
);
}
}
class _DropdownMenu<T> extends StatelessWidget {
const _DropdownMenu({
required this.rootKey,
required this.controller,
required this.children,
});
final GlobalKey rootKey;
final OverlayPortalController controller;
final List<_DropdownMenuItem<T>> children;
@override
Widget build(final BuildContext context) => TapRegion(
onTapOutside: (final _) => controller.hide(),
groupId: rootKey,
child: _DropdownMenuLayout(rootKey: rootKey, children: children),
);
}
class _DropdownMenuItem<T> extends StatelessWidget {
const _DropdownMenuItem({
super.key,
required this.controller,
required this.item,
required this.onChanged,
});
final OverlayPortalController controller;
final MapEntry<T, Widget> item;
final ValueChanged<T> onChanged;
@override
Widget build(final BuildContext context) => InkWell(
onTap: () {
onChanged(item.key);
controller.hide();
},
child: Align(alignment: Alignment.centerLeft, child: item.value),
);
}
class _DropdownMenuLayout extends StatelessWidget {
const _DropdownMenuLayout({required this.rootKey, required this.children});
final GlobalKey rootKey;
final List<Widget> children;
@override
Widget build(final BuildContext context) {
final rootContext = rootKey.currentContext!;
final topLeft =
(rootContext.findRenderObject()! as RenderBox).localToGlobal(
Offset.zero,
ancestor: Overlay.of(rootContext).context.findRenderObject(),
);
return CustomSingleChildLayout(
delegate: _DropdownMenuPositionDelegate(topLeft: topLeft),
child: IntrinsicWidth(
child: Material(
child: Column(mainAxisSize: MainAxisSize.min, children: children),
),
),
);
}
}
class _DropdownMenuPositionDelegate extends SingleChildLayoutDelegate {
const _DropdownMenuPositionDelegate({required this.topLeft});
final Offset topLeft;
@override
BoxConstraints getConstraintsForChild(final BoxConstraints constraints) =>
BoxConstraints(
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
);
@override
Offset getPositionForChild(final Size size, final Size childSize) => topLeft;
@override
bool shouldRelayout(final _DropdownMenuPositionDelegate oldDelegate) =>
oldDelegate.topLeft != topLeft;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment