Browse Source

chore: load field typeOption

appflowy 2 years ago
parent
commit
0d6c04ae81

+ 85 - 17
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -2,6 +2,7 @@ import 'dart:async';
 import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
 import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
+import 'package:appflowy_board/appflowy_board.dart';
 import 'package:dartz/dartz.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@@ -14,11 +15,32 @@ import 'dart:collection';
 part 'board_bloc.freezed.dart';
 
 class BoardBloc extends Bloc<BoardEvent, BoardState> {
-  final GridDataController dataController;
+  final GridDataController _gridDataController;
+  late final BoardDataController boardDataController;
 
   BoardBloc({required ViewPB view})
-      : dataController = GridDataController(view: view),
+      : _gridDataController = GridDataController(view: view),
         super(BoardState.initial(view.id)) {
+    boardDataController = BoardDataController(
+      onMoveColumn: (
+        fromIndex,
+        toIndex,
+      ) {},
+      onMoveColumnItem: (
+        columnId,
+        fromIndex,
+        toIndex,
+      ) {},
+      onMoveColumnItemToColumn: (
+        fromColumnId,
+        fromIndex,
+        toColumnId,
+        toIndex,
+      ) {},
+    );
+
+    // boardDataController.addColumns(_buildColumns());
+
     on<BoardEvent>(
       (event, emit) async {
         await event.when(
@@ -27,21 +49,19 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
             await _loadGrid(emit);
           },
           createRow: () {
-            dataController.createRow();
+            _gridDataController.createRow();
           },
-          didReceiveGridUpdate: (grid) {
+          didReceiveGridUpdate: (GridPB grid) {
             emit(state.copyWith(grid: Some(grid)));
           },
-          didReceiveFieldUpdate: (fields) {
-            emit(state.copyWith(
-              fields: GridFieldEquatable(fields),
-            ));
+          didReceiveFieldUpdate: (UnmodifiableListView<GridFieldPB> fields) {
+            emit(state.copyWith(fields: GridFieldEquatable(fields)));
           },
-          didReceiveRowUpdate: (newRowInfos, reason) {
-            emit(state.copyWith(
-              rowInfos: newRowInfos,
-              reason: reason,
-            ));
+          didReceiveRowUpdate: (
+            List<GridRowInfo> newRowInfos,
+            GridRowChangeReason reason,
+          ) {
+            emit(state.copyWith(rowInfos: newRowInfos, reason: reason));
           },
         );
       },
@@ -50,17 +70,17 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   @override
   Future<void> close() async {
-    await dataController.dispose();
+    await _gridDataController.dispose();
     return super.close();
   }
 
   GridRowCache? getRowCache(String blockId, String rowId) {
-    final GridBlockCache? blockCache = dataController.blocks[blockId];
+    final GridBlockCache? blockCache = _gridDataController.blocks[blockId];
     return blockCache?.rowCache;
   }
 
   void _startListening() {
-    dataController.addListener(
+    _gridDataController.addListener(
       onGridChanged: (grid) {
         if (!isClosed) {
           add(BoardEvent.didReceiveGridUpdate(grid));
@@ -73,14 +93,43 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
       },
       onFieldsChanged: (fields) {
         if (!isClosed) {
+          _buildColumns(fields);
           add(BoardEvent.didReceiveFieldUpdate(fields));
         }
       },
     );
   }
 
+  void _buildColumns(UnmodifiableListView<GridFieldPB> fields) {
+    List<BoardColumnData> columns = [];
+
+    for (final field in fields) {
+      if (field.fieldType == FieldType.SingleSelect) {
+        //  return BoardColumnData(customData: field, id: field.id, desc: "1");
+      }
+    }
+
+    boardDataController.addColumns(columns);
+
+    // final column1 = BoardColumnData(id: "To Do", items: [
+    //   TextItem("Card 1"),
+    //   TextItem("Card 2"),
+    //   RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'),
+    //   TextItem("Card 4"),
+    // ]);
+    // final column2 = BoardColumnData(id: "In Progress", items: [
+    //   RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'),
+    //   TextItem("Card 6"),
+    // ]);
+
+    // final column3 = BoardColumnData(id: "Done", items: []);
+    // boardDataController.addColumn(column1);
+    // boardDataController.addColumn(column2);
+    // boardDataController.addColumn(column3);
+  }
+
   Future<void> _loadGrid(Emitter<BoardState> emit) async {
-    final result = await dataController.loadData();
+    final result = await _gridDataController.loadData();
     result.fold(
       (grid) => emit(
         state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
@@ -159,3 +208,22 @@ class GridFieldEquatable extends Equatable {
 
   UnmodifiableListView<GridFieldPB> get value => UnmodifiableListView(_fields);
 }
+
+class TextItem extends ColumnItem {
+  final String s;
+
+  TextItem(this.s);
+
+  @override
+  String get id => s;
+}
+
+class RichTextItem extends ColumnItem {
+  final String title;
+  final String subtitle;
+
+  RichTextItem({required this.title, required this.subtitle});
+
+  @override
+  String get id => title;
+}

+ 51 - 111
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -3,19 +3,20 @@
 import 'package:appflowy_board/appflowy_board.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-
 import '../application/board_bloc.dart';
 
-class BoardPage2 extends StatelessWidget {
+class BoardPage extends StatelessWidget {
   final ViewPB view;
-  const BoardPage2({required this.view, Key? key}) : super(key: key);
+  const BoardPage({required this.view, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
-      create: (context) => BoardBloc(view: view),
+      create: (context) =>
+          BoardBloc(view: view)..add(const BoardEvent.initial()),
       child: BlocBuilder<BoardBloc, BoardState>(
         builder: (context, state) {
           return state.loadingState.map(
@@ -23,7 +24,7 @@ class BoardPage2 extends StatelessWidget {
                 const Center(child: CircularProgressIndicator.adaptive()),
             finish: (result) {
               return result.successOrFail.fold(
-                (_) => const BoardContent(),
+                (_) => BoardContent(),
                 (err) => FlowyErrorPage(err.toString()),
               );
             },
@@ -35,100 +36,58 @@ class BoardPage2 extends StatelessWidget {
 }
 
 class BoardContent extends StatelessWidget {
-  const BoardContent({Key? key}) : super(key: key);
+  final config = BoardConfig(
+    columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+  );
+
+  BoardContent({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return Container();
+    return BlocBuilder<BoardBloc, BoardState>(
+      builder: (context, state) {
+        return Container(
+          color: Colors.white,
+          child: Padding(
+            padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
+            child: Board(
+              dataController: context.read<BoardBloc>().boardDataController,
+              headerBuilder: _buildHeader,
+              footBuilder: _buildFooter,
+              cardBuilder: (context, item) {
+                return AppFlowyColumnItemCard(
+                  key: ObjectKey(item),
+                  child: _buildCard(item),
+                );
+              },
+              columnConstraints: const BoxConstraints.tightFor(width: 240),
+              config: BoardConfig(
+                columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
+              ),
+            ),
+          ),
+        );
+      },
+    );
   }
-}
-
-class BoardPage extends StatefulWidget {
-  final ViewPB _view;
-
-  const BoardPage({required ViewPB view, Key? key})
-      : _view = view,
-        super(key: key);
-
-  @override
-  State<BoardPage> createState() => _BoardPageState();
-}
-
-class _BoardPageState extends State<BoardPage> {
-  final BoardDataController boardDataController = BoardDataController(
-    onMoveColumn: (fromIndex, toIndex) {
-      debugPrint('Move column from $fromIndex to $toIndex');
-    },
-    onMoveColumnItem: (columnId, fromIndex, toIndex) {
-      debugPrint('Move $columnId:$fromIndex to $columnId:$toIndex');
-    },
-    onMoveColumnItemToColumn: (fromColumnId, fromIndex, toColumnId, toIndex) {
-      debugPrint('Move $fromColumnId:$fromIndex to $toColumnId:$toIndex');
-    },
-  );
-
-  @override
-  void initState() {
-    final column1 = BoardColumnData(id: "To Do", items: [
-      TextItem("Card 1"),
-      TextItem("Card 2"),
-      RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'),
-      TextItem("Card 4"),
-    ]);
-    final column2 = BoardColumnData(id: "In Progress", items: [
-      RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'),
-      TextItem("Card 6"),
-    ]);
-
-    final column3 = BoardColumnData(id: "Done", items: []);
 
-    boardDataController.addColumn(column1);
-    boardDataController.addColumn(column2);
-    boardDataController.addColumn(column3);
-    super.initState();
+  Widget _buildHeader(BuildContext context, BoardColumnData columnData) {
+    return AppFlowyColumnHeader(
+      icon: const Icon(Icons.lightbulb_circle),
+      title: Text(columnData.desc),
+      addIcon: const Icon(Icons.add, size: 20),
+      moreIcon: const Icon(Icons.more_horiz, size: 20),
+      height: 50,
+      margin: config.columnItemPadding,
+    );
   }
 
-  @override
-  Widget build(BuildContext context) {
-    final config = BoardConfig(
-      columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
-    );
-    return Container(
-      color: Colors.white,
-      child: Padding(
-        padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
-        child: Board(
-          dataController: boardDataController,
-          footBuilder: (context, columnData) {
-            return AppFlowyColumnFooter(
-              icon: const Icon(Icons.add, size: 20),
-              title: const Text('New'),
-              height: 50,
-              margin: config.columnItemPadding,
-            );
-          },
-          headerBuilder: (context, columnData) {
-            return AppFlowyColumnHeader(
-              icon: const Icon(Icons.lightbulb_circle),
-              title: Text(columnData.id),
-              addIcon: const Icon(Icons.add, size: 20),
-              moreIcon: const Icon(Icons.more_horiz, size: 20),
-              height: 50,
-              margin: config.columnItemPadding,
-            );
-          },
-          cardBuilder: (context, item) {
-            return AppFlowyColumnItemCard(
-              key: ObjectKey(item),
-              child: _buildCard(item),
-            );
-          },
-          columnConstraints: const BoxConstraints.tightFor(width: 240),
-          config: BoardConfig(
-            columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
-          ),
-        ),
-      ),
+  Widget _buildFooter(BuildContext context, BoardColumnData columnData) {
+    return AppFlowyColumnFooter(
+      icon: const Icon(Icons.add, size: 20),
+      title: const Text('New'),
+      height: 50,
+      margin: config.columnItemPadding,
     );
   }
 
@@ -171,25 +130,6 @@ class _BoardPageState extends State<BoardPage> {
   }
 }
 
-class TextItem extends ColumnItem {
-  final String s;
-
-  TextItem(this.s);
-
-  @override
-  String get id => s;
-}
-
-class RichTextItem extends ColumnItem {
-  final String title;
-  final String subtitle;
-
-  RichTextItem({required this.title, required this.subtitle});
-
-  @override
-  String get id => title;
-}
-
 extension HexColor on Color {
   static Color fromHex(String hexString) {
     final buffer = StringBuffer();

+ 192 - 0
frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart

@@ -0,0 +1,192 @@
+import 'dart:collection';
+
+import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flutter/foundation.dart';
+
+import '../row/row_cache.dart';
+
+class FieldsNotifier extends ChangeNotifier {
+  List<GridFieldPB> _fields = [];
+
+  set fields(List<GridFieldPB> fields) {
+    _fields = fields;
+    notifyListeners();
+  }
+
+  List<GridFieldPB> get fields => _fields;
+}
+
+typedef FieldChangesetCallback = void Function(GridFieldChangesetPB);
+typedef FieldsCallback = void Function(List<GridFieldPB>);
+
+class GridFieldCache {
+  final String gridId;
+  final GridFieldsListener _fieldListener;
+  FieldsNotifier? _fieldNotifier = FieldsNotifier();
+  final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
+  final Map<FieldChangesetCallback, FieldChangesetCallback>
+      _changesetCallbackMap = {};
+
+  GridFieldCache({required this.gridId})
+      : _fieldListener = GridFieldsListener(gridId: gridId) {
+    _fieldListener.start(onFieldsChanged: (result) {
+      result.fold(
+        (changeset) {
+          _deleteFields(changeset.deletedFields);
+          _insertFields(changeset.insertedFields);
+          _updateFields(changeset.updatedFields);
+          for (final listener in _changesetCallbackMap.values) {
+            listener(changeset);
+          }
+        },
+        (err) => Log.error(err),
+      );
+    });
+  }
+
+  Future<void> dispose() async {
+    await _fieldListener.stop();
+    _fieldNotifier?.dispose();
+    _fieldNotifier = null;
+  }
+
+  UnmodifiableListView<GridFieldPB> get unmodifiableFields =>
+      UnmodifiableListView(_fieldNotifier?.fields ?? []);
+
+  List<GridFieldPB> get fields => [..._fieldNotifier?.fields ?? []];
+
+  set fields(List<GridFieldPB> fields) {
+    _fieldNotifier?.fields = [...fields];
+  }
+
+  void addListener({
+    FieldsCallback? onFields,
+    FieldChangesetCallback? onChangeset,
+    bool Function()? listenWhen,
+  }) {
+    if (onChangeset != null) {
+      fn(c) {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onChangeset(c);
+      }
+
+      _changesetCallbackMap[onChangeset] = fn;
+    }
+
+    if (onFields != null) {
+      fn() {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onFields(fields);
+      }
+
+      _fieldsCallbackMap[onFields] = fn;
+      _fieldNotifier?.addListener(fn);
+    }
+  }
+
+  void removeListener({
+    FieldsCallback? onFieldsListener,
+    FieldChangesetCallback? onChangesetListener,
+  }) {
+    if (onFieldsListener != null) {
+      final fn = _fieldsCallbackMap.remove(onFieldsListener);
+      if (fn != null) {
+        _fieldNotifier?.removeListener(fn);
+      }
+    }
+
+    if (onChangesetListener != null) {
+      _changesetCallbackMap.remove(onChangesetListener);
+    }
+  }
+
+  void _deleteFields(List<GridFieldIdPB> deletedFields) {
+    if (deletedFields.isEmpty) {
+      return;
+    }
+    final List<GridFieldPB> newFields = fields;
+    final Map<String, GridFieldIdPB> deletedFieldMap = {
+      for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
+    };
+
+    newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
+    _fieldNotifier?.fields = newFields;
+  }
+
+  void _insertFields(List<IndexFieldPB> insertedFields) {
+    if (insertedFields.isEmpty) {
+      return;
+    }
+    final List<GridFieldPB> newFields = fields;
+    for (final indexField in insertedFields) {
+      if (newFields.length > indexField.index) {
+        newFields.insert(indexField.index, indexField.field_1);
+      } else {
+        newFields.add(indexField.field_1);
+      }
+    }
+    _fieldNotifier?.fields = newFields;
+  }
+
+  void _updateFields(List<GridFieldPB> updatedFields) {
+    if (updatedFields.isEmpty) {
+      return;
+    }
+    final List<GridFieldPB> newFields = fields;
+    for (final updatedField in updatedFields) {
+      final index =
+          newFields.indexWhere((field) => field.id == updatedField.id);
+      if (index != -1) {
+        newFields.removeAt(index);
+        newFields.insert(index, updatedField);
+      }
+    }
+    _fieldNotifier?.fields = newFields;
+  }
+}
+
+class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
+  final GridFieldCache _cache;
+  FieldChangesetCallback? _onChangesetFn;
+  FieldsCallback? _onFieldFn;
+  GridRowFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
+
+  @override
+  UnmodifiableListView<GridFieldPB> get fields => _cache.unmodifiableFields;
+
+  @override
+  void onRowFieldsChanged(VoidCallback callback) {
+    _onFieldFn = (_) => callback();
+    _cache.addListener(onFields: _onFieldFn);
+  }
+
+  @override
+  void onRowFieldChanged(void Function(GridFieldPB) callback) {
+    _onChangesetFn = (GridFieldChangesetPB changeset) {
+      for (final updatedField in changeset.updatedFields) {
+        callback(updatedField);
+      }
+    };
+
+    _cache.addListener(onChangeset: _onChangesetFn);
+  }
+
+  @override
+  void onRowDispose() {
+    if (_onFieldFn != null) {
+      _cache.removeListener(onFieldsListener: _onFieldFn!);
+      _onFieldFn = null;
+    }
+
+    if (_onChangesetFn != null) {
+      _cache.removeListener(onChangesetListener: _onChangesetFn!);
+      _onChangesetFn = null;
+    }
+  }
+}

+ 5 - 3
frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart

@@ -13,7 +13,8 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
     required String gridId,
     required String fieldName,
     required IFieldTypeOptionLoader loader,
-  })  : dataController = TypeOptionDataController(gridId: gridId, loader: loader),
+  })  : dataController =
+            TypeOptionDataController(gridId: gridId, loader: loader),
         super(FieldEditorState.initial(gridId, fieldName)) {
     on<FieldEditorEvent>(
       (event, emit) async {
@@ -24,7 +25,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
                 add(FieldEditorEvent.didReceiveFieldChanged(field));
               }
             });
-            await dataController.loadData();
+            await dataController.loadTypeOptionData();
           },
           updateName: (name) {
             dataController.fieldName = name;
@@ -48,7 +49,8 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
 class FieldEditorEvent with _$FieldEditorEvent {
   const factory FieldEditorEvent.initial() = _InitialField;
   const factory FieldEditorEvent.updateName(String name) = _UpdateName;
-  const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = _DidReceiveFieldChanged;
+  const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) =
+      _DidReceiveFieldChanged;
 }
 
 @freezed

+ 5 - 3
frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart

@@ -146,7 +146,8 @@ abstract class IFieldTypeOptionLoader {
   String get gridId;
   Future<Either<FieldTypeOptionDataPB, FlowyError>> load();
 
-  Future<Either<FieldTypeOptionDataPB, FlowyError>> switchToField(String fieldId, FieldType fieldType) {
+  Future<Either<FieldTypeOptionDataPB, FlowyError>> switchToField(
+      String fieldId, FieldType fieldType) {
     final payload = EditFieldPayloadPB.create()
       ..gridId = gridId
       ..fieldId = fieldId
@@ -206,7 +207,7 @@ class TypeOptionDataController {
     required IFieldTypeOptionLoader loader,
   }) : _loader = loader;
 
-  Future<Either<Unit, FlowyError>> loadData() async {
+  Future<Either<Unit, FlowyError>> loadTypeOptionData() async {
     final result = await _loader.load();
     return result.fold(
       (data) {
@@ -238,7 +239,8 @@ class TypeOptionDataController {
     _updateData(newTypeOptionData: typeOptionData);
   }
 
-  void _updateData({String? newName, GridFieldPB? newField, List<int>? newTypeOptionData}) {
+  void _updateData(
+      {String? newName, GridFieldPB? newField, List<int>? newTypeOptionData}) {
     _data = _data.rebuild((rebuildData) {
       if (newName != null) {
         rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart

@@ -13,13 +13,13 @@ class MultiSelectTypeOptionContext
   final TypeOptionService service;
 
   MultiSelectTypeOptionContext({
-    required MultiSelectTypeOptionWidgetDataParser dataBuilder,
+    required MultiSelectTypeOptionWidgetDataParser dataParser,
     required TypeOptionDataController dataController,
   })  : service = TypeOptionService(
           gridId: dataController.gridId,
           fieldId: dataController.field.id,
         ),
-        super(dataParser: dataBuilder, dataController: dataController);
+        super(dataParser: dataParser, dataController: dataController);
 
   @override
   List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {

+ 4 - 4
frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart

@@ -14,12 +14,12 @@ class SingleSelectTypeOptionContext
 
   SingleSelectTypeOptionContext({
     required SingleSelectTypeOptionWidgetDataParser dataBuilder,
-    required TypeOptionDataController fieldContext,
+    required TypeOptionDataController dataController,
   })  : service = TypeOptionService(
-          gridId: fieldContext.gridId,
-          fieldId: fieldContext.field.id,
+          gridId: dataController.gridId,
+          fieldId: dataController.field.id,
         ),
-        super(dataParser: dataBuilder, dataController: fieldContext);
+        super(dataParser: dataBuilder, dataController: dataController);
 
   @override
   List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {

+ 329 - 0
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -0,0 +1,329 @@
+import 'dart:collection';
+import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
+import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
+import 'package:flutter/foundation.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+part 'row_cache.freezed.dart';
+
+typedef RowUpdateCallback = void Function();
+
+abstract class IGridRowFieldNotifier {
+  UnmodifiableListView<GridFieldPB> get fields;
+  void onRowFieldsChanged(VoidCallback callback);
+  void onRowFieldChanged(void Function(GridFieldPB) callback);
+  void onRowDispose();
+}
+
+/// Cache the rows in memory
+/// Insert / delete / update row
+///
+/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information.
+
+class GridRowCache {
+  final String gridId;
+  final GridBlockPB block;
+
+  /// _rows containers the current block's rows
+  /// Use List to reverse the order of the GridRow.
+  List<GridRowInfo> _rowInfos = [];
+
+  /// Use Map for faster access the raw row data.
+  final HashMap<String, GridRowPB> _rowByRowId;
+
+  final GridCellCache _cellCache;
+  final IGridRowFieldNotifier _fieldNotifier;
+  final _GridRowChangesetNotifier _rowChangeReasonNotifier;
+
+  UnmodifiableListView<GridRowInfo> get rows => UnmodifiableListView(_rowInfos);
+  GridCellCache get cellCache => _cellCache;
+
+  GridRowCache({
+    required this.gridId,
+    required this.block,
+    required IGridRowFieldNotifier notifier,
+  })  : _cellCache = GridCellCache(gridId: gridId),
+        _rowByRowId = HashMap(),
+        _rowChangeReasonNotifier = _GridRowChangesetNotifier(),
+        _fieldNotifier = notifier {
+    //
+    notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
+        .receive(const GridRowChangeReason.fieldDidChange()));
+    notifier.onRowFieldChanged((field) => _cellCache.remove(field.id));
+    _rowInfos = block.rows
+        .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble()))
+        .toList();
+  }
+
+  Future<void> dispose() async {
+    _fieldNotifier.onRowDispose();
+    _rowChangeReasonNotifier.dispose();
+    await _cellCache.dispose();
+  }
+
+  void applyChangesets(List<GridBlockChangesetPB> changesets) {
+    for (final changeset in changesets) {
+      _deleteRows(changeset.deletedRows);
+      _insertRows(changeset.insertedRows);
+      _updateRows(changeset.updatedRows);
+      _hideRows(changeset.hideRows);
+      _showRows(changeset.visibleRows);
+    }
+  }
+
+  void _deleteRows(List<String> deletedRows) {
+    if (deletedRows.isEmpty) {
+      return;
+    }
+
+    final List<GridRowInfo> newRows = [];
+    final DeletedIndexs deletedIndex = [];
+    final Map<String, String> deletedRowByRowId = {
+      for (var rowId in deletedRows) rowId: rowId
+    };
+
+    _rowInfos.asMap().forEach((index, row) {
+      if (deletedRowByRowId[row.id] == null) {
+        newRows.add(row);
+      } else {
+        _rowByRowId.remove(row.id);
+        deletedIndex.add(DeletedIndex(index: index, row: row));
+      }
+    });
+    _rowInfos = newRows;
+    _rowChangeReasonNotifier.receive(GridRowChangeReason.delete(deletedIndex));
+  }
+
+  void _insertRows(List<InsertedRowPB> insertRows) {
+    if (insertRows.isEmpty) {
+      return;
+    }
+
+    InsertedIndexs insertIndexs = [];
+    for (final insertRow in insertRows) {
+      final insertIndex = InsertedIndex(
+        index: insertRow.index,
+        rowId: insertRow.rowId,
+      );
+      insertIndexs.add(insertIndex);
+      _rowInfos.insert(insertRow.index,
+          (buildGridRow(insertRow.rowId, insertRow.height.toDouble())));
+    }
+
+    _rowChangeReasonNotifier.receive(GridRowChangeReason.insert(insertIndexs));
+  }
+
+  void _updateRows(List<UpdatedRowPB> updatedRows) {
+    if (updatedRows.isEmpty) {
+      return;
+    }
+
+    final UpdatedIndexs updatedIndexs = UpdatedIndexs();
+    for (final updatedRow in updatedRows) {
+      final rowId = updatedRow.rowId;
+      final index = _rowInfos.indexWhere((row) => row.id == rowId);
+      if (index != -1) {
+        _rowByRowId[rowId] = updatedRow.row;
+
+        _rowInfos.removeAt(index);
+        _rowInfos.insert(
+            index, buildGridRow(rowId, updatedRow.row.height.toDouble()));
+        updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
+      }
+    }
+
+    _rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs));
+  }
+
+  void _hideRows(List<String> hideRows) {}
+
+  void _showRows(List<String> visibleRows) {}
+
+  void onRowsChanged(
+    void Function(GridRowChangeReason) onRowChanged,
+  ) {
+    _rowChangeReasonNotifier.addListener(() {
+      onRowChanged(_rowChangeReasonNotifier.reason);
+    });
+  }
+
+  RowUpdateCallback addListener({
+    required String rowId,
+    void Function(GridCellMap, GridRowChangeReason)? onCellUpdated,
+    bool Function()? listenWhen,
+  }) {
+    listenerHandler() async {
+      if (listenWhen != null && listenWhen() == false) {
+        return;
+      }
+
+      notifyUpdate() {
+        if (onCellUpdated != null) {
+          final row = _rowByRowId[rowId];
+          if (row != null) {
+            final GridCellMap cellDataMap = _makeGridCells(rowId, row);
+            onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
+          }
+        }
+      }
+
+      _rowChangeReasonNotifier.reason.whenOrNull(
+        update: (indexs) {
+          if (indexs[rowId] != null) notifyUpdate();
+        },
+        fieldDidChange: () => notifyUpdate(),
+      );
+    }
+
+    _rowChangeReasonNotifier.addListener(listenerHandler);
+    return listenerHandler;
+  }
+
+  void removeRowListener(VoidCallback callback) {
+    _rowChangeReasonNotifier.removeListener(callback);
+  }
+
+  GridCellMap loadGridCells(String rowId) {
+    final GridRowPB? data = _rowByRowId[rowId];
+    if (data == null) {
+      _loadRow(rowId);
+    }
+    return _makeGridCells(rowId, data);
+  }
+
+  Future<void> _loadRow(String rowId) async {
+    final payload = GridRowIdPB.create()
+      ..gridId = gridId
+      ..blockId = block.id
+      ..rowId = rowId;
+
+    final result = await GridEventGetRow(payload).send();
+    result.fold(
+      (optionRow) => _refreshRow(optionRow),
+      (err) => Log.error(err),
+    );
+  }
+
+  GridCellMap _makeGridCells(String rowId, GridRowPB? row) {
+    var cellDataMap = GridCellMap.new();
+    for (final field in _fieldNotifier.fields) {
+      if (field.visibility) {
+        cellDataMap[field.id] = GridCellIdentifier(
+          rowId: rowId,
+          gridId: gridId,
+          field: field,
+        );
+      }
+    }
+    return cellDataMap;
+  }
+
+  void _refreshRow(OptionalRowPB optionRow) {
+    if (!optionRow.hasRow()) {
+      return;
+    }
+    final updatedRow = optionRow.row;
+    updatedRow.freeze();
+
+    _rowByRowId[updatedRow.id] = updatedRow;
+    final index =
+        _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id);
+    if (index != -1) {
+      // update the corresponding row in _rows if they are not the same
+      if (_rowInfos[index].rawRow != updatedRow) {
+        final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow);
+        _rowInfos.insert(index, row);
+
+        // Calculate the update index
+        final UpdatedIndexs updatedIndexs = UpdatedIndexs();
+        updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id);
+
+        //
+        _rowChangeReasonNotifier
+            .receive(GridRowChangeReason.update(updatedIndexs));
+      }
+    }
+  }
+
+  GridRowInfo buildGridRow(String rowId, double rowHeight) {
+    return GridRowInfo(
+      gridId: gridId,
+      blockId: block.id,
+      fields: _fieldNotifier.fields,
+      id: rowId,
+      height: rowHeight,
+    );
+  }
+}
+
+class _GridRowChangesetNotifier extends ChangeNotifier {
+  GridRowChangeReason reason = const InitialListState();
+
+  _GridRowChangesetNotifier();
+
+  void receive(GridRowChangeReason newReason) {
+    reason = newReason;
+    reason.map(
+      insert: (_) => notifyListeners(),
+      delete: (_) => notifyListeners(),
+      update: (_) => notifyListeners(),
+      fieldDidChange: (_) => notifyListeners(),
+      initial: (_) {},
+    );
+  }
+}
+
+@freezed
+class GridRowInfo with _$GridRowInfo {
+  const factory GridRowInfo({
+    required String gridId,
+    required String blockId,
+    required String id,
+    required UnmodifiableListView<GridFieldPB> fields,
+    required double height,
+    GridRowPB? rawRow,
+  }) = _GridRowInfo;
+}
+
+typedef InsertedIndexs = List<InsertedIndex>;
+typedef DeletedIndexs = List<DeletedIndex>;
+typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
+
+@freezed
+class GridRowChangeReason with _$GridRowChangeReason {
+  const factory GridRowChangeReason.insert(InsertedIndexs items) = _Insert;
+  const factory GridRowChangeReason.delete(DeletedIndexs items) = _Delete;
+  const factory GridRowChangeReason.update(UpdatedIndexs indexs) = _Update;
+  const factory GridRowChangeReason.fieldDidChange() = _FieldDidChange;
+  const factory GridRowChangeReason.initial() = InitialListState;
+}
+
+class InsertedIndex {
+  final int index;
+  final String rowId;
+  InsertedIndex({
+    required this.index,
+    required this.rowId,
+  });
+}
+
+class DeletedIndex {
+  final int index;
+  final GridRowInfo row;
+  DeletedIndex({
+    required this.index,
+    required this.row,
+  });
+}
+
+class UpdatedIndex {
+  final int index;
+  final String rowId;
+  UpdatedIndex({
+    required this.index,
+    required this.rowId,
+  });
+}

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart

@@ -65,7 +65,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder(
       );
     case FieldType.SingleSelect:
       final context = SingleSelectTypeOptionContext(
-        fieldContext: dataController,
+        dataController: dataController,
         dataBuilder: SingleSelectTypeOptionWidgetDataParser(),
       );
       return SingleSelectTypeOptionWidgetBuilder(
@@ -75,7 +75,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder(
     case FieldType.MultiSelect:
       final context = MultiSelectTypeOptionContext(
         dataController: dataController,
-        dataBuilder: MultiSelectTypeOptionWidgetDataParser(),
+        dataParser: MultiSelectTypeOptionWidgetDataParser(),
       );
       return MultiSelectTypeOptionWidgetBuilder(
         context,

+ 6 - 0
frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart

@@ -20,6 +20,12 @@ class Log {
     }
   }
 
+  static void warn(String? message) {
+    if (enableLog) {
+      debugPrint('🐛[Warn]=> $message');
+    }
+  }
+
   static void trace(String? message) {
     if (enableLog) {
       // debugPrint('❗️[Trace]=> $message');

+ 18 - 8
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart

@@ -56,25 +56,26 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
 
   /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the
   /// [fromIndex] equal to the [toIndex].
-  void move(int fromIndex, int toIndex) {
+  bool move(int fromIndex, int toIndex) {
     assert(fromIndex >= 0);
     assert(toIndex >= 0);
 
     if (fromIndex == toIndex) {
-      return;
+      return false;
     }
     Log.debug(
         '[$BoardColumnDataController] $columnData move item from $fromIndex to $toIndex');
     final item = columnData._items.removeAt(fromIndex);
     columnData._items.insert(toIndex, item);
     notifyListeners();
+    return true;
   }
 
   /// 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}) {
+  bool insert(int index, ColumnItem item, {bool notify = true}) {
     assert(index >= 0);
     Log.debug(
         '[$BoardColumnDataController] $columnData insert $item at $index');
@@ -85,9 +86,14 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
       columnData._items.add(item);
     }
 
-    if (notify) {
-      notifyListeners();
-    }
+    if (notify) notifyListeners();
+    return true;
+  }
+
+  bool add(ColumnItem item, {bool notify = true}) {
+    columnData._items.add(item);
+    if (notify) notifyListeners();
+    return true;
   }
 
   /// Replace the item at index with the [newItem].
@@ -107,14 +113,18 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin {
 }
 
 /// [BoardColumnData] represents the data of each Column of the Board.
-class BoardColumnData extends ReoderFlexItem with EquatableMixin {
+class BoardColumnData<CustomData> extends ReoderFlexItem with EquatableMixin {
   @override
   final String id;
+  final String desc;
   final List<ColumnItem> _items;
+  final CustomData? customData;
 
   BoardColumnData({
+    this.customData,
     required this.id,
-    required List<ColumnItem> items,
+    this.desc = "",
+    List<ColumnItem> items = const [],
   }) : _items = items;
 
   /// Returns the readonly List<ColumnItem>

+ 59 - 7
frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart

@@ -44,32 +44,84 @@ class BoardDataController extends ChangeNotifier
     this.onMoveColumnItemToColumn,
   });
 
-  void addColumn(BoardColumnData columnData) {
+  void addColumn(BoardColumnData columnData, {bool notify = true}) {
+    if (_columnControllers[columnData.id] != null) return;
+
     final controller = BoardColumnDataController(columnData: columnData);
     _columnDatas.add(columnData);
     _columnControllers[columnData.id] = controller;
+    if (notify) notifyListeners();
+  }
+
+  void addColumns(List<BoardColumnData> columns, {bool notify = true}) {
+    for (final column in columns) {
+      addColumn(column, notify: false);
+    }
+
+    if (columns.isNotEmpty && notify) notifyListeners();
+  }
+
+  void removeColumn(String columnId, {bool notify = true}) {
+    final index = _columnDatas.indexWhere((column) => column.id == columnId);
+    if (index == -1) {
+      Log.warn(
+          'Try to remove Column:[$columnId] failed. Column:[$columnId] not exist');
+    }
+
+    if (index != -1) {
+      _columnDatas.removeAt(index);
+      _columnControllers.remove(columnId);
+
+      if (notify) notifyListeners();
+    }
+  }
+
+  void removeColumns(List<String> columnIds, {bool notify = true}) {
+    for (final columnId in columnIds) {
+      removeColumn(columnId, notify: false);
+    }
+
+    if (columnIds.isNotEmpty && notify) notifyListeners();
   }
 
   BoardColumnDataController columnController(String columnId) {
     return _columnControllers[columnId]!;
   }
 
-  void moveColumn(int fromIndex, int toIndex) {
+  BoardColumnDataController? getColumnController(String columnId) {
+    final columnController = _columnControllers[columnId];
+    if (columnController == null) {
+      Log.warn('Column:[$columnId] \'s controller is not exist');
+    }
+
+    return columnController;
+  }
+
+  void moveColumn(int fromIndex, int toIndex, {bool notify = true}) {
     final columnData = _columnDatas.removeAt(fromIndex);
     _columnDatas.insert(toIndex, columnData);
     onMoveColumn?.call(fromIndex, toIndex);
-    notifyListeners();
+    if (notify) notifyListeners();
   }
 
   void moveColumnItem(String columnId, int fromIndex, int toIndex) {
-    final columnController = _columnControllers[columnId];
-    assert(columnController != null);
-    if (columnController != null) {
-      columnController.move(fromIndex, toIndex);
+    if (getColumnController(columnId)?.move(fromIndex, toIndex) ?? false) {
       onMoveColumnItem?.call(columnId, fromIndex, toIndex);
     }
   }
 
+  void addColumnItem(String columnId, ColumnItem item) {
+    getColumnController(columnId)?.add(item);
+  }
+
+  void insertColumnItem(String columnId, int index, ColumnItem item) {
+    getColumnController(columnId)?.insert(index, item);
+  }
+
+  void removeColumnItem(String columnId, String itemId) {
+    getColumnController(columnId)?.removeWhere((item) => item.id == itemId);
+  }
+
   @override
   @protected
   void swapColumnItem(