Melhor Menu Flutter

Tempo de leitura: 17 minutes

O menu pop-up do Flutter precisava de melhorias

Eu nunca gostei do menu pop-up do Flutter. É feio. É um bloco branco no canto da tela. Tem que ser apenas um quadrado como aquele colocado em cima da AppBar? Não parece bom. Parece amador. Por que não pode se parecer mais com o menu abaixo no lado direito? Isso é um pouco melhor.

A desordem do Flutter

Outra coisa que não gosto nos menus pop-up do Flutter é a confusão. Eu odeio a ‘confusão de Flutter!’ Você sabe o que quero dizer. Eu suspeito que, quando você estava aprendendo Flutter, você também teve que se acostumar com essas longas listas verticais de parâmetros. Muitos widgets têm longas listas de parâmetros, e o widget PopupMenuBotton não é exceção.

Ele lista seus itens de menu, é claro, mas também lista as configurações de como o menu deve aparecer – a propósito, atribuindo essas configurações, você obterá a ‘melhor aparência’ como visto na captura de tela acima – pelo menos isso é prontamente acessível. No entanto, ele também pode listar suas funções anônimas, tornando a confusão ainda maior. Quero dizer, você nem vê o menu pop-up até tocar nesses três pontinhos no canto da tela, e ainda assim seu código pode ocupar metade da função build(). Veja abaixo. Não senhor! eu não gosto.

Então, sabe o que eu fiz? Adivinha o que eu fiz. Eu escrevi uma biblioteca de classes e melhorei o menu pop-up para o desenvolvimento geral de aplicativos. Isso é o que eu fiz. Quer dar uma olhada? Vamos dar uma olhada.

 

Sem imagens em movimento, sem redes sociais

Haverá arquivos gif neste artigo demonstrando aspectos do tópico em questão. No entanto, diz-se que a visualização desses arquivos gif não é possível ao ler este artigo em plataformas como Instagram ou Facebook. Eles podem sair como imagens estáticas ou simplesmente espaços reservados em branco.

Vamos começar.

Aqui está o link para o aplicativo de exemplo, que usaremos para este artigo. Na captura de tela abaixo no lado esquerdo, você vê seu widget AppBar lista mais de um widget PopupMenuButton no parâmetro, actions. Na verdade, se você tocar na legenda da imagem e observar o código no GitHub, verá que existem cinco widgets PopupMenuButton listados com o AppBar!

Existem cinco deles, e ainda assim eles não estão compondo metade do código na classe State. Há outra melhoria para você aqui! Alguns foram atribuídos a um determinado campo de propriedade ou variável, limpando um pouco o parâmetro ‘ações’. No entanto, essa não é a única razão para atribuir tal widget a uma variável de memória. Você verá em breve.

Por enquanto, é menos ‘Flutter desordem’ para distrair o desenvolvedor do que deve ser exibido no objeto State. Além disso, o código do ‘menu’ é um pouco mais modular. Em outras palavras, para os três primeiros menus pop-up (appMenu01, appMenu02 e appMenu03), o código PopupMenuButton reside em outro lugar. Duas são classes separadas e uma está em um getter. Novamente, esses arranjos têm suas vantagens individuais, e você verá isso em breve.

É um embrulho

Vamos primeiro dar uma olhada na biblioteca de classes que escrevi. Após uma inspeção mais detalhada, você verá que eu simplesmente escrevi uma classe wrapper para o widget PopupMenuButton do Flutter. Na captura de tela abaixo, você vê que essa classe se estende de um StatelessWidget (a propósito, o PopupMenuButton do Flutter é um StatefulWidget). Os parâmetros do wrapper espelham os parâmetros encontrados no próprio widget PopupMenuButton. Como uma classe wrapper, ele eventualmente instancia um objeto PopupMenuButton passando esses valores de parâmetro. Me segue até agora? É bem direto.

class PopupMenu<T> extends StatelessWidget {
  /// Supply all the properties to instantiate a custom [PopupMenuButton].
  const PopupMenu({
    super.key,
    this.items,
    this.menuEntries,
    this.itemBuilder,
    this.initialValue,
    this.onSelected,
    this.onCanceled,
    this.tooltip,
    this.elevation,
    this.padding,
    this.splashRadius,
    this.child,
    this.icon,
    this.iconSize,
    this.offset,
    this.enabled,
    this.shape,
    this.color,
    this.enableFeedback,
    this.constraints,
    this.position,
    this.inTooltip,
    this.inElevation,
    this.inPadding,
    this.inSplashRadius,
    this.inChild,
    this.inIcon,
    this.inIconSize,
    this.inOffset,
    this.inEnabled,
    this.inShape,
    this.inColor,
    this.inEnableFeedback,
    this.inConstraints,
    this.inPosition,
  });

Então, qual é a necessidade de uma classe wrapper? Vamos continuar examinando o código e descobrir. Após o parâmetro formal, position, as coisas começam a divergir tornando o que considero uma classe PopupMenu mais útil para o desenvolvimento de aplicativos. Você verá os nomes dos parâmetros formais agora começando com ‘in’. Essas são as funções ‘in-line’ que você pode usar agora em vez dos parâmetros tradicionais listados acima.

Em vez disso, você fornece essas funções ao widget PopupMenuButton. Por que funções? Haverá momentos em que um algoritmo mais elaborado será necessário para determinar a forma ou a cor do seu menu pop-up, por exemplo. Você ficará surpreso com a frequência com que esses casos surgem e agora você terá os meios para resolvê-los facilmente com a função apropriada. Acredite, vai aparecer. É uma grande ajuda.

this.inTooltip,
this.inElevation,
this.inPadding,
this.inSplashRadius,
this.inChild,
this.inIcon,
this.inIconSize,
this.inOffset,
this.inEnabled,
this.inShape,

Existe uma função para isso

No entanto, como essas funções ‘em linha’ são implementadas oferece ao desenvolvedor ainda mais opções. Vamos continuar e examinar a captura de tela abaixo. Ele exibe a parte da classe wrapper PopupMenu onde o PopupMenuButton está sendo instanciado com seus muitos parâmetros fornecidos com valores possíveis pela classe wrapper.

Você pode ver prontamente os parâmetros formais do wrapper sendo passados para o widget PopupMenuButton. Se eles são nulos, o operador ‘if null’ (??) então tenta um valor chamando essas funções ‘on’ que você ainda não viu. Onde estão as funções ‘em linha’ sobre as quais estávamos falando?

@override
 Widget build(BuildContext context) {
   Widget popupMenu = PopupMenuButton<T>(
     itemBuilder: onItemBuilder,
     initialValue: initialValue ?? onInitialValue(),
     onSelected: (T value) {
       if (onSelected == null) {
         selected(value);
       } else {
         onSelected!(value);
       }
     },
     onCanceled: () {
       if (onCanceled == null) {
         canceled();
       } else {
         onCanceled!();
       }
     },
     tooltip: tooltip ?? onTooltip(),
     elevation: elevation ?? onElevation(),
     padding: padding ?? onPadding() ?? const EdgeInsets.all(8),
     splashRadius: splashRadius ?? onSplashRadius(),
     icon: icon ?? onIcon(),
     iconSize: iconSize ?? onIconSize(),
     offset: offset ?? onOffset() ?? Offset.zero,
     enabled: enabled ?? onEnabled() ?? true,
     shape: shape ?? onShape(),
     color: color ?? onColor(),
     enableFeedback: enableFeedback ?? onEnableFeedback(),
     constraints: constraints ?? onConstraints(),
     position: position ?? onPosition() ?? PopupMenuPosition.over,
     child: child ?? onChild(),
   );

As funções ‘in’ estão em suas funções ‘on’ correspondentes! Agora, o que isso te diz? Lembre-se, as funções em linha que começam com ‘in’ permitem que o PopupMenuButton receba funções em execução como parâmetros junto com os parâmetros formais tradicionais. No entanto, se você decidir estender a classe wrapper com sua própria subclasse, poderá substituir as muitas funções ‘on’ e personalizar ainda mais seu menu pop-up. Criando uma abordagem mais modular, de alta coesão e baixo acoplamento ao fornecer um menu de aplicativo ao seu aplicativo — uma pedra angular do bom desenvolvimento de software.

Veja o que quero dizer? Lembra dos três primeiros menus pop-up (appMenu01, appMenu02 e appMenu03)? Ambos appMenu01 e appMenu03 estão em classes separadas. Veja abaixo. Cada um estende a classe, AppPopupMenu, que usa PopupMenu. A classe, AppPopupMenu, acompanha a classe, PopupMenu, no mesmo arquivo Dart e permite que o desenvolvedor altere os muitos valores de parâmetro ‘on the fly’ enquanto executa o aplicativo – esses parâmetros são finais na classe PopupMenu, mas não estão na classe AppPopupMenu . Você verá mais sobre isso em breve.

class SubClass01 extends AppPopupMenu<String> {
  SubClass01({super.key});

  /// items tem precedência sobre menuItems
  /// comente isso e veja de 5 a 8 opções.
  @override
  List<String> onItems() => [
    'Option 1',
    'Option 2',
    'Option 3',
    'Option 4',
  ];
class SubClass03<T> extends AppPopupMenu<T> {
  SubClass03({
    Key? key,
    PopupMenuItemSelected<T>? onSelected,
  }) : super(
    key: key,
    onSelected: onSelected,
  );
}

Primeiro, vamos revisar. Como você vê nas capturas de tela da classe wrapper abaixo, se as funções ‘on’ não forem substituídas em uma subclasse, elas ainda serão chamadas, mas apenas suas funções ‘in’ correspondentes, se alguma for realmente executada. Veja como isso funciona com as expressões condicionais (?:) em cada função ‘on’? Se nenhuma função ‘in-line’ for fornecida, um nulo será retornado e fornecido ao widget PopupMenuButton.

/// Texto que descreve a ação que ocorrerá quando o botão for pressionado.
String? onTooltip() => inTooltip == null ? null : inTooltip!();

/// função 'em Parâmetros'
final String? Function()? inTooltip;

/// Isso controla o tamanho da sombra abaixo do menu.
double? onElevation() => inElevation == null ? null : inElevation!();

/// função 'em Parâmetros'
final double? Function()? inElevation;

/// Em alguns casos, é útil poder definir o preenchimento como zero.
EdgeInsetsGeometry? onPadding() => inPadding == null ? null : inPadding!();

/// função 'em Parâmetros'
final EdgeInsetsGeometry? Function()? inPadding;
/// O raio de respingo. Se nulo, o raio de respingo padrão de [InkWell] ou [IconButton] é usado.
double? onSplashRadius() => inSplashRadius == null ? null : inSplashRadius!();

/// função 'em Parâmetros'
final double? Function()? inSplashRadius;

/// O widget usado para este botão.
Widget? onChild() => inChild == null ? null : inChild!();

/// função 'em Parâmetros'
final Widget? Function()? inChild;

/// O ícone é usado para este botão
Widget? onIcon() => inIcon == null ? null : inIcon!();

/// função 'em Parâmetros'
final Widget? Function()? inIcon;

Como qualquer classe wrapper deveria, ela faz todo esse trabalho para você, para que você não precise. Basta implementar o menu pop-up do seu aplicativo da maneira que você deseja e pronto. Me pega?

Alguns itens têm precedência

A propósito, você notou a função onItemBuilder() fornecida ao parâmetro ‘required’ do PopupMenuButton, itemBuilder? Eu exibi essa captura de tela novamente abaixo. Ele determina os itens de menu que aparecerão no menu pop-up – mesmo quando houver mais de uma lista disponível. Por que há mais de uma lista possível de itens de menu? Porque eu gosto de opções.

@override
  Widget build(BuildContext context) {
    Widget popupMenu = PopupMenuButton<T>(
      itemBuilder: onItemBuilder,
      initialValue: initialValue ?? onInitialValue(),
      onSelected: (T value) {
        if (onSelected == null) {
          selected(value);
        } else {
          onSelected!(value);
        }
      },
      onCanceled: () {
        if (onCanceled == null) {
          canceled();
        } else {
          onCanceled!();
        }
      },

Ler o código da função abaixo de cima para baixo lhe dará sua ordem de precedência. Em outras palavras, qualquer coisa listada no parâmetro formal, items, terá precedência sobre qualquer coisa atribuída ao getter, menuItems. É assim que está agora. Você vê, eu amo o desenvolvedor ter opções. O desenvolvedor pode fornecer mais de uma listagem envolvendo itens de menu, mas apenas uma fonte será exibida por enquanto. Talvez, com o tempo, eu tenha tudo, não importa como seja fornecido, listado em uma ordem específica. No entanto, a necessidade ainda não surgiu. De qualquer forma, as linhas de comentários no código abaixo devem dar uma ideia das diferentes listas disponíveis para o desenvolvedor.

/// Fornece os itens de menu apropriados
List<PopupMenuEntry<T>> onItemBuilder(BuildContext context) {
  List<PopupMenuEntry<T>>? menuOptions;
  // itens ou onItems()
  final List<T>? options = items ?? onItems();
  if (options != null && options.isNotEmpty) {
    menuOptions = _onItems(options).call(context);
  }
  // menuEntries
  if (menuOptions == null) {
    final entries = menuEntries;
    if (entries != null && entries.isNotEmpty) {
      menuOptions = entries;
    }
  }
  // o getter, menuItems
  if (menuOptions == null && menuItems.isNotEmpty) {
    menuOptions = menuItems;
  }
  // itemBuilder()
  menuOptions ??= itemBuilder == null ? null : itemBuilder!(context);
  return menuOptions ??= <PopupMenuEntry<T>>[];
}

Francamente, outros podem argumentar que a ordem deve ser revertida. Ou seja, as entradas items e onItems devem ter a precedência mais baixa e a entrada itemBuilder a mais alta. Se é assim que você se sente, tire uma cópia e mude! O mundo é sua ostra! Eu tenho minha própria cópia. Eu apenas pensei em compartilhar minha abordagem e dar algumas ideias para a barra de menu do seu próximo aplicativo. Legal?

Uma mistura de materiais

O último bit da classe wrapper tem o objeto de menu pop-up envolvido em um widget Material se, por exemplo, você estiver executando um AppBar em uma interface Cupertino. Você embrulha para não quebrar. Um pequeno truque simples. Novamente, a classe wrapper se preocupa com essas coisas para que você não precise. O que segue a classe na captura de tela abaixo é um mixin chamado PopupMenuButtonFunctions, que é usado pela classe AppPopupMenu para fornecer as muitas funções ‘on’ definidas nesta biblioteca de classes. O código repetido é um grande não-não em boas práticas de desenvolvimento de software – mixins são uma ferramenta útil nesse sentido.

///
mixin PopupMenuButtonFunctions<T> {
  /// Lista de itens de menu a serem exibidos no menu pop-up.
  List<T>? onItems() => [];

  /// O valor do item de menu, se houver, que deve ser destacado quando o menu for aberto.
  T? onInitialValue() => null;

  /// Chamado quando o usuário seleciona um valor do menu popup criado por este botão.
  void selected(T value) {}

  /// Chamado quando o usuário dispensa o menu popup sem selecionar um item.
  void canceled() {}

  /// Text that describes the action that will occur when the button is pressed.
  String? onTooltip() => null;

  /// Isso controla o tamanho da sombra abaixo do menu.
  double? onElevation() => null;

  /// Em alguns casos, é útil poder definir o preenchimento como zero.
  EdgeInsetsGeometry? onPadding() => null;

 

Uma abordagem mais funcional

Novamente, a classe, AppPopMenu, permite que você altere os vários valores de parâmetros ‘on the fly’ enquanto executa o aplicativo. Ele pode fazer isso porque, ao contrário da classe PopupMenu, seus muitos parâmetros formais que você vê abaixo não são propriedades de campo finais — eles podem ser reatribuídos repetidamente enquanto o aplicativo está em execução. As propriedades podem ser reatribuídas diretamente do objeto de instância: appMenu03.elevation = 14;

/// Cria um [PopupMenuButton] personalizado.
class AppPopupMenu<T> with PopupMenuButtonFunctions<T> {
  /// Forneça todas as propriedades para instanciar um [PopupMenuButton] personalizado.
  AppPopupMenu({
    this.key,
    this.items,
    this.menuEntries,
    this.itemBuilder,
    this.initialValue,
    this.onSelected,
    this.onCanceled,
    this.tooltip,
    this.elevation,
    this.padding,
    this.child,
    this.splashRadius,
    this.icon,
    this.iconSize,
    this.offset,
    this.enabled,
    this.shape,
    this.color,
    this.enableFeedback,
    this.constraints,
    this.position, // PopupMenuPosition.over ou PopupMenuPosition.under
  });
/// Chave para o PopupMenuButton
Key? key;

/// Lista opcional de itens de menu a serem exibidos no menu pop-up.
List<T>? items;

/// Lista opcional de itens de menu a serem exibidos no menu pop-up.
List<PopupMenuEntry<T>>? menuEntries;

/// substitui na subclasse
List<PopupMenuEntry<T>> get menuItems => [];

/// O construtor de itens se nenhuma Lista estiver disponível.
PopupMenuItemBuilder<T>? itemBuilder;

/// O valor do item de menu, se houver, que deve ser destacado quando o menu for aberto.
T? initialValue;

/// Chamado quando um item de menu é selecionado.
PopupMenuItemSelected<T>? onSelected;

Como você vê abaixo, seu getter, popupMenuButton, chama uma função privada chamada _popupMenuButton(). Essa função retorna um Widget que contém a classe wrapper, PopupMenu. E como a classe AppPopupMenu é explicitamente passada para esta função, você tem um menu popup disponível que pode ser alterado dinamicamente em tempo de execução: _popMenuButton<T>(this);

  /// Retorna o objeto de estado com o nome, botão.
  Widget get popupMenuButton => _popupMenuButton<T>(this);

  /// Objetos [BuildContext] são na verdade objetos [Element]. O [BuildContext]
  /// interface é usada para desencorajar a manipulação direta de objetos [Element].
  BuildContext? get context => _context;
  BuildContext? _context;
}

///
Widget _popupMenuButton<T>(AppPopupMenu<T> app) => Builder(builder: (context) {
  app._context = context;
  Widget button = PopupMenu<T>(
    key: app.key,
    items: app.items ?? app.onItems(),
    menuEntries: app.menuEntries ?? app.menuItems,
    itemBuilder: app.itemBuilder,
    initialValue: app.initialValue ?? app.onInitialValue(),
    onSelected: app.onSelected ?? app.selected,
    onCanceled: app.onCanceled ?? app.canceled,
    tooltip: app.tooltip ?? app.onTooltip(),
    elevation: app.elevation ?? app.onElevation(),
    padding: app.padding ?? app.onPadding() ?? const EdgeInsets.all(8),
    splashRadius: app.splashRadius ?? app.onSplashRadius(),
    icon: app.icon ?? app.onIcon(),
    iconSize: app.iconSize ?? app.onIconSize(),
    offset: app.offset ?? app.onOffset() ?? Offset.zero,
    enabled: app.enabled ?? app.onEnabled() ?? true,
    shape: app.shape ?? app.onShape(),
    color: app.color ?? app.onColor(),
    enableFeedback: app.enableFeedback ?? app.onEnableFeedback(),
    constraints: app.constraints ?? app.onConstraints(),
    position: app.position ?? app.onPosition() ?? PopupMenuPosition.over,
    child: app.child ?? app.onChild(),
  );

 

Com grande poder…

Agora, essa capacidade deve ser usada com cuidado. Qualquer pessoa com acesso a essa variável de instância pode manipular o menu pop-up de um aplicativo — alguém que não deveria. É verdade que a necessidade de alterar o conteúdo e o comportamento do menu pop-up do seu aplicativo não deve surgir com muita frequência. Quero dizer, o menu de um aplicativo tradicionalmente permanece estático e imutável. No entanto, se houver necessidade, a classe AppPopupMenu será uma solução rápida. Caso contrário, a classe PopupMenu deve ser sua implementação ‘go-to’.

Obter o menu

Lembre-se sempre de tocar nas imagens deste artigo para ver mais de perto. De qualquer forma, vamos voltar ao aplicativo de exemplo e falar sobre minha implementação de menu pop-up favorita: Usando um getter. Com um getter, a classe wrapper, PopupMenu, não é acessada até que o getter seja – um fato importante se você não quiser que ele seja avaliado quando seu aplicativo estiver inicializando. Alguns dos elementos que compõem seu menu pop-up podem não “estar prontos” na inicialização. Eles estarão prontos apenas mais tarde em tempo de execução. Um getter é um ótimo veículo para esses casos.

Para fins de demonstração, implementei todas as funções ‘in’ disponíveis para você, mas é claro, deve-se implementar apenas as necessárias para obter a ‘aparência’ desejada – sua aparência e comportamento desejados. É uma abordagem limpa e eficiente. Eu gosto disso.

  /// Forneça o menu com um getter.
  // Retorna o objeto de menu imutável.
  // Está listando todas as funções 'inline' disponíveis para você.
  PopupMenu<int> get appMenu02 => PopupMenu<int>(
//        items: const [1, 2],  // Descomente para ter precedência
    menuEntries: const [
      PopupMenuItem(
        value: 1,
        child: Text('Primeiro'),
      ),
      PopupMenuItem(
        value: 2,
        child: Text('Segundo'),
      ),
    ],
    onSelected: (int value) {
      InheritedData.of(context)?.data = value;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('appMenu02 option: $value'),
        ),
      );
    },
    onCanceled: () {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('appMenu02: Nada selecionado.'),
        ),
      );
    },
    inTooltip: () {
      return "Aqui vai uma dica para você.";
    },
    inElevation: () {
      return 8;
    },
    inPadding: () {
      return null;
    },
    inSplashRadius: () {
      return null;
    },
    inChild: () {
      return null;
    },
    inIcon: () {
      return null;
    },
    inIconSize: () {
      return null;
    },
    inOffset: () {
      return const Offset(0, 65);
    },
    inEnabled: () {
      return null;
    },
    inShape: () {
      return RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      );
    },
    inColor: () {
      return Colors.deepOrangeAccent;
    },
    inEnableFeedback: () {
      return null;
    },
    inConstraints: () {
      return null;
    },
    inPosition: () {
      return null;
    },
  );
}

 

Um menu simples

O quarto menu pop-up exibido neste aplicativo de exemplo (veja abaixo) tem o ícone ‘sinal de mais em um círculo’. Neste, estamos simplesmente instanciando a classe PopupMenu diretamente no parâmetro actions da AppBar. Como você pode ver destacado abaixo, quando uma opção de menu é selecionada ou não, os dois parâmetros, onSelected e onCanceled, são atribuídos a funções anônimas envolvendo um objeto SnackBar. Aos demais parâmetros formais são atribuídos valores relativos à aparência geral do menu popup quando aberto. Não é minha abordagem favorita com sua desordem, mas certamente outra opção para desenvolvedores.

// Um menu anônimo não atribuído a uma variável.
        PopupMenu<String>(
          items: const [
            'Este',
            'é',
            'legal'
          ], // Comente isto para obter as entradas abaixo.
          menuEntries: const [
            PopupMenuItem(
              child: Text('Este'),
            ),
            PopupMenuItem(
              child: Text('é'),
            ),
            PopupMenuItem(
              child: Text('também.'),
            ),
          ], // Comente tudo acima para itemBuilder's 'Só é este!' Itens.
          itemBuilder: (context) => const [
            PopupMenuItem(
              child: Text('Só'),
            ),
            PopupMenuItem(
              child: Text('é'),
            ),
            PopupMenuItem(
              child: Text('este!'),
            ),
          ],
          onSelected: (String value) {
            InheritedData.of(context)?.data = value;
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('Opção anônima: $value'),
              ),
            );
          },
          onCanceled: () => ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Anônimo: Nada selecionado'),
            ),
          ),
          tooltip: "Menu pop-up anônimo",
          elevation: 14,
          //  icon: const Icon(Icons.add_circle),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
          //  color: Colors.amber,
          position: PopupMenuPosition.under,
        ),

Na primeira classe

Em seguida, destacada abaixo está a classe, SubClass01, e é o primeiro menu popup listado no parâmetro actions do AppBar. Como você já sabe, é uma subclasse da classe AppPopupMenu, que chama a classe PopupMenu. Na captura de tela abaixo, o único valor de parâmetro passado quando instanciado é um identificador de chave. É verdade, porque ele estende a classe AppPopupMenu, você pode atribuir valores adicionais a essa instância a qualquer momento, em qualquer lugar enquanto seu aplicativo estiver em execução. No entanto, você também tem acesso ao seu objeto BuildContext.

@override
void initState() {
  super.initState();

  /// O menu do App vem de uma subclasse
  appMenu01 = SubClass01(key: const Key('SubClass01'));

  /// O menu do App vem de uma subclasse
  appMenu03 = SubClass03<String>(
      key: const Key('SubClass03'),
      onSelected: (String value) {
        InheritedData.of(appMenu03.context!)?.data = value;
        ScaffoldMessenger.of(appMenu03.context!).showSnackBar(
          SnackBar(
            content: Text('appMenu03 option: $value'),
          ),
        );
      });

Olhando para a aula em si, vemos que é muito leve em conteúdo. Você pode ver abaixo apenas a lista de itens do menu, e as duas funções ‘selecionar itens ou não’ são definidas na classe. Observe o objeto de contexto referenciado nessas duas funções. É necessário utilizar o widget SnackBar.

class SubClass01 extends AppPopupMenu<String> {
  SubClass01({super.key});

  /// items tem precedência sobre menuItems
  /// comente isso e veja de 5 a 8 opções.
  @override
  List<String> onItems() => [
    'Option 1',
    'Option 2',
    'Option 3',
    'Option 4',
  ];

  @override
  List<PopupMenuEntry<String>> get menuItems => const [
    PopupMenuItem(value: '1', child: Text('Option 5')),
    PopupMenuItem(value: '2', child: Text('Option 6')),
    PopupMenuItem(value: '3', child: Text('Option 7')),
    PopupMenuItem(value: '4', child: Text('Option 8')),
  ];

  @override
  void selected(String value) {
    InheritedData.of(context!)?.data = value;
    ScaffoldMessenger.of(context!).showSnackBar(
      SnackBar(
        content: Text('SubClass01 selected value: $value'),
      ),
    );
  }

  @override
  void canceled() => ScaffoldMessenger.of(context!).showSnackBar(
    const SnackBar(
      content: Text('SubClass01: Nada selecionado.'),
    ),
  );
}

Mantenha-o em Contexto

A única outra razão, eu acho que você usaria a classe AppPopupMenu é porque ela tem acesso a um objeto BuildContext na forma de um getter (veja abaixo) permitindo que seu menu pop-up abra telas e afins na interface do seu aplicativo.

  /// Retorna o objeto de estado com o nome, botão.
  Widget get popupMenuButton => _popupMenuButton<T>(this);

  /// Objetos [BuildContext] são na verdade objetos [Element]. O [BuildContext]
  /// interface é usada para desencorajar a manipulação direta de objetos [Element].
  BuildContext? get context => _context;
  BuildContext? _context;
}

///
Widget _popupMenuButton<T>(AppPopupMenu<T> app) => Builder(builder: (context) {
  app._context = context;
  Widget button = PopupMenu<T>(
    key: app.key,
    items: app.items ?? app.onItems(),
    menuEntries: app.menuEntries ?? app.menuItems,
    itemBuilder: app.itemBuilder,
    initialValue: app.initialValue ?? app.onInitialValue(),

De volta ao nosso aplicativo de exemplo, você pode ver a variável de instância, appMenu01, representando a classe SubClass01 que nos dá aquele feio ‘bloco branco’ de opções no arquivo gif exibido abaixo. Ele simplesmente não tem muito definido em sua definição de classe. Claro, você pode alterá-lo, pois é um AppPopuMenu.

@override
 Widget build(BuildContext context) => Scaffold(
   appBar: AppBar(
     title: const Text('Popup Menu Examples'),
     actions: [
       appMenu01.popupMenuButton,
       appMenu02, // a menu assigned to a getter instead.
       appMenu03.popupMenuButton,
       // Um menu anônimo não atribuído a uma variável.
       PopupMenu<String>(
         items: const [
           'Este',
           'é',
           'legal'
         ], // Comente isto para obter as entradas abaixo.
         menuEntries: const [
           PopupMenuItem(
             child: Text('Este'),
           ),
           PopupMenuItem(
             child: Text('é'),
           ),
           PopupMenuItem(
             child: Text('também.'),
           ),
         ], // Comente tudo acima para itemBuilder's 'Só é este!' Itens.
         itemBuilder: (context) => const [
           PopupMenuItem(
             child: Text('Só'),
           ),
           PopupMenuItem(
             child: Text('é'),
           ),
           PopupMenuItem(
             child: Text('este!'),
           ),
         ],
         onSelected: (String value) {
           InheritedData.of(context)?.data = value;
           ScaffoldMessenger.of(context).showSnackBar(
             SnackBar(
               content: Text('Opção anônima: $value'),
             ),
           );
         },

Ao contrário da classe SubClass01, a classe SubClass05 se estende diretamente da classe wrapper, PopupMenu. As capturas de tela abaixo descrevem o que foi implementado e o que foi substituído. Você verá que muitos substituídos estão apenas retornando null. Eles são substituídos para fins de demonstração e não precisam estar lá – apenas para mostrar as muitas funções ‘ligadas’ disponíveis para você.

class SubClass05 extends PopupMenu<String> {
  const SubClass05({
    Key? key,
    this.onSelect,
    this.onCancel,
  }) : super(key: key ?? const Key('SubClass05'));

  /// Quando uma opção de menu é selecionada
  final void Function(String value)? onSelect;

  /// Quando o menu é fechado sem nada selecionado
  final void Function()? onCancel;

  /// Lista de itens de menu a serem exibidos no menu pop-up.
  @override
  List<String>? onItems() => const ['Dentro', 'seu', 'próprio', 'classe'];

  /// Comente a linha onItems acima para obter essas entradas.
  @override
  List<PopupMenuEntry<String>> get menuItems => const [
    PopupMenuItem(
      child: Text('Este'),
    ),
    PopupMenuItem(
      child: Text('é'),
    ),
    PopupMenuItem(
      child: Text('outro'),
    ),
    PopupMenuItem(
      child: Text('caminho'),
    ),
  ];

  /// Comente os itens acima para que este itemBuilder retorne as opções do menu.
  @override
  List<PopupMenuEntry<String>> Function(BuildContext context)?
  get itemBuilder => (context) => const [
    PopupMenuItem(
      child: Text('Este'),
    ),
    PopupMenuItem(
      child: Text('é'),
    ),
    PopupMenuItem(
      child: Text('ainda'),
    ),
    PopupMenuItem(
      child: Text('outro'),
    ),
  ];

  /// O valor do item de menu, se houver, que deve ser destacado quando o menu for aberto.
  @override
  String? onInitialValue() => null;

  /// Chamado quando o usuário seleciona um valor do menu popup criado por este botão.
  @override
  void selected(String value) {
    onSelect?.call(value);
  }

  /// Chamado quando o usuário dispensa o menu popup sem selecionar um item.
  @override
  void canceled() {
    onCancel?.call();
  }

  /// Texto que descreve a ação que ocorrerá quando o botão for pressionado.
  @override
  String? onTooltip() => "As funções 'on' são implementadas nesta classe.";

  /// Isso controla o tamanho da sombra abaixo do menu.
  @override
  double? onElevation() => 12;

  /// Em alguns casos, é útil poder definir o preenchimento como zero.
  @override
  EdgeInsetsGeometry? onPadding() => const EdgeInsets.all(16);

  /// O raio de respingo. Se nulo, o raio de respingo padrão de [InkWell] ou [IconButton] é usado.
  @override
  double? onSplashRadius() => null;

  /// O widget usado para este botão
  @override
  Widget? onChild() => null;

  /// O ícone é usado para este botão
  @override
  Widget? onIcon() => null;

  /// o tamanho do [Ícone].
  @override
  double? onIconSize() => null;

  /// O deslocamento é aplicado em relação à posição inicial
  @override
  Offset? onOffset() => const Offset(0, 85);

  /// Se este botão do menu popup é interativo
  @override
  bool? onEnabled() => true;

  /// A forma usada para o menu
  @override
  ShapeBorder? onShape() => RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(20),
  );

  /// A cor de fundo usada para o menu
  @override
  Color? onColor() => Colors.yellow;

  /// Se os gestos detectados devem fornecer feedback acústico e/ou tátil
  @override
  bool? onEnableFeedback() => null;

  /// Se os gestos detectados devem fornecer feedback acústico e/ou tátil
  @override
  BoxConstraints? onConstraints() => null;

  /// Se o menu está posicionado sobre ou sob o botão do menu pop-up
  @override
  PopupMenuPosition? onPosition() => null;
}

 

Um Menu Dinâmico

O último exemplo de menu também estende a classe AppPopupMenu. É instanciado na captura de tela abaixo, e você pode dizer que estende a classe AppPopupMenu porque seu getter, contexto, é acessado em sua função anônima ‘onSelected’.

/// O menu do App vem de uma subclasse
   appMenu03 = SubClass03<String>(
       key: const Key('SubClass03'),
       onSelected: (String value) {
         InheritedData.of(appMenu03.context!)?.data = value;
         ScaffoldMessenger.of(appMenu03.context!).showSnackBar(
           SnackBar(
             content: Text('appMenu03 option: $value'),
           ),
         );
       });

As próximo Dart,  mostram as propriedades mutáveis da variável de instância com valores atribuídos. Acontece que tudo isso está sendo feito em uma função initState(). É concebível que, dependendo do escopo da variável, você possa alterá-las no momento, local e circunstância apropriados.

// Comente para ver as opções do menu, 'Olha isso aqui!', abaixo.
    appMenu03.menuEntries = const [
      PopupMenuItem(
        value: '1',
        child: Text('Olha'),
      ),
      PopupMenuItem(
        value: '2',
        child: Text('isso'),
      ),
      PopupMenuItem(
        value: '3',
        child: Text('aqui!'),
      ),
    ];

    // Isso tem a menor precedência entre os itens de menu fornecidos.
    // Deve ter mais precedência? Você me diz.
    appMenu03.itemBuilder = (BuildContext context) => const [
      PopupMenuItem(
        value: '4',
        child: Text('Olhar'),
      ),
      PopupMenuItem(
        value: '5',
        child: Text('no'),
      ),
      PopupMenuItem(
        value: '6',
        child: Text('naquele!'),
      ),
    ];

    appMenu03.onCanceled = () {
      ScaffoldMessenger.of(appMenu03.context!).showSnackBar(
        const SnackBar(
          content: Text('appMenu03: Nada selecionado.'),
        ),
      );
    };
    //
    appMenu03.tooltip = "Aqui está uma dica para você.";
    appMenu03.elevation = 14;
    appMenu03.padding = const EdgeInsets.all(8);
    appMenu03.icon = const Icon(Icons.settings);
    appMenu03.shape =
        RoundedRectangleBorder(borderRadius: BorderRadius.circular(8));

    /// A subclass of the class, PopupMenu.
    appMenu05 = SubClass05(
        onSelect: (String value) {
          InheritedData.of(context)?.data = value;
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('appMenu05 option: $value'),
            ),
          );
        },
        onCancel: () => ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('appMenu05: Nothing selected.'),
          ),
        ));
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: const Text('Popup Menu Examples'),
      actions: [
        appMenu01.popupMenuButton,
        appMenu02, // a menu assigned to a getter instead.
        appMenu03.popupMenuButton,
        // Um menu anônimo não atribuído a uma variável.

Em muitos casos, um menu pop-up fornece grande parte da funcionalidade de um aplicativo. Inicialmente, fora de vista, um usuário toca em uma barra de aplicativos para revelar um menu pop-up – um menu pop-up possivelmente cheio de opções. Depois que uma opção é selecionada, ela fica fora de vista novamente. Uma interface muito eficaz. Agora você tem uma biblioteca de classes para fornecer este menu pop-up – espero que seja igualmente eficaz.

 

Código fonte no Github -> Fonte