Flutter 封装:最佳实践 —— 下拉菜单 MenuAnchor

一、需求来源

研究源码(Flutter 3.10.6)时发现了 MenuAnchor 组件,下拉菜单的最佳实践;

效果如下:

二、使用示例

scala 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/n_menu_anchor.dart';


enum SomeItemType { none, itemOne, itemTwo, itemThree }


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

  @override
  State<MenuAnchorDemo> createState() => _MenuAnchorDemoState();
}

class _MenuAnchorDemoState extends State<MenuAnchorDemo> {

  final _selectedItemVN = ValueNotifier<SomeItemType>(SomeItemType.none);

  String defaultValue = "-";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('$widget')),
      body: buildBody(),
      // body: CheckboxMenuDemo(message: kMessage,),
    );
  }

  buildBody() {
    return Column(
      children: [
        // Spacer(),
        ValueListenableBuilder(
          valueListenable: _selectedItemVN,
          builder: (context, value, child){

            return Text(value.name ?? defaultValue);
          }
        ),
        // 直接使用
        // buildMenuAnchor<SomeItemType>(
        //   values: SomeItemType.values,
        //   initialItem: SomeItemType.itemThree,
        //   cbName: (e) => e.name,
        //   onChanged: (SomeItemType e) {
        //     debugPrint(e.name);
        //     _selectedItemVN.value = e;
        //   },
        // ),

        // 简单封装
        NMenuAnchor<SomeItemType>(
          values: SomeItemType.values,
          initialItem: SomeItemType.itemThree,
          cbName: (e) => e.name,
          onChanged: (SomeItemType e) {
            debugPrint(e.name);
            _selectedItemVN.value = e;
          },
        ),

      ],
    );
  }

  buildMenuAnchor<E>({
    required List<E> values, 
    required E initialItem,
    required String Function(E e) cbName,
    required ValueChanged<E> onChanged,
    Widget Function(MenuController controller, E? selectedItem)? itemBuilder,
  }) {
    var selectedItem = initialItem;

    return StatefulBuilder(
      builder: (BuildContext context, StateSetter setState) {

        return MenuAnchor(
          alignmentOffset: Offset(0, 0),
          builder: (context, MenuController controller, Widget? child) {

            return itemBuilder?.call(controller, selectedItem) ?? OutlinedButton(
              onPressed: (){
                if (controller.isOpen) {
                  controller.close();
                } else {
                  controller.open();
                }
              },
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(cbName(selectedItem)),
                  Icon(Icons.arrow_drop_down),
                ],
              ),
            );
          },
          menuChildren: values.map((e) {
            return MenuItemButton(
              onPressed: () {
                selectedItem = e;
                setState(() {});
                onChanged.call(e);
              },
              child: Text(cbName(e)),
            );
          }).toList(),
        );
      }
    );
  }
}

三、源码

1、NMenuAnchor 源码

kotlin 复制代码
//
//  NEnumMenuAnchor.dart
//  flutter_templet_project
//
//  Created by shang on 2023/10/31 19:29.
//  Copyright © 2023/10/31 shang. All rights reserved.
//


import 'package:flutter/material.dart';

/// MenuAnchor 简易封装,方便代码复用
class NMenuAnchor<E> extends StatelessWidget {

  NMenuAnchor({
    super.key,
    required this.values,
    // this.selectedItem,
    required this.initialItem,
    this.itemBuilder,
    required this.onChanged,
    required this.cbName,
  });
  /// 数据源
  final List<E> values;
  // E? selectedItem;
  /// 初始化数据
  final E initialItem;
  /// item 子视图构建器
  final Widget Function(MenuController controller, E? selectedItem)? itemBuilder;
  /// 选择回调
  final ValueChanged<E> onChanged;
  /// 标题回调
  final String Function(E e) cbName;


  @override
  Widget build(BuildContext context) {
    var selectedItem = initialItem;

    return StatefulBuilder(
        builder: (BuildContext context, StateSetter setState) {

          return MenuAnchor(
            builder: (context, MenuController controller, Widget? child) {

              return itemBuilder?.call(controller, selectedItem) ?? OutlinedButton(
                style: OutlinedButton.styleFrom(
                  // backgroundColor: Color(0xff5690F4).withOpacity(0.1),
                  // foregroundColor: Color(0xff5690F4),
                  elevation: 0,
                  // shape: StadiumBorder(),
                  tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                  // minimumSize: Size(64, 32),
                  padding: EdgeInsets.only(left: 8, right: 2, top: 6, bottom: 6),
                  foregroundColor: Colors.black87,
                ),
                onPressed: (){
                  if (controller.isOpen) {
                    controller.close();
                  } else {
                    controller.open();
                  }
                },
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text(cbName(selectedItem)),
                    Icon(Icons.arrow_drop_down),
                  ],
                ),
              );
            },
            menuChildren: values.map((e) {
              return MenuItemButton(
                onPressed: () {
                  selectedItem = e;
                  setState(() {});
                  onChanged.call(e);
                },
                child: Text(cbName(e)),
              );
            }).toList(),
          );
        }
    );
  }
}

四、总结

2、NMenuAnchor 只是简易封装,方便代码复用,大家可以根据项目需要进行扩展;
3、NMenuAnchor 用 cbName 函数进行标题显示,极大的扩展了适用范围;不需要额外的虚拟类或者类协议类进行标题属性限制;任何对象的任何属性可以在此函数中处理返回显示即可;
arduino 复制代码
  /// 标题回调
  final String Function(E e) cbName;
4、MenuAnchor 是下拉菜单 DropMenu 的最佳实践,下拉菜单却不是它的能力边界;它还可以实现下拉树形菜单的效果;效果类似 mac 右上角顶部菜单栏;

相关组件还有: MenuBar MenuAcceleratorLabel RadioMenuButton CheckboxMenuButton SubmenuButton

相关推荐
爱吃的小肥羊1 小时前
比 Claude Code 便宜一半!Codex 国内部署使用教程,三种方法任选一!
前端
IT_陈寒2 小时前
SpringBoot项目启动慢?5个技巧让你的应用秒级响应!
前端·人工智能·后端
树上有只程序猿3 小时前
2026低代码选型指南,主流低代码开发平台排名出炉
前端·后端
橙某人3 小时前
LogicFlow 小地图性能优化:从「实时克隆」到「占位缩略块」!🚀
前端·javascript·vue.js
高端章鱼哥3 小时前
为什么说用OpenClaw对打工人来说“不划算”
前端·后端
大脸怪3 小时前
告别 F12!前端开发者必备:一键管理 localStorage / Cookie / SessionStorage 神器
前端·后端·浏览器
Mr_Mao3 小时前
我受够了混乱的 API 代码,所以我写了个框架
前端·api
小徐_23333 小时前
向日葵 x AI:把远程控制封装成 MCP,让 AI 替我远程控制设备
前端·人工智能
冴羽3 小时前
来自顶级大佬 TypeScript 之父的 7 个启示
前端·typescript
leafyyuki4 小时前
在 Vue 项目中玩转 FullCalendar:从零搭建可交互的事件日历
前端·javascript·vue.js