Explorar o código

chore: add documentation

appflowy %!s(int64=2) %!d(string=hai) anos
pai
achega
9e4dbc53f7

+ 5 - 5
frontend/app_flowy/packages/flowy_board/example/lib/multi_board_list_example.dart

@@ -9,7 +9,7 @@ class MultiBoardListExample extends StatefulWidget {
 }
 
 class _MultiBoardListExampleState extends State<MultiBoardListExample> {
-  final BoardDataController boardData = BoardDataController();
+  final BoardDataController boardDataController = BoardDataController();
 
   @override
   void initState() {
@@ -34,9 +34,9 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
       TextItem("D"),
     ]);
 
-    boardData.setColumnData(column1);
-    boardData.setColumnData(column2);
-    boardData.setColumnData(column3);
+    boardDataController.setColumnData(column1);
+    boardDataController.setColumnData(column2);
+    boardDataController.setColumnData(column3);
 
     super.initState();
   }
@@ -44,7 +44,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
   @override
   Widget build(BuildContext context) {
     return Board(
-      dataController: boardData,
+      dataController: boardDataController,
       background: Container(color: Colors.red),
       footBuilder: (context, columnData) {
         return Container(

+ 54 - 36
frontend/app_flowy/packages/flowy_board/lib/src/widgets/board.dart

@@ -67,7 +67,7 @@ class Board extends StatelessWidget {
             footBuilder: footBuilder,
             headerBuilder: headerBuilder,
             phantomController: phantomController,
-            onReorder: dataController.onReorder,
+            onReorder: dataController.moveColumn,
           );
         },
       ),
@@ -99,7 +99,7 @@ class BoardContent extends StatefulWidget {
 
   final BoardPhantomController phantomController;
 
-  const BoardContent({
+  BoardContent({
     required this.onReorder,
     required this.delegate,
     required this.dataController,
@@ -107,22 +107,23 @@ class BoardContent extends StatefulWidget {
     this.onDragEnded,
     this.scrollController,
     this.background,
-    this.spacing = 0.0,
-    this.config = const ReorderFlexConfig(),
+    this.spacing = 10.0,
     required this.columnConstraints,
     required this.cardBuilder,
     this.footBuilder,
     this.headerBuilder,
     required this.phantomController,
     Key? key,
-  }) : super(key: key);
+  })  : config = ReorderFlexConfig(spacing: spacing),
+        super(key: key);
 
   @override
   State<BoardContent> createState() => _BoardContentState();
 }
 
 class _BoardContentState extends State<BoardContent> {
-  final GlobalKey _columnContainerOverlayKey = GlobalKey(debugLabel: '$BoardContent overlay key');
+  final GlobalKey _columnContainerOverlayKey =
+      GlobalKey(debugLabel: '$BoardContent overlay key');
   late BoardOverlayEntry _overlayEntry;
 
   @override
@@ -144,7 +145,6 @@ class _BoardContentState extends State<BoardContent> {
           onDragEnded: widget.onDragEnded,
           dataSource: widget.dataController,
           direction: Axis.horizontal,
-          spacing: widget.spacing,
           interceptor: interceptor,
           children: _buildColumns(),
         );
@@ -171,36 +171,54 @@ class _BoardContentState extends State<BoardContent> {
   }
 
   List<Widget> _buildColumns() {
-    final acceptColumns = widget.dataController.columnIds;
-
-    final List<Widget> children = widget.dataController.columnDatas.map((columnData) {
-      final dataController = widget.dataController.columnController(columnData.id);
-
-      return ChangeNotifierProvider.value(
-        key: ValueKey(columnData.id),
-        value: dataController,
-        child: Consumer<BoardColumnDataController>(
-          builder: (context, value, child) {
-            return ConstrainedBox(
-              constraints: widget.columnConstraints,
-              child: BoardColumnWidget(
-                headerBuilder: widget.headerBuilder,
-                footBuilder: widget.footBuilder,
-                cardBuilder: widget.cardBuilder,
-                acceptedColumns: acceptColumns,
-                dataController: dataController,
-                scrollController: ScrollController(),
-                phantomController: widget.phantomController,
-                onReorder: (_, int fromIndex, int toIndex) {
-                  dataController.move(fromIndex, toIndex);
-                },
-              ),
-            );
-          },
-        ),
-      );
-    }).toList();
+    final List<Widget> children = widget.dataController.columnDatas.map(
+      (columnData) {
+        final dataSource = _BoardColumnDataSourceImpl(
+          columnId: columnData.id,
+          dataController: widget.dataController,
+        );
+
+        return ChangeNotifierProvider.value(
+          key: ValueKey(columnData.id),
+          value: widget.dataController.columnController(columnData.id),
+          child: Consumer<BoardColumnDataController>(
+            builder: (context, value, child) {
+              return ConstrainedBox(
+                constraints: widget.columnConstraints,
+                child: BoardColumnWidget(
+                  headerBuilder: widget.headerBuilder,
+                  footBuilder: widget.footBuilder,
+                  cardBuilder: widget.cardBuilder,
+                  dataSource: dataSource,
+                  scrollController: ScrollController(),
+                  phantomController: widget.phantomController,
+                  onReorder: widget.dataController.moveColumnItem,
+                  spacing: 10,
+                ),
+              );
+            },
+          ),
+        );
+      },
+    ).toList();
 
     return children;
   }
 }
+
+class _BoardColumnDataSourceImpl extends BoardColumnDataDataSource {
+  String columnId;
+  final BoardDataController dataController;
+
+  _BoardColumnDataSourceImpl({
+    required this.columnId,
+    required this.dataController,
+  });
+
+  @override
+  BoardColumnData get columnData =>
+      dataController.columnController(columnId).columnData;
+
+  @override
+  List<String> get acceptedColumnIds => dataController.columnIds;
+}

+ 57 - 25
frontend/app_flowy/packages/flowy_board/lib/src/widgets/board_column/board_column.dart

@@ -1,29 +1,67 @@
-import 'package:flutter/material.dart';
+import 'dart:collection';
 
+import 'package:flutter/material.dart';
 import '../../rendering/board_overlay.dart';
+import '../../utils/log.dart';
 import '../phantom/phantom_controller.dart';
 import '../flex/reorder_flex.dart';
 import '../flex/drag_target_inteceptor.dart';
 import 'data_controller.dart';
 
 typedef OnColumnDragStarted = void Function(int index);
+
 typedef OnColumnDragEnded = void Function(String listId);
+
 typedef OnColumnReorder = void Function(
-    String listId, int fromIndex, int toIndex);
+  String listId,
+  int fromIndex,
+  int toIndex,
+);
+
 typedef OnColumnDeleted = void Function(String listId, int deletedIndex);
+
 typedef OnColumnInserted = void Function(String listId, int insertedIndex);
 
 typedef BoardColumnCardBuilder = Widget Function(
-    BuildContext context, ColumnItem item);
+  BuildContext context,
+  ColumnItem item,
+);
 
 typedef BoardColumnHeaderBuilder = Widget Function(
-    BuildContext context, BoardColumnData columnData);
+  BuildContext context,
+  BoardColumnData columnData,
+);
 
 typedef BoardColumnFooterBuilder = Widget Function(
-    BuildContext context, BoardColumnData columnData);
+  BuildContext context,
+  BoardColumnData columnData,
+);
+
+abstract class BoardColumnDataDataSource extends ReoderFlextDataSource {
+  BoardColumnData get columnData;
+
+  List<String> get acceptedColumnIds;
+
+  @override
+  String get identifier => columnData.id;
 
+  @override
+  UnmodifiableListView<ColumnItem> get items => columnData.items;
+
+  void debugPrint() {
+    String msg = '[$BoardColumnDataDataSource] $columnData data: ';
+    for (var element in items) {
+      msg = '$msg$element,';
+    }
+
+    Log.debug(msg);
+  }
+}
+
+/// [BoardColumnWidget] represents the column of the Board.
+///
 class BoardColumnWidget extends StatefulWidget {
-  final BoardColumnDataController dataController;
+  final BoardColumnDataDataSource dataSource;
   final ScrollController? scrollController;
   final ReorderFlexConfig config;
 
@@ -33,9 +71,7 @@ class BoardColumnWidget extends StatefulWidget {
 
   final BoardPhantomController phantomController;
 
-  String get columnId => dataController.identifier;
-
-  final List<String> acceptedColumns;
+  String get columnId => dataSource.columnData.id;
 
   final BoardColumnCardBuilder cardBuilder;
 
@@ -43,23 +79,20 @@ class BoardColumnWidget extends StatefulWidget {
 
   final BoardColumnFooterBuilder? footBuilder;
 
-  final double? spacing;
-
-  const BoardColumnWidget({
+  BoardColumnWidget({
     Key? key,
     this.headerBuilder,
     this.footBuilder,
     required this.cardBuilder,
     required this.onReorder,
-    required this.dataController,
+    required this.dataSource,
     required this.phantomController,
-    required this.acceptedColumns,
-    this.config = const ReorderFlexConfig(),
-    this.spacing,
     this.onDragStarted,
     this.scrollController,
     this.onDragEnded,
-  }) : super(key: key);
+    double? spacing,
+  })  : config = ReorderFlexConfig(spacing: spacing),
+        super(key: key);
 
   @override
   State<BoardColumnWidget> createState() => _BoardColumnWidgetState();
@@ -75,20 +108,20 @@ class _BoardColumnWidgetState extends State<BoardColumnWidget> {
   void initState() {
     _overlayEntry = BoardOverlayEntry(
       builder: (BuildContext context) {
-        final children = widget.dataController.items
+        final children = widget.dataSource.columnData.items
             .map((item) => _buildWidget(context, item))
             .toList();
 
-        final header = widget.headerBuilder
-            ?.call(context, widget.dataController.columnData);
+        final header =
+            widget.headerBuilder?.call(context, widget.dataSource.columnData);
 
         final footer =
-            widget.footBuilder?.call(context, widget.dataController.columnData);
+            widget.footBuilder?.call(context, widget.dataSource.columnData);
 
         final interceptor = CrossReorderFlexDragTargetInterceptor(
           reorderFlexId: widget.columnId,
           delegate: widget.phantomController,
-          acceptedReorderFlexIds: widget.acceptedColumns,
+          acceptedReorderFlexIds: widget.dataSource.acceptedColumnIds,
           draggableTargetBuilder: PhantomDraggableBuilder(),
         );
 
@@ -109,11 +142,10 @@ class _BoardColumnWidgetState extends State<BoardColumnWidget> {
           onDragEnded: () {
             widget.phantomController.columnEndDragging(widget.columnId);
             widget.onDragEnded?.call(widget.columnId);
-            widget.dataController.debugPrintItems();
+            widget.dataSource.debugPrint();
           },
-          dataSource: widget.dataController,
+          dataSource: widget.dataSource,
           interceptor: interceptor,
-          spacing: widget.spacing,
           children: children,
         );
 

+ 56 - 40
frontend/app_flowy/packages/flowy_board/lib/src/widgets/board_column/data_controller.dart

@@ -9,36 +9,18 @@ abstract class ColumnItem extends ReoderFlexItem {
   bool get isPhantom => false;
 
   @override
-  String toString() {
-    if (isPhantom) {
-      return 'phantom:$id';
-    } else {
-      return id;
-    }
-  }
-}
-
-class BoardColumnData extends ReoderFlexItem with EquatableMixin {
-  @override
-  final String id;
-  final List<ColumnItem> _items;
-
-  BoardColumnData({
-    required this.id,
-    required List<ColumnItem> items,
-  }) : _items = items;
-
-  @override
-  List<Object?> get props => [id, ..._items];
-
-  @override
-  String toString() {
-    return 'Column$id';
-  }
+  String toString() => id;
 }
 
-class BoardColumnDataController extends ChangeNotifier
-    with EquatableMixin, ReoderFlextDataSource {
+/// [BoardColumnDataController] is used to handle the [BoardColumnData].
+/// * Remove an item by calling [removeAt] method.
+/// * Move item to another position by calling [move] method.
+/// * Insert item to index by calling [insert] method
+/// * Replace item at index by calling [replace] method.
+///
+/// All there operations will notify listeners by default.
+///
+class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
   final BoardColumnData columnData;
 
   BoardColumnDataController({
@@ -48,7 +30,18 @@ class BoardColumnDataController extends ChangeNotifier
   @override
   List<Object?> get props => columnData.props;
 
+  /// Returns the readonly List<ColumnItem>
+  UnmodifiableListView<ColumnItem> get items =>
+      UnmodifiableListView(columnData.items);
+
+  /// Remove the item at [index].
+  /// * [index] the index of the item you want to remove
+  /// * [notify] the default value of [notify] is true, it will notify the
+  /// listener. Set to [false] if you do not want to notify the listeners.
+  ///
   ColumnItem removeAt(int index, {bool notify = true}) {
+    assert(index >= 0);
+
     Log.debug('[$BoardColumnDataController] $columnData remove item at $index');
     final item = columnData._items.removeAt(index);
     if (notify) {
@@ -57,7 +50,16 @@ class BoardColumnDataController extends ChangeNotifier
     return item;
   }
 
+  int removeWhere(bool Function(ColumnItem) condition) {
+    return items.indexWhere(condition);
+  }
+
+  /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the
+  /// [fromIndex] equal to the [toIndex].
   void move(int fromIndex, int toIndex) {
+    assert(fromIndex >= 0);
+    assert(toIndex >= 0);
+
     if (fromIndex == toIndex) {
       return;
     }
@@ -68,7 +70,12 @@ class BoardColumnDataController extends ChangeNotifier
     notifyListeners();
   }
 
+  /// Insert an item to [index] and notify the listen if the value of [notify]
+  /// is true.
+  ///
+  /// The default value of [notify] is true.
   void insert(int index, ColumnItem item, {bool notify = true}) {
+    assert(index >= 0);
     Log.debug(
         '[$BoardColumnDataController] $columnData insert $item at $index');
 
@@ -83,26 +90,35 @@ class BoardColumnDataController extends ChangeNotifier
     }
   }
 
-  void replace(int index, ColumnItem item) {
+  /// Replace the item at index with the [newItem].
+  void replace(int index, ColumnItem newItem) {
     final removedItem = columnData._items.removeAt(index);
-    columnData._items.insert(index, item);
+    columnData._items.insert(index, newItem);
     Log.debug(
-        '[$BoardColumnDataController] $columnData replace $removedItem with $item at $index');
+        '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index');
     notifyListeners();
   }
+}
 
-  void debugPrintItems() {
-    String msg = '[$BoardColumnDataController] $columnData data: ';
-    for (var element in items) {
-      msg = '$msg$element,';
-    }
+/// [BoardColumnData] represents the data of each Column of the Board.
+class BoardColumnData extends ReoderFlexItem with EquatableMixin {
+  @override
+  final String id;
+  final List<ColumnItem> _items;
 
-    Log.debug(msg);
-  }
+  BoardColumnData({
+    required this.id,
+    required List<ColumnItem> items,
+  }) : _items = items;
+
+  /// Returns the readonly List<ColumnItem>
+  UnmodifiableListView<ColumnItem> get items => UnmodifiableListView(_items);
 
   @override
-  List<ColumnItem> get items => UnmodifiableListView(columnData._items);
+  List<Object?> get props => [id, ..._items];
 
   @override
-  String get identifier => columnData.id;
+  String toString() {
+    return 'Column$id';
+  }
 }

+ 63 - 2
frontend/app_flowy/packages/flowy_board/lib/src/widgets/board_data.dart

@@ -3,6 +3,7 @@ import 'dart:collection';
 import 'package:equatable/equatable.dart';
 
 import '../../flowy_board.dart';
+import '../utils/log.dart';
 import 'flex/reorder_flex.dart';
 import 'package:flutter/material.dart';
 import 'phantom/phantom_controller.dart';
@@ -31,12 +32,35 @@ class BoardDataController extends ChangeNotifier
     return _columnControllers[columnId]!;
   }
 
-  void onReorder(int fromIndex, int toIndex) {
+  void moveColumn(int fromIndex, int toIndex) {
     final columnData = _columnDatas.removeAt(fromIndex);
     _columnDatas.insert(toIndex, columnData);
     notifyListeners();
   }
 
+  void moveColumnItem(String columnId, int fromIndex, int toIndex) {
+    final columnController = _columnControllers[columnId];
+    assert(columnController != null);
+    if (columnController != null) {
+      columnController.move(fromIndex, toIndex);
+    }
+  }
+
+  @override
+  void swapColumnItem(
+    String fromColumnId,
+    int fromColumnIndex,
+    String toColumnId,
+    int toColumnIndex,
+  ) {
+    final item = columnController(fromColumnId).removeAt(fromColumnIndex);
+
+    assert(
+        columnController(toColumnId).items[toColumnIndex] is PhantomColumnItem);
+
+    columnController(toColumnId).replace(toColumnIndex, item);
+  }
+
   @override
   List<Object?> get props {
     return [_columnDatas];
@@ -51,5 +75,42 @@ class BoardDataController extends ChangeNotifier
   String get identifier => '$BoardDataController';
 
   @override
-  List<ReoderFlexItem> get items => _columnDatas;
+  UnmodifiableListView<ReoderFlexItem> get items =>
+      UnmodifiableListView(_columnDatas);
+
+  @override
+  bool removePhantom(String columnId) {
+    final columnController = this.columnController(columnId);
+    final index = columnController.items.indexWhere((item) => item.isPhantom);
+
+    final isExist = index != -1;
+    if (isExist) {
+      columnController.removeAt(index);
+
+      Log.debug(
+          '[$BoardPhantomController] Column$columnId remove phantom, current count: ${columnController.items.length}');
+    }
+    return isExist;
+  }
+
+  @override
+  void updatePhantom(String columnId, int newIndex) {
+    final columnDataController = columnController(columnId);
+    final index =
+        columnDataController.items.indexWhere((item) => item.isPhantom);
+
+    assert(index != -1);
+    if (index != -1) {
+      if (index != newIndex) {
+        // Log.debug('[$BoardPhantomController] update $toColumnId:$index to $toColumnId:$phantomIndex');
+        final item = columnDataController.removeAt(index, notify: false);
+        columnDataController.insert(newIndex, item, notify: false);
+      }
+    }
+  }
+
+  @override
+  void insertPhantom(String columnId, int index, PhantomColumnItem item) {
+    columnController(columnId).insert(index, item);
+  }
 }

+ 1 - 1
frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/drag_target.dart

@@ -77,7 +77,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
 
 class _ReorderDragTargetState<T extends DragTargetData>
     extends State<ReorderDragTarget<T>> {
-  /// Return the dragTarget's size
+  /// Returns the dragTarget's size
   Size? _draggingFeedbackSize = Size.zero;
 
   @override

+ 9 - 7
frontend/app_flowy/packages/flowy_board/lib/src/widgets/flex/reorder_flex.dart

@@ -1,3 +1,4 @@
+import 'dart:collection';
 import 'dart:math';
 
 import 'package:flutter/material.dart';
@@ -18,10 +19,11 @@ typedef OnReveivePassedInPhantom = void Function(
 
 abstract class ReoderFlextDataSource {
   String get identifier;
-  List<ReoderFlexItem> get items;
+  UnmodifiableListView<ReoderFlexItem> get items;
 }
 
 abstract class ReoderFlexItem {
+  /// [id] is used to identify the item
   String get id;
 }
 
@@ -30,7 +32,9 @@ class ReorderFlexConfig {
   final double draggingWidgetOpacity = 0.2;
   final Duration reorderAnimationDuration = const Duration(milliseconds: 250);
   final Duration scrollAnimationDuration = const Duration(milliseconds: 250);
-  const ReorderFlexConfig();
+
+  final double? spacing;
+  const ReorderFlexConfig({this.spacing});
 }
 
 class ReorderFlex extends StatefulWidget {
@@ -38,7 +42,6 @@ class ReorderFlex extends StatefulWidget {
 
   final List<Widget> children;
   final EdgeInsets? padding;
-  final double? spacing;
   final Axis direction;
   final MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start;
   final ScrollController? scrollController;
@@ -62,7 +65,6 @@ class ReorderFlex extends StatefulWidget {
     this.onDragEnded,
     this.interceptor,
     this.padding,
-    this.spacing,
     this.direction = Axis.vertical,
   }) : super(key: key);
 
@@ -132,8 +134,8 @@ class ReorderFlexState extends State<ReorderFlex>
     for (int i = 0; i < widget.children.length; i += 1) {
       Widget child = widget.children[i];
 
-      if (widget.spacing != null) {
-        children.add(SizedBox(width: widget.spacing!));
+      if (widget.config.spacing != null) {
+        children.add(SizedBox(width: widget.config.spacing!));
       }
 
       final wrapChild = _wrap(child, i);
@@ -203,7 +205,7 @@ class ReorderFlexState extends State<ReorderFlex>
           dragSpace = SizedBox.fromSize(size: dragState.dropAreaSize);
         }
 
-        /// Return the dragTarget it is not start dragging. The size of the
+        /// Returns the dragTarget it is not start dragging. The size of the
         /// dragTarget is the same as the the passed in child.
         ///
         if (dragState.isNotDragging()) {

+ 43 - 62
frontend/app_flowy/packages/flowy_board/lib/src/widgets/phantom/phantom_controller.dart

@@ -8,24 +8,39 @@ import 'phantom_state.dart';
 
 abstract class BoardPhantomControllerDelegate {
   BoardColumnDataController? controller(String columnId);
-}
 
-mixin ColumnDataPhantomMixim {
-  BoardColumnDataController? get;
+  bool removePhantom(String columnId);
+
+  /// Insert the phantom into column
+  ///
+  /// * [toColumnId] id of the column
+  /// * [phantomIndex] the phantom occupies index
+  void insertPhantom(
+    String columnId,
+    int index,
+    PhantomColumnItem item,
+  );
+
+  /// Update the column's phantom index if it exists.
+  /// [toColumnId] the id of the column
+  /// [dragTargetIndex] the index of the dragTarget
+  void updatePhantom(String columnId, int newIndex);
+
+  void swapColumnItem(
+    String fromColumnId,
+    int fromColumnIndex,
+    String toColumnId,
+    int toColumnIndex,
+  );
 }
 
 class BoardPhantomController extends OverlapReorderFlexDragTargetDelegate
     with CrossReorderFlexDragTargetDelegate {
-  final BoardPhantomControllerDelegate delegate;
-
   PhantomRecord? phantomRecord;
-
+  final BoardPhantomControllerDelegate delegate;
   final columnsState = ColumnPhantomStateController();
-
   BoardPhantomController({required this.delegate});
 
-  bool get hasPhantom => phantomRecord != null;
-
   bool isFromColumn(String columnId) {
     if (phantomRecord != null) {
       return phantomRecord!.fromColumnId == columnId;
@@ -64,67 +79,25 @@ class BoardPhantomController extends OverlapReorderFlexDragTargetDelegate
     if (columnsState.isDragging(phantomRecord!.fromColumnId) == false) {
       return;
     }
-    final item = delegate
-        .controller(phantomRecord!.fromColumnId)
-        ?.removeAt(phantomRecord!.fromColumnIndex);
-    assert(item != null);
-    assert(delegate
-        .controller(phantomRecord!.toColumnId)
-        ?.items[phantomRecord!.toColumnIndex] is PhantomColumnItem);
-    delegate
-        .controller(phantomRecord!.toColumnId)
-        ?.replace(phantomRecord!.toColumnIndex, item!);
+    delegate.swapColumnItem(
+      phantomRecord!.fromColumnId,
+      phantomRecord!.fromColumnIndex,
+      phantomRecord!.toColumnId,
+      phantomRecord!.toColumnIndex,
+    );
 
     Log.debug("[$BoardPhantomController] did move ${phantomRecord.toString()}");
     phantomRecord = null;
   }
 
-  /// Update the column's phantom index if it exists.
-  /// [toColumnId] the id of the column
-  /// [dragTargetIndex] the index of the dragTarget
-  void _updatePhantom(
-    String toColumnId,
-    int dragTargetIndex,
-  ) {
-    final columnDataController = delegate.controller(toColumnId);
-    final index =
-        columnDataController?.items.indexWhere((item) => item.isPhantom);
-    if (index == null) return;
-
-    assert(index != -1);
-    if (index != -1) {
-      if (index != dragTargetIndex) {
-        // Log.debug('[$BoardPhantomController] update $toColumnId:$index to $toColumnId:$phantomIndex');
-        final item = columnDataController!.removeAt(index, notify: false);
-        columnDataController.insert(dragTargetIndex, item, notify: false);
-      }
-    }
-  }
-
   /// Remove the phantom in the column if it contains phantom
   void _removePhantom(String columnId) {
-    final index = delegate
-        .controller(columnId)
-        ?.items
-        .indexWhere((item) => item.isPhantom);
-
-    if (index == null) return;
-
-    assert(index != -1);
-
-    if (index != -1) {
-      delegate.controller(columnId)?.removeAt(index);
-      Log.debug(
-          '[$BoardPhantomController] Column$columnId remove phantom, current count: ${delegate.controller(columnId)?.items.length}');
+    if (delegate.removePhantom(columnId)) {
       columnsState.notifyDidRemovePhantom(columnId);
       columnsState.removeColumnListener(columnId);
     }
   }
 
-  /// Insert the phantom into column
-  ///
-  /// * [toColumnId] id of the column
-  /// * [phantomIndex] the phantom occupies index
   void _insertPhantom(
     String toColumnId,
     FlexDragTargetData dragTargetData,
@@ -135,9 +108,12 @@ class BoardPhantomController extends OverlapReorderFlexDragTargetDelegate
       dragTargetData: dragTargetData,
     );
     columnsState.addColumnListener(toColumnId, phantomContext);
-    delegate
-        .controller(toColumnId)
-        ?.insert(phantomIndex, PhantomColumnItem(phantomContext));
+
+    delegate.insertPhantom(
+      toColumnId,
+      phantomIndex,
+      PhantomColumnItem(phantomContext),
+    );
 
     WidgetsBinding.instance.addPostFrameCallback((_) {
       Future.delayed(const Duration(milliseconds: 100), () {
@@ -204,7 +180,7 @@ class BoardPhantomController extends OverlapReorderFlexDragTargetDelegate
     assert(phantomRecord != null);
     if (phantomRecord!.toColumnId == reorderFlexId) {
       /// Update the existing phantom index
-      _updatePhantom(phantomRecord!.toColumnId, dragTargetIndex);
+      delegate.updatePhantom(phantomRecord!.toColumnId, dragTargetIndex);
     }
   }
 
@@ -283,6 +259,11 @@ class PhantomColumnItem extends ColumnItem {
   Widget get draggingWidget => phantomContext.draggingWidget == null
       ? const SizedBox()
       : phantomContext.draggingWidget!;
+
+  @override
+  String toString() {
+    return 'phantom:$id';
+  }
 }
 
 class PassthroughPhantomContext extends FakeDragTargetEventTrigger