row_cache.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import 'dart:collection';
  2. import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
  3. import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
  4. import 'package:flowy_sdk/dispatch/dispatch.dart';
  5. import 'package:flowy_sdk/log.dart';
  6. import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
  7. import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
  8. import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
  9. import 'package:flutter/foundation.dart';
  10. import 'package:freezed_annotation/freezed_annotation.dart';
  11. part 'row_cache.freezed.dart';
  12. typedef RowUpdateCallback = void Function();
  13. abstract class IGridRowFieldNotifier {
  14. UnmodifiableListView<GridFieldContext> get fields;
  15. void onRowFieldsChanged(VoidCallback callback);
  16. void onRowFieldChanged(void Function(FieldPB) callback);
  17. void onRowDispose();
  18. }
  19. /// Cache the rows in memory
  20. /// Insert / delete / update row
  21. ///
  22. /// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information.
  23. class GridRowCache {
  24. final String gridId;
  25. final BlockPB block;
  26. /// _rows containers the current block's rows
  27. /// Use List to reverse the order of the GridRow.
  28. List<RowInfo> _rowInfos = [];
  29. /// Use Map for faster access the raw row data.
  30. final HashMap<String, RowInfo> _rowInfoByRowId;
  31. final GridCellCache _cellCache;
  32. final IGridRowFieldNotifier _fieldNotifier;
  33. final _RowChangesetNotifier _rowChangeReasonNotifier;
  34. UnmodifiableListView<RowInfo> get visibleRows {
  35. var visibleRows = [..._rowInfos];
  36. visibleRows.retainWhere((element) => element.visible);
  37. return UnmodifiableListView(visibleRows);
  38. }
  39. GridCellCache get cellCache => _cellCache;
  40. GridRowCache({
  41. required this.gridId,
  42. required this.block,
  43. required IGridRowFieldNotifier notifier,
  44. }) : _cellCache = GridCellCache(gridId: gridId),
  45. _rowInfoByRowId = HashMap(),
  46. _rowChangeReasonNotifier = _RowChangesetNotifier(),
  47. _fieldNotifier = notifier {
  48. //
  49. notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
  50. .receive(const RowsChangedReason.fieldDidChange()));
  51. notifier.onRowFieldChanged(
  52. (field) => _cellCache.removeCellWithFieldId(field.id));
  53. for (final row in block.rows) {
  54. final rowInfo = buildGridRow(row);
  55. _rowInfos.add(rowInfo);
  56. _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
  57. }
  58. }
  59. Future<void> dispose() async {
  60. _fieldNotifier.onRowDispose();
  61. _rowChangeReasonNotifier.dispose();
  62. await _cellCache.dispose();
  63. }
  64. void applyChangesets(GridBlockChangesetPB changeset) {
  65. _deleteRows(changeset.deletedRows);
  66. _insertRows(changeset.insertedRows);
  67. _updateRows(changeset.updatedRows);
  68. _hideRows(changeset.invisibleRows);
  69. _showRows(changeset.visibleRows);
  70. }
  71. void _deleteRows(List<String> deletedRows) {
  72. if (deletedRows.isEmpty) {
  73. return;
  74. }
  75. final List<RowInfo> newRows = [];
  76. final DeletedIndexs deletedIndex = [];
  77. final Map<String, String> deletedRowByRowId = {
  78. for (var rowId in deletedRows) rowId: rowId
  79. };
  80. _rowInfos.asMap().forEach((index, RowInfo rowInfo) {
  81. if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
  82. newRows.add(rowInfo);
  83. } else {
  84. _rowInfoByRowId.remove(rowInfo.rowPB.id);
  85. deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
  86. }
  87. });
  88. _rowInfos = newRows;
  89. _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
  90. }
  91. void _insertRows(List<InsertedRowPB> insertRows) {
  92. if (insertRows.isEmpty) {
  93. return;
  94. }
  95. InsertedIndexs insertIndexs = [];
  96. for (final InsertedRowPB insertRow in insertRows) {
  97. final insertIndex = InsertedIndex(
  98. index: insertRow.index,
  99. rowId: insertRow.row.id,
  100. );
  101. insertIndexs.add(insertIndex);
  102. final rowInfo = buildGridRow(insertRow.row);
  103. _rowInfos.insert(insertRow.index, rowInfo);
  104. _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
  105. }
  106. _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
  107. }
  108. void _updateRows(List<RowPB> updatedRows) {
  109. if (updatedRows.isEmpty) {
  110. return;
  111. }
  112. final UpdatedIndexs updatedIndexs = UpdatedIndexs();
  113. for (final RowPB updatedRow in updatedRows) {
  114. final rowId = updatedRow.id;
  115. final index = _rowInfos.indexWhere(
  116. (rowInfo) => rowInfo.rowPB.id == rowId,
  117. );
  118. if (index != -1) {
  119. final rowInfo = buildGridRow(updatedRow);
  120. _rowInfoByRowId[rowId] = rowInfo;
  121. _rowInfos.removeAt(index);
  122. _rowInfos.insert(index, rowInfo);
  123. updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
  124. }
  125. }
  126. _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
  127. }
  128. void _hideRows(List<String> invisibleRows) {
  129. for (final rowId in invisibleRows) {
  130. _rowInfoByRowId[rowId]?.visible = false;
  131. }
  132. if (invisibleRows.isNotEmpty) {
  133. _rowChangeReasonNotifier
  134. .receive(const RowsChangedReason.filterDidChange());
  135. }
  136. }
  137. void _showRows(List<String> visibleRows) {
  138. for (final rowId in visibleRows) {
  139. _rowInfoByRowId[rowId]?.visible = true;
  140. }
  141. if (visibleRows.isNotEmpty) {
  142. _rowChangeReasonNotifier
  143. .receive(const RowsChangedReason.filterDidChange());
  144. }
  145. }
  146. void onRowsChanged(void Function(RowsChangedReason) onRowChanged) {
  147. _rowChangeReasonNotifier.addListener(() {
  148. onRowChanged(_rowChangeReasonNotifier.reason);
  149. });
  150. }
  151. RowUpdateCallback addListener({
  152. required String rowId,
  153. void Function(GridCellMap, RowsChangedReason)? onCellUpdated,
  154. bool Function()? listenWhen,
  155. }) {
  156. listenerHandler() async {
  157. if (listenWhen != null && listenWhen() == false) {
  158. return;
  159. }
  160. notifyUpdate() {
  161. if (onCellUpdated != null) {
  162. final rowInfo = _rowInfoByRowId[rowId];
  163. if (rowInfo != null) {
  164. final GridCellMap cellDataMap =
  165. _makeGridCells(rowId, rowInfo.rowPB);
  166. onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
  167. }
  168. }
  169. }
  170. _rowChangeReasonNotifier.reason.whenOrNull(
  171. update: (indexs) {
  172. if (indexs[rowId] != null) notifyUpdate();
  173. },
  174. fieldDidChange: () => notifyUpdate(),
  175. );
  176. }
  177. _rowChangeReasonNotifier.addListener(listenerHandler);
  178. return listenerHandler;
  179. }
  180. void removeRowListener(VoidCallback callback) {
  181. _rowChangeReasonNotifier.removeListener(callback);
  182. }
  183. GridCellMap loadGridCells(String rowId) {
  184. final RowPB? data = _rowInfoByRowId[rowId]?.rowPB;
  185. if (data == null) {
  186. _loadRow(rowId);
  187. }
  188. return _makeGridCells(rowId, data);
  189. }
  190. Future<void> _loadRow(String rowId) async {
  191. final payload = RowIdPB.create()
  192. ..gridId = gridId
  193. ..blockId = block.id
  194. ..rowId = rowId;
  195. final result = await GridEventGetRow(payload).send();
  196. result.fold(
  197. (optionRow) => _refreshRow(optionRow),
  198. (err) => Log.error(err),
  199. );
  200. }
  201. GridCellMap _makeGridCells(String rowId, RowPB? row) {
  202. // ignore: prefer_collection_literals
  203. var cellDataMap = GridCellMap();
  204. for (final field in _fieldNotifier.fields) {
  205. if (field.visibility) {
  206. cellDataMap[field.id] = GridCellIdentifier(
  207. rowId: rowId,
  208. gridId: gridId,
  209. fieldContext: field,
  210. );
  211. }
  212. }
  213. return cellDataMap;
  214. }
  215. void _refreshRow(OptionalRowPB optionRow) {
  216. if (!optionRow.hasRow()) {
  217. return;
  218. }
  219. final updatedRow = optionRow.row;
  220. updatedRow.freeze();
  221. final index =
  222. _rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id);
  223. if (index != -1) {
  224. // update the corresponding row in _rows if they are not the same
  225. if (_rowInfos[index].rowPB != updatedRow) {
  226. final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow);
  227. _rowInfos.insert(index, rowInfo);
  228. _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
  229. // Calculate the update index
  230. final UpdatedIndexs updatedIndexs = UpdatedIndexs();
  231. updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
  232. index: index,
  233. rowId: rowInfo.rowPB.id,
  234. );
  235. //
  236. _rowChangeReasonNotifier
  237. .receive(RowsChangedReason.update(updatedIndexs));
  238. }
  239. }
  240. }
  241. RowInfo buildGridRow(RowPB rowPB) {
  242. return RowInfo(
  243. gridId: gridId,
  244. fields: _fieldNotifier.fields,
  245. rowPB: rowPB,
  246. visible: true,
  247. );
  248. }
  249. }
  250. class _RowChangesetNotifier extends ChangeNotifier {
  251. RowsChangedReason reason = const InitialListState();
  252. _RowChangesetNotifier();
  253. void receive(RowsChangedReason newReason) {
  254. reason = newReason;
  255. reason.map(
  256. insert: (_) => notifyListeners(),
  257. delete: (_) => notifyListeners(),
  258. update: (_) => notifyListeners(),
  259. fieldDidChange: (_) => notifyListeners(),
  260. initial: (_) {},
  261. filterDidChange: (_FilterDidChange value) => notifyListeners(),
  262. );
  263. }
  264. }
  265. @unfreezed
  266. class RowInfo with _$RowInfo {
  267. factory RowInfo({
  268. required String gridId,
  269. required UnmodifiableListView<GridFieldContext> fields,
  270. required RowPB rowPB,
  271. required bool visible,
  272. }) = _RowInfo;
  273. }
  274. typedef InsertedIndexs = List<InsertedIndex>;
  275. typedef DeletedIndexs = List<DeletedIndex>;
  276. typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
  277. @freezed
  278. class RowsChangedReason with _$RowsChangedReason {
  279. const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert;
  280. const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete;
  281. const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update;
  282. const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
  283. const factory RowsChangedReason.filterDidChange() = _FilterDidChange;
  284. const factory RowsChangedReason.initial() = InitialListState;
  285. }
  286. class InsertedIndex {
  287. final int index;
  288. final String rowId;
  289. InsertedIndex({
  290. required this.index,
  291. required this.rowId,
  292. });
  293. }
  294. class DeletedIndex {
  295. final int index;
  296. final RowInfo row;
  297. DeletedIndex({
  298. required this.index,
  299. required this.row,
  300. });
  301. }
  302. class UpdatedIndex {
  303. final int index;
  304. final String rowId;
  305. UpdatedIndex({
  306. required this.index,
  307. required this.rowId,
  308. });
  309. }