You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何通过拖拽重定位分组列表内条目及分组?

Flutter实现分组列表的条目与分组拖拽重定位

核心实现思路

通过Draggable和DragTarget组件组合,结合状态管理维护分组及条目的数据源,实现两个核心功能:

  • 单个条目在同组内或跨组间的拖拽重排
  • 整个分组的上下拖拽调整位置

步骤1:定义数据模型

先创建分组和条目的数据类,封装数据同时支持复制操作,避免直接修改原数据导致状态异常:

class Group {
  final String name;
  List<String> items;

  Group({required this.name, required this.items});

  Group copyWith({String? name, List<String>? items}) {
    return Group(
      name: name ?? this.name,
      items: items ?? List.from(this.items),
    );
  }
}

步骤2:实现分组拖拽功能

将分组头部包装为Draggable,同时在每个分组上方放置DragTarget,接收拖拽的分组并调整整体顺序:

// 分组头部可拖拽组件
Widget _buildGroupHeader(int groupIndex) {
  return Draggable<int>(
    data: groupIndex,
    feedback: Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
      color: Colors.grey[200],
      child: Text(
        groups[groupIndex].name,
        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
    ),
    childWhenDragging: Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
      color: Colors.grey[100],
      child: const Text("..."),
    ),
    child: Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
      color: Colors.blue[50],
      child: Text(
        groups[groupIndex].name,
        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
    ),
  );
}

// 分组间的拖拽接收组件
Widget _buildGroupDragTarget(int targetGroupIndex) {
  return DragTarget<int>(
    onAccept: (sourceGroupIndex) {
      if (sourceGroupIndex != targetGroupIndex) {
        setState(() {
          final Group movedGroup = groups.removeAt(sourceGroupIndex);
          groups.insert(targetGroupIndex, movedGroup);
        });
      }
    },
    builder: (context, candidateData, rejectedData) {
      return const SizedBox(height: 4);
    },
  );
}

步骤3:实现条目拖拽功能

每个条目作为Draggable,携带所在分组和自身索引;条目位置放置DragTarget,支持同组内移动或跨组移动:

// 条目可拖拽组件
Widget _buildItem(int groupIndex, int itemIndex) {
  final item = groups[groupIndex].items[itemIndex];
  return Draggable<Map<String, int>>(
    data: {'group': groupIndex, 'item': itemIndex},
    feedback: Material(
      child: Container(
        width: double.infinity,
        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20),
        margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
        color: Colors.white,
        child: Text(item),
      ),
    ),
    childWhenDragging: Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20),
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      color: Colors.grey[100],
      child: const Text("..."),
    ),
    child: _buildItemDragTarget(groupIndex, itemIndex),
  );
}

// 条目位置的拖拽接收组件
Widget _buildItemDragTarget(int targetGroupIndex, int targetItemIndex) {
  return DragTarget<Map<String, int>>(
    onAccept: (sourceData) {
      final sourceGroupIndex = sourceData['group']!;
      final sourceItemIndex = sourceData['item']!;

      if (sourceGroupIndex == targetGroupIndex && sourceItemIndex == targetItemIndex) return;

      setState(() {
        final movedItem = groups[sourceGroupIndex].items.removeAt(sourceItemIndex);
        if (sourceGroupIndex == targetGroupIndex) {
          // 同组内移动
          groups[sourceGroupIndex].items.insert(targetItemIndex, movedItem);
        } else {
          // 跨组移动
          groups[targetGroupIndex].items.insert(targetItemIndex, movedItem);
          // 源分组为空时自动删除(可选逻辑)
          if (groups[sourceGroupIndex].items.isEmpty) {
            groups.removeAt(sourceGroupIndex);
          }
        }
      });
    },
    builder: (context, candidateData, rejectedData) {
      return Container(
        width: double.infinity,
        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20),
        margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
        color: candidateData.isNotEmpty ? Colors.blue[50] : Colors.white,
        child: Text(groups[targetGroupIndex].items[targetItemIndex]),
      );
    },
  );
}

步骤4:完整页面整合

将上述组件整合到StatefulWidget中,初始化测试数据:

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

  @override
  State<DraggableGroupedList> createState() => _DraggableGroupedListState();
}

class _DraggableGroupedListState extends State<DraggableGroupedList> {
  late List<Group> groups;

  @override
  void initState() {
    super.initState();
    // 初始化测试数据
    groups = [
      Group(name: "分组A", items: ["条目A1", "条目A2", "条目A3"]),
      Group(name: "分组B", items: ["条目B1", "条目B2"]),
      Group(name: "分组C", items: ["条目C1"]),
    ];
  }

  // 插入上述的_buildGroupHeader、_buildGroupDragTarget、_buildItem、_buildItemDragTarget方法

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("可拖拽分组列表")),
      body: ListView.builder(
        itemCount: groups.length,
        itemBuilder: (context, groupIndex) {
          return Column(
            children: [
              _buildGroupDragTarget(groupIndex),
              _buildGroupHeader(groupIndex),
              ...List.generate(groups[groupIndex].items.length, (itemIndex) {
                return _buildItem(groupIndex, itemIndex);
              }),
            ],
          );
        },
      ),
    );
  }
}

关键注意事项

  • 数据源不可变性:修改分组或条目列表时,必须创建新的列表实例(如List.from),避免直接修改原列表导致状态不更新。
  • 视觉反馈:通过childWhenDragging和feedback组件提供清晰的拖拽提示,提升用户体验。
  • 边界处理:可根据业务需求扩展空分组处理、拖拽到空分组等逻辑,上述代码已包含基础的空分组自动删除逻辑。
  • 性能优化:若分组和条目数量较多,建议结合AutomaticKeepAliveClientMixin避免列表项重复重建。

内容的提问来源于stack exchange,提问作者PradeepKumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 13:13:32