Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MenuAnchor] needs simpler example #148104

Open
mafreud opened this issue May 10, 2024 · 3 comments
Open

[MenuAnchor] needs simpler example #148104

mafreud opened this issue May 10, 2024 · 3 comments
Labels
c: proposal A detailed proposal for a change to Flutter d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. P2 Important issues not at the top of the work list team-design Owned by Design Languages team triaged-design Triaged by Design Languages team

Comments

@mafreud
Copy link

mafreud commented May 10, 2024

Use case

The current MenuAnchor example is very comprehensive, whereas for a beginner like me, it is very complicated. Therefore, I think more developers would use MenuAnchor if there were simpler and less time-consuming examples to understand.

Proposal

Eliminated shortcuts, enums, etc., resulting in less code.

import 'package:flutter/material.dart';

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

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

  @override
  State<MyCascadingMenu> createState() => _MyCascadingMenuState();
}

class _MyCascadingMenuState extends State<MyCascadingMenu> {
  final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');

  @override
  void dispose() {
    _buttonFocusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MenuAnchor(
      childFocusNode: _buttonFocusNode,
      menuChildren: <Widget>[
        MenuItemButton(
          child: const Text('Revert'),
          onPressed: () {},
        ),
        MenuItemButton(
          child: const Text('Setting'),
          onPressed: () {},
        ),
        MenuItemButton(
          child: const Text('Send Feedback'),
          onPressed: () {},
        ),
      ],
      builder: (_, MenuController controller, Widget? child) {
        return IconButton(
          focusNode: _buttonFocusNode,
          onPressed: () {
            if (controller.isOpen) {
              controller.close();
            } else {
              controller.open();
            }
          },
          icon: const Icon(Icons.more_vert),
        );
      },
    );
  }
}

class MenuApp extends StatelessWidget {
  const MenuApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('MenuAnchor Simple Example'),
          actions: const [
            MyCascadingMenu(),
          ],
        ),
      ),
    );
  }
}
@mafreud mafreud changed the title [MenuAnchor] needs more simple example [MenuAnchor] needs simpler example May 10, 2024
@huycozy huycozy added the in triage Presently being triaged by the triage team label May 10, 2024
@huycozy
Copy link
Member

huycozy commented May 10, 2024

Thanks for your proposal. I think we can add this as a new example on API docs. Please feel free to create a PR for this.

@huycozy huycozy added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. d: examples Sample code and demos team-design Owned by Design Languages team c: proposal A detailed proposal for a change to Flutter and removed in triage Presently being triaged by the triage team labels May 10, 2024
@mafreud
Copy link
Author

mafreud commented May 10, 2024

Sure 😊

@FireSourcery
Copy link

On the same note -

MenuAnchor is supposed to an updated PopupMenuButton
Although PopupMenu still offers an simpler way of including the menu item key/id in its callback. void Function(T value), as oppose to void Function(void).

I'd also like to consider a case of sharing the same visual menu mapped to different callbacks. For instance a right click menu whose functions are relative the object clicked.

I came up with the following solution, essentially an implementation of the flyweight pattern

// MenuSource<T> is a flyweight factory, where the menu items are shared across instances
//  either MenuItemButton callback layer indirection via context - build time
//  or instances use shallow copy - init time
class MenuSource<T> {
  MenuSource._({required this.menuItems});
  MenuSource._instance(MenuSource<T> menuSource) : menuItems = menuSource.menuItems;

  const MenuSource.items(List<MenuSourceItem> this.menuItems);
 
  MenuSource.itemBuilder({
    required Iterable<T> itemKeys,
    required Widget Function(T) itemBuilder,
    ValueSetter<T>? onPressed, 
    void Function(BuildContext context, T newValue, T oldValue)? onPressedExt,
  }) : menuItems = [
          for (final key in itemKeys)
            MenuSourceItem<T>(
              itemKey: key,
              onPressed: onPressed, 
              onPressedExt: onPressedExt,
              menuItemButton: MenuItemButton(child: itemBuilder(key)),
            ),
        ];

  final List<Widget> menuItems;

  MenuSourceInstance<T> instance() => MenuSourceInstance(this);
 
}

// alternatively shallow copy
class MenuSourceInstance<T> extends MenuSource<T> {
  MenuSourceInstance(super.menuSource) : super._instance();

  ValueNotifier<T?> notifier = ValueNotifier<T?>(null); 
}

// wrapper around MenuItemButton, to allow for a shared List<MenuItemButton> across instances
// use the same data as MenuItemButton, replacing onPressed with a callback to the notifier
// build time copy allows menuItemButton to be shared, alternatively use copyWith to create a shallow copy per instance
class MenuSourceItem<T> extends StatelessWidget {
  const MenuSourceItem({super.key, required this.menuItemButton, required this.itemKey, this.onPressed, this.onPressedExt});

  final MenuItemButton menuItemButton;
  final ValueSetter<T>? onPressed;
  final void Function(BuildContext context, T newValue, T? oldValue)? onPressedExt;
  final T itemKey;

  @override
  Widget build(BuildContext context) {
    final notifier = MenuSourceContext.of<T>(context);
    return MenuItemButton(
      onPressed: () {
        onPressed?.call(itemKey);
        onPressedExt?.call(context, itemKey, notifier.value);
        notifier.value = itemKey;
      },
      child: menuItemButton.child,
    );
  }
}

// Although MenuSource generally controls only 1 MenuListenableWidget, maps 1:1, InheritedNotifier simplifies implementation.
class MenuSourceContext<T> extends InheritedNotifier<ValueNotifier<T?>> {
  const MenuSourceContext._({super.key, required ValueNotifier<T?> super.notifier, required super.child});
  MenuSourceContext({super.key, required MenuSourceInstance<T?> source, required super.child}) : super(notifier: source.notifier);

  static ValueNotifier<T?> of<T>(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MenuSourceContext<T>>()!.notifier!;
  }
}

class MenuSourceButton<T> extends StatelessWidget {
  const MenuSourceButton({super.key, required this.source});

  final MenuSourceInstance<T> source;

  @override
  Widget build(BuildContext context) {
    return MenuSourceContext<T>(
      source: source,
      child: MenuAnchorButton(items: source.menuItems),
    );
  }
}

// Menu 'hosts' must wrap MenuAnchor under MenuSourceContext, to allow for the notifier to be accessed by the menu items
class MenuSourceWidget<T> extends StatelessWidget {
  const MenuSourceWidget({super.key, required this.source, this.child, required this.builder});

  final MenuSourceInstance<T> source;
  final ValueWidgetBuilder<T?> builder;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    // menuItems onPressed will find the notifier from MenuSourceInstance
    return MenuSourceContext<T>(
      source: source,
      // "Dependents are notified whenever the notifier sends notifications, or whenever the identity of the notifier changes."
      // not working without ValueListenableBuilder?
      child: MenuAnchorOverlay(
        items: source.menuItems,
        child: ValueListenableBuilder<T?>(valueListenable: source.notifier, builder: builder, child: child),
      ),
    );
  }
}

// case where child depends on menu without displaying the menu
class MenuListenableBuilder<T> extends StatelessWidget {
  const MenuListenableBuilder({super.key, required this.builder, required this.source, this.child});

  final MenuSourceInstance<T> source;
  final ValueWidgetBuilder<T?> builder;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<T?>(valueListenable: source.notifier, builder: builder, child: child);
  }
}

class MenuSourceTheme extends ThemeExtension<MenuSourceTheme> {
  const MenuSourceTheme({this.trailingImage});

  final ImageProvider? trailingImage;

  @override
  ThemeExtension<MenuSourceTheme> copyWith() {
    throw UnimplementedError();
  }

  @override
  ThemeExtension<MenuSourceTheme> lerp(covariant ThemeExtension<MenuSourceTheme>? other, double t) {
    throw UnimplementedError();
  }
}

@Piinks Piinks added P2 Important issues not at the top of the work list triaged-design Triaged by Design Languages team labels May 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: proposal A detailed proposal for a change to Flutter d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. P2 Important issues not at the top of the work list team-design Owned by Design Languages team triaged-design Triaged by Design Languages team
Projects
None yet
Development

No branches or pull requests

4 participants