import 'dart:async'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'block/block_cache.dart'; import 'grid_service.dart'; import 'row/row_service.dart'; import 'dart:collection'; part 'grid_bloc.freezed.dart'; class GridBloc extends Bloc { final String gridId; final GridService _gridService; final GridFieldCache fieldCache; // key: the block id final LinkedHashMap _blocks; List get rowInfos { final List rows = []; for (var block in _blocks.values) { rows.addAll(block.rows); } return rows; } GridBloc({required ViewPB view}) : gridId = view.id, _blocks = LinkedHashMap.identity(), _gridService = GridService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id), super(GridState.initial(view.id)) { on( (event, emit) async { await event.when( initial: () async { _startListening(); await _loadGrid(emit); }, createRow: () { _gridService.createRow(); }, didReceiveRowUpdate: (newRowInfos, reason) { emit(state.copyWith(rowInfos: newRowInfos, reason: reason)); }, didReceiveFieldUpdate: (fields) { emit(state.copyWith(rowInfos: rowInfos, fields: GridFieldEquatable(fields))); }, ); }, ); } @override Future close() async { await _gridService.closeGrid(); await fieldCache.dispose(); for (final blockCache in _blocks.values) { blockCache.dispose(); } return super.close(); } GridRowCache? getRowCache(String blockId, String rowId) { final GridBlockCache? blockCache = _blocks[blockId]; return blockCache?.rowCache; } void _startListening() { fieldCache.addListener( listenWhen: () => !isClosed, onFields: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)), ); } Future _loadGrid(Emitter emit) async { final result = await _gridService.loadGrid(); return Future( () => result.fold( (grid) async { _initialBlocks(grid.blocks); await _loadFields(grid, emit); }, (err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))), ), ); } Future _loadFields(GridPB grid, Emitter emit) async { final result = await _gridService.getFields(fieldIds: grid.fields); return Future( () => result.fold( (fields) { fieldCache.fields = fields.items; emit(state.copyWith( grid: Some(grid), fields: GridFieldEquatable(fieldCache.fields), rowInfos: rowInfos, loadingState: GridLoadingState.finish(left(unit)), )); }, (err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))), ), ); } void _initialBlocks(List blocks) { for (final block in blocks) { if (_blocks[block.id] != null) { Log.warn("Intial duplicate block's cache: ${block.id}"); return; } final cache = GridBlockCache( gridId: gridId, block: block, fieldCache: fieldCache, ); cache.addListener( listenWhen: () => !isClosed, onChangeReason: (reason) => add(GridEvent.didReceiveRowUpdate(rowInfos, reason)), ); _blocks[block.id] = cache; } } } @freezed class GridEvent with _$GridEvent { const factory GridEvent.initial() = InitialGrid; const factory GridEvent.createRow() = _CreateRow; const factory GridEvent.didReceiveRowUpdate(List rows, GridRowChangeReason listState) = _DidReceiveRowUpdate; const factory GridEvent.didReceiveFieldUpdate(List fields) = _DidReceiveFieldUpdate; } @freezed class GridState with _$GridState { const factory GridState({ required String gridId, required Option grid, required GridFieldEquatable fields, required List rowInfos, required GridLoadingState loadingState, required GridRowChangeReason reason, }) = _GridState; factory GridState.initial(String gridId) => GridState( fields: const GridFieldEquatable([]), rowInfos: [], grid: none(), gridId: gridId, loadingState: const _Loading(), reason: const InitialListState(), ); } @freezed class GridLoadingState with _$GridLoadingState { const factory GridLoadingState.loading() = _Loading; const factory GridLoadingState.finish(Either successOrFail) = _Finish; } class GridFieldEquatable extends Equatable { final List _fields; const GridFieldEquatable(List fields) : _fields = fields; @override List get props { return [ _fields.length, _fields.map((field) => field.width).reduce((value, element) => value + element), ]; } UnmodifiableListView get value => UnmodifiableListView(_fields); }