bcell 1.1.0 copy "bcell: ^1.1.0" to clipboard
bcell: ^1.1.0 copied to clipboard

A Flutter data-grid widget with a pluto_grid-shaped API and spreadsheet features: a formula engine, multi-sheet workbooks, charts, pivot tables, and CSV/XLSX/PDF import-export.

bcell #

A data grid for Flutter with a pluto_grid-shaped API that has grown into a spreadsheet-capable widget: sortable and resizable columns, tap-to-edit cells and responsive auto-sizing, a formula engine, multi-sheet workbooks, charts and pivot tables, and a broad import/export surface (CSV/TSV/HTML/XML/Markdown/JSON/PDF/XLSX). Styling resolves from your app's ThemeData, so light mode, dark mode, and high-contrast themes work with zero configuration.

Live demo — interactive Widgetbook playground — every feature below, with knobs, theme switching, and device viewports.

Features #

  • Sorting — tap a header to cycle ascending → descending → original order, or stateManager.sortByColumns([...]) for multi-column priority sort with tie-breaking.
  • AggregationsstateManager.sum/average/min/max/count('field') over the filtered rows (spreadsheet-footer summaries).
  • ChartsBCellChart(stateManager, type: BCellChartType.line/.bar/.pie, categoryField: 'month', valueFields: ['sales', 'cost']) — a companion widget (same calling shape as BCellPagination) that plots the grid's filtered rows and re-draws live on edit/filter/sort; formula cells chart their computed value. Backed by fl_chart; drop to it directly for stacked/area/scatter and other advanced kinds.
  • Pivot tablesstateManager.pivot(rowField: 'region', columnField: 'quarter', valueField: 'sales', aggregate: BCellPivotAgg.sum) groups the filtered rows into a rows × columns matrix of aggregated values (sum/count/average/min/max) with value(r, c), rowTotal/columnTotal/grandTotal. Values read through the formula engine; totals re-aggregate the raw numbers (so an average total is a true mean, not a mean of means). .toGrid() renders the result straight into BCellGrid(columns:, rows:) columns+rows (row-field header + a column per key + optional totals row/column). Single row/column field, keys in first-appearance order — no multi-level nesting or slicers.
  • Column resizing — drag a header's right edge. Modes: normal, pushAndPull (total width stays constant, cascades across columns), or none.
  • Inline editing — tap a cell to select, tap again to edit. Number columns validate input and keep the old value when parsing fails.
  • Column typesBCellColumnType.text(), .number() (right-aligned, numeric sort/coerce; optional Excel-style format: code — #,##0.00, 0%, $#,##0.00, 000 — applies display grouping/decimals/percent/prefix while cells stay raw numbers so sort/export are unaffected), and .date() (holds a DateTime; displays yyyy-MM-dd by default or a custom format: callback; editing opens a native date picker; import/paste coerce ISO strings and Excel date serials into DateTime via BCellColumnTypeDate.tryParse/fromExcelSerial).
  • Custom cell rendererBCellColumn(renderer: (ctx) => Widget) draws any widget in place of the default cell text (badges, icons, colored text); ctx carries the column, row, cell, and formatted value. Editing still uses the inline editor.
  • Conditional formattingBCellColumn(cellStyle: (ctx) => BCellStyle?) colors a cell (backgroundColor / textStyle) by its value, e.g. red text for negatives. Lighter than renderer when you only need color; selection/range highlight still paints on top.
  • Conditional-formatting visualsBCellConditionalFormat.colorScale/dataBar/iconSet(stateManager).toRenderer() produces a renderer for a numeric column: a two/three-stop color-scale background, a proportional data bar behind the value, or a threshold-bucketed leading icon (default up/flat/down arrows). Min/max are read from the column's current displayed rows; non-numeric/null cells render plainly. Colors are overridable and default to theme-friendly values.
  • Banded rows (zebra striping)BCellGridStyleConfig(oddRowColor: ...) tints odd-indexed rows for readability; null (default) disables it and every row uses rowColor. Display-only and virtualization-safe (one isOdd check per built row), so the 10k-row scroll is unaffected.
  • Padding, icon & fill-handle stylingBCellGridStyleConfig also exposes defaultColumnTitlePadding / defaultCellPadding (header/cell insets), iconSize / iconColor (the header sort icon; a null color inherits the ambient IconTheme), and fillHandleSize / fillHandleColor / fillHandleBorderColor (the drag-fill square on the focused cell; color defaults to activatedColor, its outline to colorScheme.surface). The first four match pluto_grid's field names; defaults reproduce the previous look.
  • Named rangesstateManager.defineName('SALES', 'A2:A10') maps a name to an A1 ref or range so formulas read =SUM(SALES) or =PRICE*QTY instead of raw addresses. Names are case-insensitive and resolved by rewriting the formula to A1 before evaluation, so display, sort, and export all honour them; removeName/clearNames/definedNames/namedRange round it out. Zero-cost when unused (the formula path skips substitution until the first name is defined).
  • Structured referencesstateManager.enableStructuredRefs() lets a formula name a column with [Title] (or Table[Title] — the table-name prefix is decorative, the grid holds one table) and get its whole data range: =SUM([Sales]) instead of =SUM(C1:C40). The bracketed title is case-insensitive and resolves live to the column's current-displayed-rows range, so filtering/paging and row add/remove stay correct. Composes with named ranges (both work at once). Whole-column only — no [@Col] this-row refs (they'd need the calling cell's row, which the context-free evaluator doesn't carry).
  • Hyperlink cellsBCellColumn(renderer: BCellHyperlink.column(onTap: (url) {...}).toRenderer()) renders a URL cell as styled, tappable link text (theme primary + underline, Semantics(link: true), 44px min tap target). Optional url/label extractors split display text from the address (e.g. a "Label|https://…" value). Null/empty cells render plain with no tap. No dependency — you wire url_launcher (or anything) in onTap.
  • Rich text in cells — set a cell value to a BCellRichText([BCellTextRun('bold', bold: true), BCellTextRun(' plain')]) and give the column renderer: BCellRichText.column().toRenderer() (same shape as the hyperlink / conditional-format helpers). Runs carry bold/italic/underline/color and render on one line via Text.rich. Exports flatten to plain text automatically (toString), so CSV/PDF/etc. emit the flat value with no markup leak; a plain string in the same column still renders normally. One-line inline runs only — no rich editor.
  • Per-row heightBCellRow(height: 90) overrides that row's height; null rows use the grid default. Virtualization is preserved (itemExtentBuilder), so the 10k-row scroll stays O(1).
  • Freeze columnsBCellColumn(frozen: BCellColumnFrozen.start) / .end pins a column to the left or right in a non-scrolling section while the middle scrolls horizontally; all sections stay virtualized and scroll vertically together. Frozen columns are still visible columns for export and keyboard nav.
  • Merge cellsBCellValue(value: ..., colspan: 2, rowspan: 2) merges a cell across N columns and/or rows (combined width/height; covered cells draw nothing). Display-only: the data model keeps every cell, so selection, export, and nav are unaffected. Colspan stays on the virtualized path; using rowspan switches to a non-virtualized body (fine for small merged tables, not 10k rows).
  • Drag-fill — a fill handle on the selected cell's bottom-right corner; drag it down to fill that value down the column (uses fillSelection under the hood).
  • Series / auto fillstateManager.fillSeriesDown() reads the leading seed cells of a selection and extends the pattern into the empty cells below instead of copying: numeric seeds step arithmetically (1, 3 → 5, 7, 9; a lone number steps +1), date columns step by the seed's day-difference (a lone date +1 day), and non-numeric seeds fall back to copying. Every value is coerced to the column type; read-only cells are skipped.
  • Flash fillstateManager.flashFillDown() infers a transform from a column's seed cell(s) against the row's other columns and fills the rest: e.g. a "Full name" column with a seed John beside it detects "the text before the first space" and fills each row's first name; also splits an email at @, copies a column, etc. Multiple seed rows disambiguate; if no single transform explains the seeds it's a no-op. (Copy + delimiter-split only — no regex / template inference.)
  • Responsive auto-sizingBCellAutoSizeMode.equal or .scale fits columns to any viewport (rotation, window resize, split view). Pin a column with suppressedAutoSize: true. Falls back to horizontal scrolling below minWidth.
  • FilteringstateManager.setFilter((row) => ...); source rows are untouched, clearing restores everything.
  • PagingstateManager.setPageSize(20) plus the ready-made BCellPagination(stateManager) pager widget.
  • Column hidingstateManager.hideColumn(column, true); hidden columns are skipped by the grid, CSV, and navigation.
  • Row operationsinsertRows/prependRows/appendRows/removeRows/removeCurrentRow, with currentRow/currentRowIdx getters.
  • Keyboard navigation — arrows/Tab move the selection, Enter/F2 edits, Escape cancels an edit without committing.
  • CSV exportstateManager.toCsv() (visible columns, filtered rows, all pages), with opt-in formula-injection sanitizing for untrusted data.
  • PDF exportawait stateManager.toPdf(title: 'People') returns PDF bytes (visible columns, filtered rows) ready for printing, share, or file write. Pass selectionOnly: true for a print area — only the selected range's columns and rows (via stateManager.selectionRect); falls back to the whole grid when nothing is selected.
  • JSON exportstateManager.toJson() returns List<Map<String, dynamic>> (visible columns, filtered rows), ready for jsonEncode.
  • TSV / HTML exportstateManager.toTsv() (tab-separated, clipboard-friendly, with the same opt-in sanitizeFormulas guard as CSV) and toHtml() (a <table>; every value HTML-escaped against XSS).
  • TSV / CSV / HTML / Markdown importstateManager.importTsv(text, hasHeader: ...) / importCsv(...) / importHtml(...) / importMarkdown(...) parse a delimited, <table>, or | pipe | block and append rows, mapping columns left-to-right onto visible columns and coercing number columns (the inverse of toTsv/toCsv/toHtml/toMarkdown; CSV uses an RFC 4180 quote-aware parser, HTML unescapes entities, Markdown drops the --- separator and unescapes \|).
  • JSON importstateManager.importJson(list) (inverse of toJson) appends List<Map<String, dynamic>> rows, mapping each map onto the visible columns by field name and coercing values to each column type.
  • XML importstateManager.importXml(text) (inverse of toXml) parses <row> elements and maps their <field> children onto the visible columns by tag name (self-describing, like JSON), XML-unescaping values and coercing to each column type.
  • XML exportstateManager.toXml() (one <row> per row, <field> elements; every value XML-escaped against injection).
  • Markdown exportstateManager.toMarkdown() (GitHub-flavored | col | table; pipes and newlines in values sanitized).
  • XLSX import/exportstateManager.toXlsx() returns .xlsx bytes (visible columns header + filtered rows; number columns write numeric cells, date columns write native date cells, formulas export their evaluated result) and importXlsx(bytes, {hasHeader = true}) reads the first sheet back onto the visible columns, coercing numbers/dates. Backed by the excel package.
  • Undo/redostateManager.undo()/redo() with canUndo/canRedo for cell edits; a fresh edit clears the redo stack.
  • Find & Go TostateManager.find('query') returns matching cells; goToCell(rowIdx, field) / goToRow(rowIdx) jump the selection to a cell.
  • Cell notessetNote/noteFor/hasNote/setCurrentCellNote/clearNotes attach a text comment to any cell.
  • Formulas — a cell whose value is =... is a formula: =A1+B2*3, =SUM(A1:B10), =IF(A1>0, A1, 0), ="n=" & B1. Supports arithmetic + - * / ( ), comparisons, & concat and string literals, A1 references + A1:B2 ranges (columns = visible columns, rows 1-based), and functions SUM/AVERAGE/MIN/MAX/MEDIAN/COUNT/COUNTA/SUMIF/COUNTIF, ROUND/ROUNDUP/ROUNDDOWN/TRUNC/CEILING/FLOOR/ABS/INT/SIGN/MOD/POWER/SQRT/PI, IF/AND/OR/NOT/IFERROR, UPPER/LOWER/LEFT/RIGHT/MID/LEN/TRIM/CONCAT/SUBSTITUTE/REPT/EXACT (SUMIF/COUNTIF take a value or an operator criterion like ">5"). The library also ships Date & Time (TODAY/NOW/DATE/YEAR/MONTH/DAY/HOUR/MINUTE/SECOND/WEEKDAY/DATEDIF/EDATE/EOMONTH), Statistical (VAR/STDEV/VARP/STDEVP/MODE/LARGE/SMALL/PERCENTILE/QUARTILE/RANK), Financial (PMT/FV/PV/NPV/IRR), Lookup (MATCH/INDEX/VLOOKUP/HLOOKUP), Information (ISBLANK/ISNUMBER/ISTEXT/ISERROR/ISNA/NA/N/TYPE/IFNA), Logical (XOR/IFS/SWITCH), Math/Trig (SIN/COS/TAN/EXP/LN/LOG10/LOG/GCD/LCM/SUMPRODUCT), multi-criteria aggregates (SUMIFS/COUNTIFS/AVERAGEIFS/AVERAGEIF/COUNTBLANK), and more text (FIND/SEARCH/REPLACE/VALUE/PROPER/TEXT/TEXTJOIN/CHOOSE). The cell shows the evaluated result (raw formula stays editable); sort and every export use the evaluated value too; a bad formula shows #DIV/0! / #NAME? / #CIRC! / #NUM!. Evaluated results are memoized per notification (keyed by the raw formula string) and the cache clears on any edit/sort/filter/row change, so paint, sort, and export don't re-walk the same formula repeatedly. Spreadsheet-pivot feature — no dependency graph / array formulas / incremental recalc yet.
  • Multi-sheet workbookBCellWorkbook holds an ordered list of named BCellSheets (each = title + columns + rows) with one active sheet and add/remove/rename/select/move/setTabColor/setProtected/setHidden ops. BCellSheetTabBar(workbook: ...) renders the tab strip (theme-resolved style, active tab highlighted, tap switches; long-press-drag a tab onto another to reorder; a per-sheet tabColor shows as an Excel-style accent underline; a hidden sheet gets no tab) and BCellWorkbookView(workbook: ...) shows the active sheet as a BCellGrid; a protected sheet is shown read-only, and setProtected(i, true, password: ...) locks the toggle behind an Excel-style optional password (unprotecting needs the matching password; only a SHA-256 hash is stored and persisted, never the plaintext). Hiding keeps at least one visible sheet and never leaves the active sheet hidden. A thin wrapper over BCellGrid — no grid internals. BCellWorkbook.toJson() / BCellWorkbook.fromJson(map) persist the whole workbook (sheets, tab colors, protected/hidden flags, column definitions incl. number-format codes, row data) to a jsonEncode-ready map and back; function-based column config (validators/renderers) and cell merges don't round-trip; loadJson(map) loads one in place (listeners survive). Version history: saveVersion({name}) snapshots the workbook (in-memory, built on toJson), versions lists them, restoreVersion(i) rolls the workbook back in place, removeVersion/clearVersions manage the list.
  • Encrypted workbook fileworkbook.toEncryptedBytes(password) seals the toJson() snapshot into an authenticated AES-256-GCM blob (key from PBKDF2-HMAC-SHA256 with a random per-file salt and a 100k-iteration default; fresh random nonce per call, so the same workbook never encrypts to the same bytes). decryptWorkbook(bytes, password) opens it into a new workbook and loadEncryptedBytes(bytes, password) loads it in place; a wrong password or a tampered byte throws BCellDecryptException — never silent garbage. The blob is self-describing (version, salt, iteration count, nonce ride along), so old files keep opening if the defaults change. Whole-file encryption only; what toJson() doesn't persist isn't protected either.
  • Shared-workbook mergeworkbook.mergeChanges(base: baseJson, theirs: theirJson) folds another copy's edits into this workbook (Excel's "Compare and Merge Workbooks"): a three-way, cell-level merge over toJson() snapshots. A cell only the other copy changed lands here; a cell both copies changed differently keeps the local value and is returned as a BCellMergeConflict (sheet, row, field, base/mine/theirs values) so the host can offer "take theirs". Rows the other copy appended and sheets it added come along. Rows align by index and sheets by name (mid-sheet inserts/deletes or renames in either copy can mis-merge below them); deletions and sheet metadata don't propagate; snapshot transport between users is the host's job.
  • Read-only gridBCellGrid(mode: BCellGridMode.readOnly) (or stateManager.setGridMode(BCellGridMode.readOnly), mirroring pluto_grid's mode) blocks every edit — typing, paste, fill, drag-fill — plus structural row changes (insert/remove/import), so a protected sheet can't be mutated. Selection, sort, filter, and export still work. Column-level BCellColumn(readOnly: true) still applies on top in normal mode. Drives workbook sheet protection.
  • Shared font — pass a base font: (a TextStyle) to any BCellGrid/BCellPagination/BCellSheetTabBar for a per-widget font, or drive them all from one font bus: create a BCellFontController, and either wrap a subtree in BCellFontScope(controller: ...) or hand the controller to widgets via fontController:. Calling controller.setFont(...) re-fonts every subscribed BCell widget live — one place to theme all BCell text. Precedence: per-widget font → controller → scope → ThemeData; the existing columnTextStyle/cellTextStyle still override per field.
  • Data validation — per-column validator: (value) => errorOrNull; a rejected edit keeps the old value (enforced centrally, so it also guards paste). changeCellValue(..., onInvalid: ...) surfaces the error.
  • Range selection — Shift+click / Shift+Arrow selects a cell rectangle; extendSelectionTo(cell), isInRange(cell), selectedCells, rangeAsTsv(). In-range cells highlight. fillSelection(value) writes one value across the range, fillDown() (Ctrl+D) / fillRight() (Ctrl+R) copy the top-row / left-column value across the range, and clearSelection() (Delete/Backspace) blanks it — all skip read-only columns.
  • ClipboardcopySelectionToClipboard() copies the selection (a range as a TSV block, else the focus cell), cutSelectionToClipboard() copies then clears the cell, and pasteFromClipboard() pastes at the current cell — a TSV block spills down and right, clipped to the grid (number columns coerce like the editor).
  • Row selectionsetRowChecked/toggleRowChecked/setAllRowsChecked with checkedRows/isAllChecked/isAnyChecked for checkbox-style multi-row selection.
  • Performance — virtualized rows (ListView.builder + itemExtentBuilder, so per-row heights stay O(1)), O(1) cell position lookup, selection changes rebuild only the affected cells. A 10k-row regression test guards this.

Requirements #

  • Flutter SDK with Dart ^3.8.1
  • Runtime dependencies (resolved automatically by pub): pdf (toPdf), excel (toXlsx/importXlsx), fl_chart (BCellChart), and crypto + pointycastle (sheet-protection password hash and encrypted-workbook files)

How to run this project #

Clone, then from the repo root:

flutter pub get                          # resolve dependencies
flutter test                             # run the test suite
flutter analyze                          # lint (strict analyzer modes are enabled)

Run the example app #

cd example
flutter pub get
flutter run                              # pick any connected device

Run the Widgetbook playground #

Interactive playground with knobs (row count, row height, column width, read-only, auto-size mode, resize mode), theme switching (auto / light / dark / high-contrast), and device viewports (phone → tablet):

cd widgetbook
flutter pub get
flutter run -d chrome

Usage #

import 'package:bcell/bcell.dart';

BCellGrid(
  columns: [
    BCellColumn(title: 'Id', field: 'id', type: BCellColumnType.number(), width: 80),
    BCellColumn(title: 'Name', field: 'name', type: BCellColumnType.text()),
  ],
  rows: [
    BCellRow(cells: {
      'id': BCellValue(value: 1),
      'name': BCellValue(value: 'Ada Lovelace'),
    }),
  ],
  onLoaded: (event) => stateManager = event.stateManager,
  onChanged: (event) => print('${event.oldValue} -> ${event.value}'),
  configuration: const BCellGridConfiguration(
    columnSize: BCellGridColumnSizeConfig(
      autoSizeMode: BCellAutoSizeMode.scale,     // responsive: fill the viewport
      resizeMode: BCellResizeMode.pushAndPull,   // resizing keeps total width
    ),
  ),
)

After the grid mounts, columns and rows are owned by the state manager (same contract as pluto_grid): change data through it — appendRows, removeRows, changeCellValue, toCsv() — obtained from onLoaded.

  • Event bus — beyond the four construction-time callbacks (onLoaded/onChanged/onSelected/onSorted), stateManager.eventManager is a single broadcast stream of every BCellGridEvent, so a consumer can subscribe/unsubscribe at runtime from one place (mirrors pluto_grid's eventManager): final sub = stateManager.eventManager.listener((e) { if (e is BCellGridOnChangedEvent) ... }); then sub.cancel(). The four event classes all implement BCellGridEvent, so filter with is/whereType. Backed by the Dart stdlib broadcast stream (no rxdart); created lazily and disposed with the manager — grids that never subscribe pay nothing. The callbacks still fire; the bus is additive.

Additional information #

  • The public API deliberately mirrors pluto_grid's calling structure with Pluto* renamed to BCell*; implementation is written from scratch.
  • Performance notes and measurement guidance: .claude/PERFORMANCE.md. Frame timings must be measured in profile mode on a real device — debug-mode numbers are not valid.
  • Issues and contributions are welcome on the repository tracker.
0
likes
160
points
93
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter data-grid widget with a pluto_grid-shaped API and spreadsheet features: a formula engine, multi-sheet workbooks, charts, pivot tables, and CSV/XLSX/PDF import-export.

Homepage
Repository (GitHub)
View/report issues

Topics

#grid #spreadsheet #table #widget #formula

License

MIT (license)

Dependencies

crypto, excel, fl_chart, flutter, pdf, pointycastle

More

Packages that depend on bcell