grid_sheet 2.2.0 copy "grid_sheet: ^2.2.0" to clipboard
grid_sheet: ^2.2.0 copied to clipboard

A powerful Flutter DataGrid/DataTable for large datasets, offering Excel-like features and fully customizable cells — ready with minimal setup. See README for full features

GridSheet #

It is a highly flexible, high-performance Flutter DataTable / DataGrid package designed for building rich tabular UIs similar to Excel or spreadsheet applications, with minimal configuration and powerful built-in features. It gives developers full control over both data management and UI presentation while remaining easy to set up and extend.

It supports essential table and data-grid features such as CRUD operations, sorting, filtering, pagination (client-side, server-side & infinite scroll), column resizing, row/column reordering, row expansion panels, dropdown cells, column formulas, data reload (manual & scheduled), and advanced cell customization, making it a robust alternative to Flutter’s built-in DataTable.

With GridSheet, you can fully customize table headers, rows, filter cells, autofill cells, footers, frozen columns, and context menus using your own widgets or wrapper-based designs. This flexibility makes it ideal for building Excel-like tables, and highly customized table experiences in Flutter—without sacrificing performance or simplicity.

pub package Platform License

Demo #

Explore GridSheet's features through our live demos.

Found a bug or have an idea to improve GridSheet? We'd love to hear from you.

  • Report bugs or request features: here

Table of Contents #

Highly Configurable #

GridSheet is designed to adapt to different application requirements rather than enforcing a fixed set of behaviors.

Most features are configurable and can be enabled, disabled, or customized through the available configuration classes and callbacks. Whether you need a lightweight data table or a fully featured spreadsheet-like experience, you can choose only the functionality your application requires.

Every application has different requirements, so GridSheet focuses on providing sensible defaults while allowing you to override behavior whenever needed. Most visual components, interactions, and data handling mechanisms can be customized through configuration objects, builder callbacks, and event listeners.

You can integrate GridSheet with local data sources, server-side APIs, or your own state management solution. Features can be enabled independently, allowing you to keep the widget lightweight while adding advanced capabilities only where they are needed.

The package also exposes customization points for headers, rows, filters, footers, cells, pagination, loading states, empty states, context menus, and styling. This makes it easy to match your application's design system without modifying the package itself.

As new features are introduced, they are designed to remain configurable and backward compatible whenever possible, ensuring existing implementations continue to work while giving you access to additional functionality.

The goal of GridSheet is to provide a flexible foundation that can scale from simple data tables to complex enterprise applications with minimal changes to your existing code.

Features #

Core Functionality #

  • Data Display & Management - Display tabular data with full CRUD operations on rows and columns
  • Multi-Column Sorting - Sort by multiple columns with ascending/descending order
  • Advanced Filtering - Filter data with multiple comparison modes (contains, exact match)
  • Pagination - Built-in pagination with customizable rows per page (client-side, server-side, and infinite scroll)
  • Column Pinning - Pin columns to the left or right side, excluded from horizontal scrolling
  • Column Reordering - Drag-and-drop column reordering with visual feedback,. onColumnReorder callback
  • Row Reordering - Drag-and-drop row repositioning with visual feedback, onRowReorder callback.
  • Column Resizing - Resize columns by dragging column borders
  • Row Resizing - Resize rows by dragging serial number column cell borders
  • Cell Editing - Inline cell editing with type-specific input formatters
  • Dropdown Cell Type - Built-in dropdown/select cells with a constrained options list and popup menu

Selection & Navigation #

  • Multi-Selection - Select multiple rows, columns, or cells simultaneously
  • Keyboard Navigation - Navigate between cells with arrow keys, Tab/Shift+Tab, and Enter/Shift+Enter
  • Select All - Tri-state header checkbox (unchecked/indeterminate/checked) to select/deselect all visible rows
  • Selection Callbacks - Events for row, column, and cell selection changes

Data Manipulation #

  • Change Tracking - Track inserted, modified, and deleted rows
  • Position-Based Insert - Insert rows/columns above/below or before/after the current selection
  • Undo Operations - Revert changes to individual rows or all changes
  • Bulk Operations - Clear, fill, find, and replace operations on cell ranges
  • Copy/Paste - Clipboard support for tab-delimited data
  • Auto-Fill - Fill cells with patterns and automatic incrementing

Visual Customization #

  • Conditional Formatting - Apply background colors and text styles based on expressions
  • Custom Cell Widgets - Build custom widgets for headers, filters, and data cells
  • Row State Indicators - Visual indicators for inserted, modified, and deleted rows
  • Theme Support - Comprehensive styling with light/dark theme support
  • Custom Wrappers - Wrap header, filter, and row widgets with custom containers

Advanced Features #

  • Row & Column Virtualization - Rows and non-pinned columns are windowed (Sliver-backed), only building what's currently visible plus a small overscan buffer — scrolls smoothly with 100K+ rows and very wide schemas
  • Row Expansion - Inline master-detail panel below each row with responsive field layout, animated open/close, and full cell widget consistency
  • Runtime Configuration Updates - Toggle visual and behavioral flags at runtime via gridManager.updateConfiguration without rebuilding the parent widget
  • Data Reload - Manual reload on demand via gridManager.reload, plus interval-based and daily scheduled auto-reload with onBeforeReload / onAfterReload lifecycle callbacks
  • Expression Evaluation - Evaluate conditional expressions for cell editability and formatting
  • Context Menus - Right-click context menus for cells, rows, and columns
  • Column Visibility - Show/hide columns dynamically
  • Row Height Management - Auto-fit or manually adjust row heights
  • Column Width Management - Auto-fit or manually adjust column widths
  • Snapshot/Restore - Save and restore grid state for undo/redo functionality
  • Statistics - Calculate sum, average, min, max, median for numeric columns
  • Data Export - Export to CSV with customizable options
  • Loading States - Display custom loading indicators during operations
  • Formula Columns - Excel-style formulas, automatic evaluation, reactive updates, and filter support

Installation #

Add grid_sheet to your pubspec.yaml:

dependencies:
  grid_sheet: ^2.2.0

Then run:

flutter pub get

Import #

import 'package:grid_sheet/grid_sheet.dart';

Usage #

Basic Setup #

GridSheet(
  columns: [
    GridSheetColumn(
      key: const ValueKey('col_name'),
      index: 0,
      name: 'COL_NAME',
      title: 'Name',
      type: GridSheetColumnType.text,
      width: 200,
    ),
    GridSheetColumn(
      key: const ValueKey('col_age'),
      index: 1,
      name: 'AGE',
      title: 'Age',
      type: GridSheetColumnType.double,
      width: 80,
      textAlign: TextAlign.right,
    ),
    GridSheetColumn(
      key: const ValueKey('col_active'),
      index: 2,
      name: 'ACTIVE',
      title: 'Active',
      type: GridSheetColumnType.boolean,
      width: 80,
    ),
  ],
  rows: List.generate(
    50,
    (i) => GridSheetRow(
      key: ValueKey('row_$i'),
      index: i,
      data: [
        'Person $i',
        20 + i,
        i.isEven,
      ],
    ),
  ),
  onLoaded: (event) {
    gridManager = event.gridManager;
    debugPrint('Table initialized!');
  },
);
GridSheet(
  columns: columns,
  rows: rows,
  headerWidget: (gridManager) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        children: [
          ElevatedButton(
            onPressed: () async {
              final newRow = GridSheetRow(
                key: ValueKey('row_${DateTime.now().millisecondsSinceEpoch}'),
                index: gridManager.rows.length,
                data: List.filled(gridManager.columns.length, null),
              );
              await gridManager.insertRowsAt(0, [newRow]);
            },
            child: Text('Add Row'),
          ),
          SizedBox(width: 8),
          ElevatedButton(
            onPressed: () async {
              await gridManager.deleteRowsByKeys(
                gridManager.selectedRowKeys,
              );
            },
            child: Text('Delete Selected'),
          ),
        ],
      ),
    );
  },
  footerWidget: (gridManager) {
    return Container(
      padding: EdgeInsets.all(8),
      child: Text('Total Rows: ${gridManager.rows.length}'),
    );
  },
);

Example #

import 'package:flutter/material.dart';
import 'package:grid_sheet/grid_sheet.dart';

void main() => runApp(GridSheetApp());

class GridSheetApp extends StatefulWidget {
  const GridSheetApp({super.key});

  @override
  State<GridSheetApp> createState() => _GridSheetAppState();
}

class _GridSheetAppState extends State<GridSheetApp> {
  late final GridSheetManager gridManager;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('GridSheet Example')),
        body: GridSheet(
          columns: [
            GridSheetColumn(
              key: ValueKey('col_name'),
              index: 0,
              name: 'COL_NAME',
              title: 'Name',
              type: GridSheetColumnType.text,
              width: 200,
            ),
            GridSheetColumn(
              key: ValueKey('col_age'),
              index: 1,
              name: 'AGE',
              title: 'Age',
              type: GridSheetColumnType.double,
              width: 80,
              textAlign: TextAlign.right,
            ),
            GridSheetColumn(
              key: ValueKey('col_active'),
              index: 2,
              name: 'ACTIVE',
              title: 'Active',
              type: GridSheetColumnType.boolean,
              width: 80,
            ),
          ],
          rows: List.generate(
            50,
            (i) => GridSheetRow(
              key: ValueKey('row_$i'),
              index: i,
              data: ['Person $i', 20 + i, i % 2 == 0],
            ),
          ),
          onLoaded: (event) {
            gridManager = event.gridManager;
            debugPrint('Table initialized!');
          },
        ),
      ),
    );
  }
}

API Overview #

GridSheet Widget #

The main widget that displays the data grid.

Key Properties:

  • columns - List of column definitions
  • rows - List of row data
  • direction - Controls the grid's layout direction, with support for left-to-right (LTR) and right-to-left (RTL) layouts.
  • indexColumn - Configures the built-in serial-number/index column, including its title, width, pinning, alignment, and visibility.
  • styleConfiguration - Visual styling options
  • scrollConfiguration - Scrollbar customization
  • autofillConfiguration - Autofill (cell-drag-to-copy) Customization
  • tableGenerationConfiguration - Used to dynamically generate table columns and rows based on the provided counts.
  • conditionalFormatRules - List of formatting rules
  • headerWidget - Custom widget above the grid
  • footerWidget - Custom widget below the grid
  • noRowsWidget - Widget shown when no data is available
  • noColumnsWidget - Widget shown (in place of the whole table) when every column is hidden
  • loadingWidget - Widget shown during async operations
  • undoDeleteWidget - Custom widget for the undo-delete prompt
  • cellBuilder - Custom cell renderer (Builds a custom widget for a grid cell)
  • contextMenuWidget - Right-click context menu builder
  • onLoaded - Callback when grid is initialized
  • onRowsSelected - Callback for row selection changes
  • onColumnsSelected - Callback for column selection changes
  • onCellsSelected - Callback for cell selection changes
  • onCellValueChanged - Callback when cell value changes
  • onPageChange - Async callback for server-side pagination and infinite scroll (receives GridSheetPageRequest, returns GridSheetPageData)
  • rowExpansionConfiguration - Inline row expansion panel configuration (GridSheetRowExpansionConfiguration)
  • rowExpandedWidgetBuilder - Fully custom expansion panel builder; overrides the built-in widget when provided
  • onColumnReorder - Callback fired after a column drag-and-drop reorder completes
  • onRowReorder - Callback fired after a row drag-and-drop reorder completes
  • reloadConfiguration - Interval and/or scheduled-time settings for auto-reload (GridSheetReloadConfiguration)
  • rowsLoader - Async callback that returns fresh List<GridSheetRow> on each client-side reload
  • onBeforeReload - Callback fired immediately before every reload (manual or scheduled); receives GridSheetReloadEvent
  • onAfterReload - Callback fired immediately after every reload; receives GridSheetReloadEvent
  • sortIconBuilder - Custom builder for sort direction indicators in column headers
  • rowStateColorBuilder - Custom builder for row state background colors in the index column
  • rowStateIconBuilder - Custom builder for row state icons (inserted/modified/deleted indicators)
  • headerWrapper - Optional wrapper widget for the entire header row
  • filterWrapper - Optional wrapper widget for the entire filter row
  • rowWrapper - Optional wrapper widget for each data row
  • evaluator - Custom expression evaluator for conditional expressions (defaults to built in GridSheetExpressionEvaluator)

GridSheetManager #

The state manager interface providing programmatic control over the grid. Access it via the onLoaded callback or other event callbacks.

Data Operations:

// Row operations
// index based row insertion
await gridManager.insertRowsAt(index, rows);
// Position based row insertion
await gridManager.insertRows(rows);

await gridManager.deleteRowsByKeys(rowKeys);
gridManager.updateRowByKey(rowKey, newData);

// Row reordering
gridManager.reorderRowsByPositions(fromIndex: 0, toIndex: 3);
gridManager.reorderRowsByKeys(fromRowKey: ValueKey('row_5'), toRowKey: ValueKey('row_2'));
gridManager.resetRowOrder(); // restore original load order

// Column operations
// Index based insert
await gridManager.insertColumnsAt(index, columns);
// Position based column insertion
await gridManager.insertColumns(columns);

await gridManager.deleteColumnsByKeys(columnKeys);
await gridManager.hideColumns(['COL_NAME']);
await gridManager.showColumns(['COL_NAME']);
await gridManager.renameColumns({
  'fname': (newName: 'first_name', newTitle: 'First Name'),
  'lname': (newName: 'last_name',  newTitle: 'Last Name'),
});

// Cell operations
gridManager.updateCurrentCell(rowKey: key, columnKey: key, value: value);
await gridManager.clearCellRange(startRow: 0, endRow: 5, startColumn: 0, endColumn: 2);

// Position based insert
await gridManager.insertRows(
  [newRow],
  position: GridSheetRowInsertPosition.belowSelection,
);
await gridManager.insertColumns(
  [newColumn],
  position: GridSheetColumnInsertPosition.afterSelection,
);

Selection:

// Programmatic selection
gridManager.selectRowsByKeys([rowKey1, rowKey2]);
gridManager.selectColumnsByKeys([colKey1]);
gridManager.selectCellsByKeys([cellKey1, cellKey2]);

// Query selection
final selectedRows = gridManager.selectedRowKeys;
final selectedCells = gridManager.selectedCellKeys;

Filtering & Sorting:

// Add filters
await gridManager.addFilters(
  {'status': ['Active', 'Pending']},
  comparison: GridSheetFilterComparison.exact,
);

// Sort by column
await gridManager.sortByColumn('age', direction: GridSheetSortDirection.desc);

// Clear all
await gridManager.clearSort();
await gridManager.removeAllFilters();

Pagination:

// Navigate (client-side and server-side)
await gridManager.goToPage(3);
gridManager.nextPage();
gridManager.previousPage();

// Change page size
await gridManager.changeRowsPerPage(50);

// Query state
debugPrint('Page ${gridManager.currentPage} of ${gridManager.totalPages}');
debugPrint('Total rows: ${gridManager.totalFilteredRows}');

// Server-side / infinite scroll specific
debugPrint('Has more rows: ${gridManager.hasMoreRows}');
debugPrint('Server total: ${gridManager.serverTotalCount}');

Data Export:

// Export as CSV
final csv = gridManager.exportToCSV(
  includeHeaders: true,
  onlyVisibleColumns: true,
);

// Export with formula columns' live formula text (e.g. '=A1+B1') instead
// of their evaluated value
final csvWithFormulas = gridManager.exportToCSVWithFormulas(
  includeFormulas: true,
);

// Get row data as maps
final maps = gridManager.rowsDataToMapList(gridManager.rows);

// Get statistics
final stats = gridManager.getColumnStatistics('salary');
debugPrint('Average: ${stats['average']}');

Note: CSV export automatically neutralizes cell values that start with =, +, -, or @ (by prefixing a leading ') to prevent CSV/spreadsheet formula injection when the exported file is opened in Excel, Sheets, or LibreOffice. A formula column's live formula text exported via includeFormulas: true is exempt, since it's meant to stay a live formula.

Runtime Configuration Updates:

// Toggle visual/behavioral flags without rebuilding the parent widget
gridManager.updateConfiguration(
  gridManager.configuration.copyWith(
    showColumnFilters: false,
    showColumnHeaderRow: true,
    enableColumnReorder: false,
    enableRowReorder: true,
    enableRowHover: false,
    showRowBorders: false,
    showColumnBorders: true,
  ),
);

Reload:

// Manual reload — shows loading overlay, then re-fetches or refreshes
await gridManager.reload();

// Runtime schedule control
gridManager.stopAutoReload();
gridManager.startAutoReload();

// Check whether auto-reload is currently active
debugPrint('Auto-reload active: ${gridManager.isAutoReloadActive}');

Core Models #

GridSheetColumn

Note: The name property serves as the unique internal identifier for a column. Use this value consistently across expressions, conditional formatting rules, and other grid configurations to reference the column reliably.

GridSheetColumn(
  key: ValueKey('unique_key'),
  name: 'COL_NAME',          // Internal identifier
  title: 'Display Title',      // Shown in header
  description: 'Full description shown as a tooltip on hover',  // optional
  type: GridSheetColumnType.text,
  width: 150,
  visible: true,
  pinnedLeft: false,           // Pin to left
  pinnedRight: false,          // Pin to right
  editable: true,
  sortable: true,
  resize: true,
  textAlign: TextAlign.left,
  conditionalEditExpression: 'age > 18',  // Control editability
)
Property Type Default Description
key ValueKey<String> required Unique stable identifier
index int required Position in GridSheetRow.data
name String required Internal identifier used in expressions and filters
title String required Text displayed in the column header
description String? null Tooltip shown on header hover — ideal for abbreviated titles
type GridSheetColumnType required Data type (text, integer, double, datetime, boolean, dropdown, formula, custom)
width double 120.0 Column width in logical pixels
visible bool true Whether the column is shown
pinnedLeft bool false Pin column to the left (excluded from horizontal scroll)
pinnedRight bool false Pin column to the right (excluded from horizontal scroll). Takes precedence over pinnedLeft if both are set
editable bool true Allow cell editing
sortable bool false Enable sort on header double-tap
resize bool false Allow column width drag-resize
textAlign TextAlign TextAlign.left Cell content alignment
filterHint String 'Search..' Placeholder text in the filter input
dropdownOptions List<String>? null Selectable values for dropdown columns
noTextControllerWidget bool false Skip TextEditingController (use for checkboxes, dropdowns)
showFormulaInEdit bool true Show raw formula while editing formula columns
showOverflowTooltip bool false Show a hover tooltip with the full value when cell text is truncated.
conditionalEditExpression String? null Expression evaluated per row to control cell editability

GridSheetRow

GridSheetRow(
  key: ValueKey('unique_key'),
  index: 0,
  data: ['value1', 'value2', 'value3'],
  height: 40.0,  // defaults to 30.0 if omitted
  state: GridSheetRowState.existing,  // existing (default), inserted, modified, deleted
)

Each row remembers the height it was constructed with. Resizing (via drag handles, updateRowHeights, setAllRowHeights, or auto-fit) only changes the live height — call row.resetHeight to restore that row's original height, or gridManager.resetAllRowHeights to restore every row at once. Rows constructed with different initial heights (e.g. 30, 40, 50) are restored to those individual values, not to a single shared default.

GridSheetConfiguration.rowHeight is deprecated in favor of the per-row height above.

GridSheetCell

final cellKey = GridSheetCell(
  ValueKey('row_1'),
  ValueKey('col_name'),
);

Configuration #

GridSheet Configuration #

GridSheetConfiguration(
  // Pagination
  enableDefaultPagination: true,
  enableCustomPagination: false,
  rowsPerPage: 20,
  
  // Selection
  enableRowSelectionOnFirstColumnTap: true,
  enableColumnSelection: true,
  enableCellSelection: true,
  enableMultiSelection: true, 
  selectOnlyPageRows: false,
  selectionIncludesPinnedLeftCells: false,
  selectionIncludesPinnedRightCells: false,
  conditionalFormatIncludesPinnedLeftCells: false,
  conditionalFormatIncludesPinnedRightCells: false,

  // Position based insertion
  rowInsertPosition: GridSheetRowInsertPosition.aboveSelection,
  columnInsertPosition: GridSheetColumnInsertPosition.beforeSelection,
  
  // Visual
  enableRowHover: true,
  enableRowResize: false,
  showBorderOnSelection: false,
  // serialNumberColumn / serialNumberColumnWidth are deprecated — use
  // GridSheet.indexColumn instead (see "GridSheet Widget" above).
  showRowBorders: true,
  showColumnBorders: true,
  // grow scrollable columns to fill leftover width instead of leaving it blank
  stretchColumnsToFillWidth: false,
  showColumnFilters: true,
  
  // To show column header row
  showColumnHeaderRow: true,

  // show filter area above column header
  filterPosition: GridSheetFilterPosition.above,
  
  // Interaction
  enableColumnReorder: true,
  enableRowReorder: true,
  enableRowContextMenu: true,
  enableColumnContextMenu: true,
  enableCellContextMenu: true,
  
  // Sizing
  headerHeight: 40,
  filterHeight: 40,
  // Deprecated use [GridSheetRow.height] instead
  rowHeight: 40,

  // Server-side / Infinite scroll
  serverSidePagination: false,      // fetch rows from server on each page change
  infiniteScroll: false,            // append rows as user scrolls to bottom
  infiniteScrollThreshold: 200.0,   // pixels from bottom that trigger the next fetch
  // show prompt after deletions instead of removing them permanently right away
  enableUndoDelete: true,
)

Dynamic Table Generation #

GridSheetDynamicTableGeneration allows GridSheet to automatically generate its columns and rows at runtime based on the provided configuration. It is designed for grid/table components where the structure can be created at runtime instead of being statically defined.

Important: columns and rows must be empty lists when using dynamic generation

GridSheet(
  columns: [], // Must be an empty list when dynamic generation is enabled
  rows: [],    // Must be an empty list when dynamic generation is enabled
  tableGenerationConfiguration: GridSheetDynamicTableGeneration(
    enabled: true,      // Enables dynamic table generation
    columnsCount: 20,   // Number of columns to generate
    rowsCount: 30,      // Number of rows to generate
    // Customizes each generated column beyond the defaults — any
    // GridSheetColumn property can be overridden per index.
    columnBuilder: (index, column) =>
        column.copyWith(type: GridSheetColumnType.integer, width: 150.0),
    // Customizes each generated row beyond the defaults — any GridSheetRow
    // property (height, state, seeded data) can be overridden per index.
    rowBuilder: (index, row) =>
        row.copyWith(state: GridSheetRowState.inserted),
  ),
);

Autofill Configuration #

GridSheetAutoFillConfiguration(
  enabled: true, // Enables or disables the auto-fill feature.
  fillHandleSize: 8.0, // Size of the fill handle (in logical pixels).
  fillHandleColor: Colors.blue, // Color of the fill handle.
  fillHandleShape: BoxShape.rectangle, // BoxShape.rectangle or BoxShape.circle.
  fillHandleBorderRadius: BorderRadius.zero, // Corner radius.
  showPreviewOverlay: true, // Whether to show a visual preview.
  enablePatternDetection: true, // Automatically detects and continues patterns.
  // Prevents auto-fill if source and target cell value column types do not match.
  enforceTypeMatching: false, 
  //Mouse cursor displayed while dragging the fill handle.
  dragCursor: SystemMouseCursors.cell, 
)

Scroll Configuration #

GridSheetScrollBarConfiguration(
  radius: const Radius.circular(4), // Corner radius of the scrollbar thumb.
  thickness: 10.0, // Thickness of the scrollbar (in logical pixels).
  thumbVisibility: true, // Whether the scrollbar thumb is always visible.
  trackVisibility: true, // Whether the scrollbar track is visible behind the thumb.
  // false disables vertical scrolling entirely (not just the bar)
  showVerticalScrollbar: true,   
  // false disables horizontal scrolling entirely (not just the bar)
  showHorizontalScrollbar: true, 
  // Only matters when showVerticalScrollbar is true.
  verticalScrollbarPlacement: GridSheetVerticalScrollbarPlacement.afterLastColumn,
  // Replaces the scrollbar widget entirely (e.g. RawScrollbar with a custom
  // thumb) instead of just restyling the built-in Scrollbar.
  scrollbarBuilder: null,
)
  • scrollbarBuilder: (context, child, controller, axis) => Widget — wrap child (the scrollable content) in your own scrollbar widget and return it, reusing the given controller. See the in-repo example (example/lib/features/scrollbar.dart) for a RawScrollbar implementation, including the notificationPredicate needed for the horizontal axis since the grid nests its horizontal scroll view inside the vertical one.
  • showVerticalScrollbar/showHorizontalScrollbar disable scrolling on that axis entirely (mouse wheel, trackpad, and drag) as well as hiding the scrollbar — scrollbarBuilder is not called for a disabled axis either.
  • verticalScrollbarPlacement: afterLastColumn (default) reserves a strip of space after the last column so the vertical scrollbar never covers content; overlay draws it directly on top of the last column instead — like a native/desktop overlay scrollbar — so the last column extends fully to the grid's right edge. Has no effect when showVerticalScrollbar is false.

Row Expansion Configuration #

GridSheetRowExpansionConfiguration(
  enabled: true,  // Show expansion chevron in the serial-number column
  // Max height in pixels — the panel shrinks to fit shorter content and scrolls past this
  panelHeight: 300.0,
  animationDuration: Duration(milliseconds: 200), // Open/close animation speed
  backgroundColor: null,  // Panel background
  // Icon shown in the serial-number column (rotates 90° when open)
  icon: Icons.chevron_right,
  showOnlyEditableColumns: true, // Only show columns editable for that row
)

Column Animation Configuration #

GridSheetAnimationConfiguration(
  enabled: false, // Off by default
  // Total time for the whole cascade, not a single column
  animationDuration: Duration(milliseconds: 700), 
  curve: Curves.easeOutCubic, // Shapes each column's own slide/fade transition
  slideDistance: 24.0,  // Logical pixels each column slides in from
  replayOnPageChange: true, // Replay the cascade on every pagination change
  // Replay the same cascade per-row after a sort/filter
  animateRowsOnSortOrFilter: false, 
   // Flash a cell's background when its value changes
  cellChangeFlashEnabled: false, 
  cellChangeFlashColor: Colors.amber, // Flash color
  // How long the flash takes to fade out
  cellChangeFlashDuration: Duration(milliseconds: 800), 
  // Fade a column in when it's shown via showColumns
  columnVisibilityFadeEnabled: false, 
  // Replace the built-in slide+fade for column/row reveal
  revealTransitionBuilder: null,
  // Replace the built-in flat-color cell-change flash
  cellFlashBuilder: null, 
)
  • Set replayOnPageChange: false to play the animation only on the initial mount. The animation never replays on reload or automatic reloads.
  • animateRowsOnSortOrFilter replays the same staggered slide-and-fade animation for rows (top to bottom) whenever the current page is sorted or filtered. The row animation additionally runs after serverSidePagination page changes, while infiniteScroll animates only newly appended rows. Client-side pagination never triggers a row reveal.
  • revealTransitionBuilder: (context, child, progress, axis) => Widget swaps out the built-in slide+fade for column/row reveal with your own transition — progress runs 0.0→1.0, already shaped by curve.
  • cellFlashBuilder: (context, progress) => Widget swaps out the built-in flat-color cell-change flash with your own effect — progress runs 1.0 (just changed) → 0.0 (settled) over cellChangeFlashDuration.

Style Configuration #

GridSheetStyleConfiguration(
  // Colors
  gridBackgroundColor: Colors.white,
  headerColor: Colors.grey[200]!,
  rowColor: Colors.white,
  evenRowColor: Colors.grey[50],
  oddRowColor: Colors.white,
  hoverColorOnRow: Colors.blue,
  filterColor: Colors.grey[100]!,
  selectionColor: Colors.blue[100]!,
  ascSortIconColor: Colors.green,
  descSortIconColor: Colors.red,
  // bgcolor of the screen overlay shown behind the loader
  loadingOverlayColor: Colors.black[300]!,
  
  // Borders
  gridBorderColor: Colors.grey[300]!,
  gridBorderWidth: 1.0,
  rowBorderColor: Colors.grey[300]!,  // Requires GridSheetConfiguration.showRowBorders: true
  rowBorderWidth: 1.0,
  columnBorderColor: Colors.grey[300]!, // Requires GridSheetConfiguration.showColumnBorders: true
  columnBorderWidth: 1.0,
  cellTextFieldBorderColor: Colors.grey[300]!, // Border of the in-place cell editor text field
  cellTextFieldBorderWidth: 1.0,
  gridBorderRadius: BorderRadius.all(Radius.circular(4)),

  // Selection / hover / resize / drag feedback
  selectionBorderWidth: 2.0,
  resizeHandleColor: null,  // null → falls back to selectionColor
  resizeHandleThickness: 3.0, // Thickness of the column/row resize indicator line
  // Elevation of the floating preview while reordering a column
  columnDragFeedbackElevation: 8.0, 
  // Elevation of the floating preview while reordering a row
  rowDragFeedbackElevation: 4.0,
  dragFeedbackOpacity: 0.35,  // Opacity of the placeholder left at the drag origin

  // Customize dividers between pinned columns and the main grid.
  pinnedLeftDividerColor: Colors.blue[100]!,
  pinnedRightDividerColor: Colors.yellow[100]!,
  pinnedColumnsDividerWidth: 2,

  // Text styles
  headerTextStyle: TextStyle(fontWeight: FontWeight.bold),
  cellTextStyle: TextStyle(fontSize: 14),
  
  // Horizontal padding — use it mostly when wrappers are provided.
  leftPadding: 8,
  rightPadding: 8,
)

Theming

You can switch between light and dark themes by configuring the style options.
For a full working example, please see the Example tab.

Column Configuration #

Column Types:

  • GridSheetColumnType.text - String values
  • GridSheetColumnType.double - Numeric values with decimal support
  • GridSheetColumnType.int - Integer values
  • GridSheetColumnType.boolean - True/false values
  • GridSheetColumnType.datetime - Date and time values
  • GridSheetColumnType.formula - Excel-style formula column
  • GridSheetColumnType.dropdown - Dropdown/select cell constrained to a fixed options list
  • GridSheetColumnType.custom - Custom type

Column Features:

GridSheetColumn(
  // Identity
  key: ValueKey('col_1'),
  name: 'internalName',
  title: 'Display Name',
  index: 0,  // Position in data array
  
  // Behavior
  type: GridSheetColumnType.text,
  editable: true,
  sortable: true,
  resize: true,
  pinnedLeft: false,
  pinnedRight: false,
  visible: true,
  // Use `noTextControllerWidget: true` for read-only columns or 
  // when using custom widgets that do not require a `TextEditingControllers`,
  // preventing unnecessary controller creation and improving performance.
  noTextControllerWidget: false,
  
  // Appearance
  width: 150,
  // caps growth under GridSheetConfiguration.stretchColumnsToFillWidth; 
  maxStretchWidth: null, // null = uncapped
  textAlign: TextAlign.left,
  
  // Conditional cell editability
  conditionalEditExpression: 'status == "Active"',

  // Dropdown options (required when type == GridSheetColumnType.dropdown)
  dropdownOptions: ['Active', 'Inactive', 'Pending'],
)

Advanced Features #

Sorting #

This feature works only when the column is configured with sortable = true and the user double-clicks on the column header to apply sorting. The custom icons are displayed when sorting is active.

Custom sort icons allow you to build and display your own sorting indicators in column headers. These icons are shown when sorting is applied, giving you full control over their appearance and behavior instead of using the default built-in sort icons.

GridSheetColumn(
  // To enable sorting for that column
  sortable: true,
);

// custom sort icons
GridSheet(
  sortIconBuilder: (column, direction, priority) {
    final isAsc = direction == GridSheetSortDirection.asc;
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(
          isAsc ? Icons.arrow_upward : Icons.arrow_downward,
          size: 16,
          color: isAsc ? Colors.green : Colors.red,
        ),
        if (priority > 1)
          Text(
            ' $priority',
            style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
          ),
      ],
    );
  },
)

Column Pinning #

Pin columns to either edge of the grid so they stay visible while the rest of the table scrolls horizontally. Pinned-left columns render in a fixed pane before the scrollable columns; pinned-right columns render in a fixed pane after them, at the true right edge of the grid — the vertical scrollbar (when verticalScrollbarPlacement: afterLastColumn, the default) renders after the pinned-right pane too, not between it and the scrollable columns.

A column can only be pinned to one side at a time — pinning it to the other side clears the existing pin.

// Declaratively, at construction:
GridSheetColumn(pinnedLeft: true, /* ... */);
GridSheetColumn(pinnedRight: true, /* ... */);

// Dynamically, via the manager — index-based:
await gridManager.pinColumnsLeftUptoIndex(1);       // pins columns [0, 1]
await gridManager.pinColumnsRightFromIndex(4);      // pins columns [4..end]

// Dynamically, by name — authoritative (moves the named columns to the
// front/end and unpins anything else previously pinned on that side):
await gridManager.pinColumnsToLeft(['id', 'name']);
await gridManager.pinColumnsToRight(['actions']);

// Unpin: all pinned columns
await gridManager.clearPinColumns();

// Read the current pin state:
gridManager.pinnedLeftColumns;
gridManager.pinnedRightColumns;

Marking the pin boundary visually: GridSheetStyleConfiguration.pinnedLeftDividerColor/pinnedRightDividerColor draw an optional divider at the seam between a pinned pane and the scrollable columns, sized by pinnedColumnsDividerWidthnull (the default) draws nothing on that side.

GridSheet(
  styleConfiguration: GridSheetStyleConfiguration(
    pinnedLeftDividerColor: Colors.grey,
    pinnedRightDividerColor: Colors.grey,
    pinnedColumnsDividerWidth: 2,
  ),
)

Each side is independent — set one, both, or neither, and each side's color can differ. A divider is only drawn when that side actually has pinned columns. Unlike the per-column separators drawn by columnBorderColor/columnBorderWidth, this divider is a real element inserted between the panes (not an overlay painted on top of them), so it always reserves its own width — however large pinnedColumnsDividerWidth is set to, it never covers any active-pane cell content; the scrollable pane is simply narrower by that amount.

Column Reorder #

Column reordering is enabled by long-pressing a column header and dragging it to the desired position.

GridSheet(
  configuration: GridSheetConfiguration(
    enableColumnReorder: true,
  ),
  onColumnReorder: (event) {
    // event.fromIndex     — original visual position
    // event.toIndex       — new visual position
    // event.fromColumnKey — stable key of the moved column
    // event.toColumnKey   — stable key of the drop-target column
    debugPrint('Column moved: ${event.fromIndex} → ${event.toIndex}');

    // Persist new order
    final order = event.gridManager.visibleColumns.map((c) => c.name).toList();
    userPrefs.saveColumnOrder(order);
  },
)

Row Reorder #

Row reordering is enabled by long-pressing the serial-number cell of a row and dragging it to the desired position. Row reorder is automatically disabled while a sort is active.

GridSheet(
  configuration: GridSheetConfiguration(
    enableRowReorder: true,
  ),
  onRowReorder: (event) {
    // event.fromIndex  — original position in the rows list
    // event.toIndex    — new position after the move
    // event.fromRowKey — stable key of the dragged row
    // event.toRowKey   — stable key of the drop-target row
    debugPrint('Row moved: ${event.fromIndex} → ${event.toIndex}');
    myDataSource.reorder(event.fromIndex, event.toIndex);
  },
)

Row Expansion #

Row expansion enables an inline master-detail panel that slides open directly below any row when the user taps the serial-number cell. The panel displays all column values using the exact same cell widgets as the grid row — editability, conditional expressions, dropdown types, and custom cellBuilder overrides all apply identically.

Enable it via rowExpansionConfiguration:

GridSheet(
  columns: columns,
  rows: rows,
  rowExpansionConfiguration: GridSheetRowExpansionConfiguration(
    enabled: true,
    panelHeight: 280.0,
    animationDuration: Duration(milliseconds: 200),
    backgroundColor: Colors.grey[50],   // optional; defaults to row background color
    icon: Icons.chevron_right,          // rotates 90° when expanded
  ),
)

Behavior:

  • Tapping the serial-number cell toggles the expansion panel open/closed.
  • Multiple rows can be expanded simultaneously.

Custom expansion panel:

Supply rowExpandedWidgetBuilder to replace the built-in form entirely:

GridSheet(
  rowExpansionConfiguration: GridSheetRowExpansionConfiguration(enabled: true),
  rowExpandedWidgetBuilder: (context, row, columns) {
    return Container(
      color: Colors.blue[50],
      padding: const EdgeInsets.all(16),
      child: Text('Detail view for row ${row.index}'),
    );
  },
)

Custom serial-number cell with expansion support:

When using cellBuilder for GridSheetCellKind.indexing, the built-in chevron icon is not added automatically. Use cell.isRowExpanded and cell.onToggleRowExpansion from GridSheetCellContext to render your own icon and handle the toggle.

Relocating the expand toggle to another column:

onToggleRowExpansion/isRowExpanded aren't limited to the index column — they're populated on GridSheetCellContext for any row cell (any column) while row expansion is enabled, so a cellBuilder on a different column can wire the toggle in there instead:

GridSheet(
  rowExpansionConfiguration: GridSheetRowExpansionConfiguration(enabled: true),
  cellBuilder: (cell) {
    if (cell.kind != GridSheetCellKind.row || cell.column.name != 'STATUS') {
      return null; // fall back to the default cell for everything else
    }
    return GestureDetector(
      onTap: cell.onToggleRowExpansion,
      child: Icon(cell.isRowExpanded == true
          ? Icons.expand_less
          : Icons.expand_more),
    );
  },
)

Right-to-Left (RTL) Support #

GridSheet provides full native RTL support, mirroring layout, alignment, scrolling, interactions, icons, and navigation while preserving logical column identity, state, selection, filters, sorting, and callbacks.

Enable it either explicitly, or by inheriting the ambient app direction — no other configuration is required:

// Explicit — overrides whatever Directionality is ambient above it:
GridSheet(
  columns: columns,
  rows: rows,
  direction: GridSheetDirection.rightToLeft,
)

// Or inherit from an ambient Directionality/Localizations (e.g. the
// app's locale) by leaving direction unset (the default, `null`):
MaterialApp(
  locale: const Locale('ar'), // or any RTL locale
  home: Scaffold(
    body: GridSheet(columns: columns, rows: rows),
  ),
)

Known limitations:

  • Mixed-direction text within a single cell value (BiDi shaping) is handled by Flutter's own text layer, not overridden per-run by the grid.
  • RTL numeral formatting (Eastern Arabic-Indic digits) is a locale/NumberFormat concern the grid doesn't touch.
  • Custom cellBuilder/headerWrapper/filterWrapper/rowWrapper widgets inherit ambient Directionality but their internal layout isn't audited — that's the consuming app's responsibility, same as any other custom Flutter widget.

Runtime Configuration Updates #

updateConfiguration lets you toggle visual and behavioral flags at runtime via GridSheetManager without triggering a parent widget rebuild.

Safe to update at runtime:

Field Description
showColumnFilters Show/hide the filter row
showColumnHeaderRow Show/hide the column header row
enableColumnReorder Enable/disable column drag-to-reorder
enableRowReorder Enable/disable row drag-to-reorder
enableRowHover Enable/disable row hover highlight
serialNumberColumn (deprecated) Show/hide the serial-number (index) column — use GridSheet.indexColumn instead
serialNumberColumnWidth (deprecated) Width of the serial-number column — use GridSheet.indexColumn instead
showRowBorders Show/hide horizontal row borders
showColumnBorders Show/hide vertical column borders
stretchColumnsToFillWidth Grow scrollable columns to fill leftover width instead of leaving it blank
headerHeight Height of the column header row
filterHeight Height of the filter row
enableRowContextMenu Enable/disable the row right-click menu
enableColumnContextMenu Enable/disable the column right-click menu
enableCellContextMenu Enable/disable the cell right-click menu
enableUndoDelete Enable/disable the undo-delete prompt for row/column deletions (see "Undo Delete")

Structural fields are locked — the following cannot be changed at runtime because they affect data loading and pagination mode. Pass a new value for them and it will be silently ignored; use the dedicated API instead:

Field Use instead
rowsPerPage gridManager.changeRowsPerPage(n)
enableDefaultPagination Rebuild the widget
enableCustomPagination Rebuild the widget
serverSidePagination Rebuild the widget
infiniteScroll Rebuild the widget
// Example: hide filters and disable column reorder from a toolbar button
gridManager.updateConfiguration(
  gridManager.configuration.copyWith(
    showColumnFilters: false,
    enableColumnReorder: false,
  ),
);

GridSheet.indexColumn isn't part of GridSheetConfiguration, so it isn't changed via updateConfiguration — rebuild the widget with a new value instead (the same pattern styleConfiguration/autofillConfiguration already use):

// Example: toggle the serial-number column via a rebuild
GridSheet(
  indexColumn: showIndex ? GridSheetColumn.defaultIndexColumn : null,
  // ...
)

Data Reload #

GridSheet supports both manual and scheduled automatic reload. On each reload the grid re-applies the current sort and filter so the displayed data stays consistent.

Reload behaviour by data mode:

Mode What happens on reload
Server-side pagination / Infinite scroll Re-fetches page 1 from the server with the current sort and filter state
Client-side with rowsLoader Calls rowsLoader, replaces internal row list, then re-applies sort/filter
Client-side without rowsLoader Re-applies the current sort and filter to existing rows (view refresh only)

Manual Reload

Call gridManager.reload at any time — from a button, a WebSocket event, or any external trigger:

GridSheet(
  rowsLoader: () async {
    final data = await api.fetchUsers();
    return data.map((e) => GridSheetRow(...)).toList();
  },
  onLoaded: (event) => gridManager = event.gridManager,
  loadingWidget: const Center(child: CircularProgressIndicator()),
  ...
)

// Reload from a toolbar button
ElevatedButton(
  onPressed: () => gridManager.reload(),
  child: const Text('Refresh'),
)

Interval-based Auto-Reload

Reload automatically every N duration using GridSheetReloadConfiguration.interval:

GridSheet(
  rowsLoader: () async => await api.fetchRows(),
  reloadConfiguration: const GridSheetReloadConfiguration(
    interval: Duration(minutes: 5),
    showLoadingOnAutoReload: false, // silent background refresh (default)
  ),
  onAfterReload: (event) {
    debugPrint('Auto-refreshed at ${DateTime.now()}');
  },
  ...
)

Daily Scheduled Auto-Reload

Reload once per day at a fixed wall-clock time using GridSheetReloadConfiguration.scheduledTime:

GridSheet(
  rowsLoader: () async => await api.fetchRows(),
  reloadConfiguration: const GridSheetReloadConfiguration(
    scheduledTime: TimeOfDay(hour: 9, minute: 0), // fires every day at 09:00
  ),
  ...
)

Combined — Interval + Scheduled

Both options can be set together:

GridSheetReloadConfiguration(
  interval: Duration(hours: 1),           // every hour
  scheduledTime: TimeOfDay(hour: 0, minute: 0), // plus a midnight reset
)

Lifecycle Callbacks

onBeforeReload and onAfterReload are fired for every reload regardless of source. Use event.isAutoReload to distinguish automatic reloads from manual ones:

GridSheet(
  onBeforeReload: (event) {
    if (!event.isAutoReload) showSpinner();
  },
  onAfterReload: (event) {
    hideSpinner();
    debugPrint('Rows after reload: ${event.gridManager.rows.length}');
  },
  ...
)

Runtime Schedule Control

Stop and restart the auto-reload schedule at runtime without rebuilding the widget:

// Pause auto-reload when the user navigates away
gridManager.stopAutoReload();

// Resume when they come back
gridManager.startAutoReload();

// Check current state
if (gridManager.isAutoReloadActive) { ... }

Set type: GridSheetColumnType.dropdown and provide a dropdownOptions list to render a built-in dropdown/select cell. Tapping the cell opens a Material popup menu anchored below the cell; the currently selected value is shown with a checkmark.

GridSheetColumn(
  key: ValueKey('col_status'),
  name: 'STATUS',
  title: 'Status',
  type: GridSheetColumnType.dropdown,
  dropdownOptions: ['Active', 'Inactive', 'Pending'],
  width: 130,
  editable: true,
)
  • Read-only state: If editable: false, conditionalEditExpression evaluates to false, or dropdownOptions is null/empty, the cell renders as plain text with no arrow.

Column Formulas #

It supports mostly Excel-style formulas in cells, with automatic evaluation, editing, and filtering. Dependent values automatically update when source cells change.

GridSheetColumn(
  name: 'FORMULA_COL',
  type: GridSheetColumnType.formula, // formula type column
  showFormulaInEdit: true, // shows formula when focused, else only evaluated value
);

// Can evaluate formulas on load
// Can contain any supported formula in its cells, 
// e.g., '=F1 * 2', '=SUM(E1:F3)', '=A1 + B2'.
// Can contain column names
rows = [
  ['=F1 * 2'],
  ['=SUM(E1:F3)'],
  ['=COLUMN_NAME3 * 2'],
];

Filter support: Works on both formulas and computed values

await gridManager.addFilters(
  {'FORMULA_COL': ['=F1 * 2']},
  comparison: GridSheetFilterComparison.exact,
);

await gridManager.addFilters(
  {'FORMULA_COL': [1400]}, // e.g., if F1 value is 700
  comparison: GridSheetFilterComparison.exact,
);

Reading a cell's formula: Get the raw formula text back (as opposed to its evaluated value)

final formula = gridManager.getCellFormula(
  rowKey: row.key,
  columnKey: column.key,
); // '=A1+B1', or null if the cell has no stored formula

Conditional Formatting #

Apply visual styles to cells dynamically based on their values, enabling better data visualization and quick identification of important or out-of-range data.

Pinned-left and pinned-right columns are excluded from conditional formatting by default — set conditionalFormatIncludesPinnedLeftCells: true / conditionalFormatIncludesPinnedRightCells: true in GridSheetConfiguration to also evaluate and apply rules to them.

GridSheet(
  conditionalFormatRules: [
    // Row-level formatting
    GridSheetConditionalFormatRule(
      name: 'HighlightHighValues',
      scope: GridSheetFormatScope.row,
      backgroundColorExpression: 'age > 50 ? "#FFCDD2" : null',
      textStyle: TextStyle(color: Color(0xFFE65100), fontStyle: FontStyle.italic),
    ),

    // Column-level formatting
    GridSheetConditionalFormatRule(
      name: 'status',
      scope: GridSheetFormatScope.column,
      backgroundColorExpression: 'status == "Active" ? "#C8E6C9" : null',
      textStyle: TextStyle(color: Color(0xFFE65100), fontStyle: FontStyle.bold),
    ),

    // Cell-level formatting
    GridSheetConditionalFormatRule(
      name: 'salary',
      scope: GridSheetFormatScope.cell,
      backgroundColorExpression: 'salary > 100000 ? "#FFE0B2" : null',
      textStyle: TextStyle(color: Color(0xFFE65100)),
    ),
  ],
)

GridSheetConditionalFormatRule(
  name: 'HighlightErrors',
  scope: GridSheetFormatScope.cell,
  backgroundColorExpression: 'status == "Error" ? "#FFCDD2" : null',
  // errors always win outright — no lower-priority rule adds to this cell
  stopIfTrue: true,
)

Priority and conflict resolution (Excel-style): conditionalFormatRules' list order is priority order — the first rule is highest priority — matching Excel's Conditional Formatting Rules Manager. When more than one rule matches the same cell:

  • For the same attribute (background color, or a given textStyle field like font weight), the highest-priority matching rule wins; lower-priority rules never override an attribute a higher-priority rule already set.
  • For different attributes, results merge instead of one rule replacing another wholesale — e.g. a higher-priority rule that only sets a background color still picks up font weight from a lower-priority rule that only sets that.
  • Set stopIfTrue: true on a rule to opt out of that merging — once a matching rule with stopIfTrue applies, no lower-priority rule is considered for that cell at all, mirroring Excel's "Stop If True".
  • Scope (row/column/cell) has no inherent priority of its own — only list position does.

Dynamic rule management: Conditional formatting rules can be added, updated or removed dynamically, allowing styles to adapt as data or requirements change.

// Add rule (appended at the end = lowest priority by default)
await gridManager.addConditionalFormatRule(rule);

// Add rule at a specific priority (0 = highest priority)
await gridManager.addConditionalFormatRule(rule, atIndex: 0);

// Change an existing rule's priority
await gridManager.reorderConditionalFormatRulesByPositions(
  fromIndex: 2,
  toIndex: 0, // make it the highest-priority rule
);

// Remove rule
await gridManager.removeConditionalFormatRule(
  name: 'HighlightHighValues',
  scope: GridSheetFormatScope.row,
);

// Update rule
await gridManager.updateConditionalFormatRule(
  oldName: 'HighlightHighValues',
  scope: GridSheetFormatScope.row,
  newRule: GridSheetConditionalFormatRule(
    name: 'HighlightHighValues',
    scope: GridSheetFormatScope.cell,
    backgroundColorExpression: 'age > 60 ? "#FFCDD2" : null',
    textStyle: TextStyle(color: Color(0xFFE65100), fontStyle: FontStyle.italic),
  ),
);

// Remove all rules by scope
await gridManager.removeConditionalFormatRulesByScope(
  GridSheetFormatScope.row,
);

// Remove all rules
await gridManager.removeAllConditionalFormatRules();

// Find rules
final rules = gridManager.findConditionalFormatRules(
  name: 'HighlightHighValues',
  scope: GridSheetFormatScope.row,
);

// Refresh formatting
gridManager.refreshConditionalFormatting();

// returns a JSON-safe map of only the cells 
// where a rule actually resolved a background color or text style
final styles = gridManager.getConditionalFormattingStyles();

final cellStyle = styles['row_3|col_amount'];
if (cellStyle != null) {
  print(cellStyle['bgColor']); // e.g. '#FFFFCDD2'
}

Custom Row State Indicators #

Use custom row state indicators to display visual markers(icon/bgColor) in the serial number column when a row is modified, deleted, or inserted, helping track changes directly in the grid.

GridSheet(
  rowStateColorBuilder: (row) {
    if (row.isInserted) return Colors.green[50]!;
    if (row.isModified) return Colors.orange[50]!;
    if (row.isDeleted) return Colors.red[50]!;
    return Colors.white;
  },
  
  rowStateIconBuilder: (row) {
    if (row.isInserted) {
      return Icon(Icons.add_circle, color: Colors.green, size: 16);
    }
    if (row.isModified) {
      return Icon(Icons.edit, color: Colors.orange, size: 16);
    }
    if (row.isDeleted) {
      return Icon(Icons.delete, color: Colors.red, size: 16);
    }
    return null;
  },
)

Custom Wrappers #

Wrappers allow you to decorate grid sections such as headers, filters, and rows by wrapping the widgets with custom UI (e.g., padding, cards, borders, or shadows) without changing the cell content itself.

Important: When using wrappers with padding, you must use styleConfiguration.leftPadding and styleConfiguration.rightPadding for horizontal spacing to avoid overflow issues.

final styleConfiguration = GridSheetStyleConfiguration(
  // Horizontal padding — mainly used when wrappers are provided.
  leftPadding: 16,
  rightPadding: 16,
);

GridSheet(
  styleConfiguration: styleConfiguration,

  headerWrapper: (context, child, isPinnedLeft, isPinnedRight) {
    return Padding(
      padding: isPinnedLeft
          ? EdgeInsets.only(left: styleConfiguration.leftPadding)
          : EdgeInsets.only(right: styleConfiguration.rightPadding),
      child: Card(
        clipBehavior: Clip.antiAlias,
        margin: const EdgeInsets.only(
          top: 4,
          bottom: 4,
        ),
        shape: RoundedRectangleBorder(
          borderRadius: isPinnedLeft
              ? const BorderRadius.only(
                  topLeft: Radius.circular(8),
                  bottomLeft: Radius.circular(8),
                )
              : const BorderRadius.only(
                  topRight: Radius.circular(8),
                  bottomRight: Radius.circular(8),
                ),
        ),
        child: child,
      ),
    );
  },

  filterWrapper: (context, child, isPinnedLeft, isPinnedRight) {
    return Padding(
      padding: isPinnedLeft
          ? EdgeInsets.only(left: styleConfiguration.leftPadding)
          : EdgeInsets.only(right: styleConfiguration.rightPadding),
      child: Card(
        clipBehavior: Clip.antiAlias,
        margin: const EdgeInsets.only(bottom: 4),
        shape: RoundedRectangleBorder(
          borderRadius: isPinnedLeft
              ? const BorderRadius.only(
                  topLeft: Radius.circular(8),
                  bottomLeft: Radius.circular(8),
                )
              : const BorderRadius.only(
                  topRight: Radius.circular(8),
                  bottomRight: Radius.circular(8),
                ),
        ),
        child: child,
      ),
    );
  },

  rowWrapper: (context, child, isPinnedLeft, isPinnedRight, isHovered, isSelected) {
    return Padding(
      padding: isPinnedLeft
          ? EdgeInsets.only(left: styleConfiguration.leftPadding)
          : EdgeInsets.only(right: styleConfiguration.rightPadding),
      child: Card(
        clipBehavior: Clip.antiAlias,
        elevation: isSelected ? 4 : 1,
        margin: const EdgeInsets.only(bottom: 4),
        shape: RoundedRectangleBorder(
          borderRadius: isPinnedLeft
              ? const BorderRadius.only(
                  topLeft: Radius.circular(8),
                  bottomLeft: Radius.circular(8),
                )
              : const BorderRadius.only(
                  topRight: Radius.circular(8),
                  bottomRight: Radius.circular(8),
                ),
        ),
        child: child,
      ),
    );
  },
);

Custom Expression Evaluator #

Default Built-in GridSheetExpressionEvaluator

Extend ExpressionEvaluator to implement your own custom logic for conditional expressions: Note: Handling null values and values type in the expression evaluator is crucial for a stable expression engine.

class CustomEvaluator extends ExpressionEvaluator {
  @override
  dynamic eval(Expression expression, Map<String, dynamic> context) {
    // Custom evaluation logic
    // Handle null values, custom functions, special operators, etc.
    return super.eval(expression, context);
  }
}

GridSheet(
  evaluator: CustomEvaluator(),
  columns: [
    GridSheetColumn(
      name: 'status',
      conditionalEditExpression: 'age >= 18 && status == "Active"',
      // This expression will be evaluated using CustomEvaluator
    ),
  ],
)

Custom Cell Widgets #

Build custom widgets for any cell type:

IGridSheetCustomCellWidgetBuilder

The IGridSheetCustomCellWidgetBuilder abstract class allows developers to create and render custom Flutter widgets for individual cells in the GridSheet. By implementing this interface, you can fully control the appearance and behavior of cells at the row, column, or cell level.

This enables embedding interactive and complex widgets—such as buttons, icons, switches, progress indicators, or formatted layouts—directly within grid cells instead of using default text-based rendering. The renderer receives contextual information including the current row, cell value, row index, and column index, allowing conditional UI rendering and user interaction handling.

Using IGridSheetCustomCellWidgetBuilder provides greater flexibility and customization while maintaining GridSheet performance and structure, making it suitable for advanced tabular UI requirements.

late IGridSheetCustomCellWidgetBuilder _cellRenderer;

@override
void initState() {
  super.initState();
  _cellRenderer = CellWidgetBuilder(
      onValueChanged: (rowIndex, colIndex, value) {
        rows[rowIndex][colIndex] = value;
      },
    );
}

GridSheet(
  // Your own custom widgets for cells.
  cellBuilder: (cell) {
    switch (cell.kind) {
      case GridSheetCellKind.selectAll: // select all rows cell
        return _cellRenderer.buildSelectAll(context, cell);
      case GridSheetCellKind.indexing: // row-indexed cells
        return _cellRenderer.buildIndexes(context, cell);
      case GridSheetCellKind.header: // column-header cells
        return _cellRenderer.buildHeader(context, cell);
      case GridSheetCellKind.filter: // column-filter cells
        return _cellRenderer.buildFilter(context, cell.column, cell);
      case GridSheetCellKind.row: // column-row cells
        final row = cell.row!;
        final column = cell.column;
        return _cellRenderer.buildCell(context, column, row, cell);
    }
  },
)

// Sample custom cell builder class for reference
class CellWidgetBuilder implements IGridSheetCustomCellWidgetBuilder {
  final CellValueChanged? onValueChanged;

  CellWidgetBuilder({this.onValueChanged});

  GridSheetCellWidgetType _resolveRenderType(GridSheetColumn column) {
    if (GridSheetColumnType.boolean == column.type) {
      return GridSheetCellWidgetType.checkbox;
    }
    if (GridSheetColumnType.datetime == column.type) {
      return GridSheetCellWidgetType.date;
    }
    return GridSheetCellWidgetType.text;
  }

  @override
  Widget buildCell(
    BuildContext context,
    GridSheetColumn column,
    GridSheetRow row,
    GridSheetCellContext cell,
  ) {
    if (column.name == 'Status') {
      return _buildStatusChip(row.data[column.index].toString());
    }

    return getCellWidget(cell, context, row, column);
  }

  Widget getCellWidget(
    GridSheetCellContext cell,
    BuildContext context,
    GridSheetRow row,
    GridSheetColumn column,
  ) {
    switch (_resolveRenderType(column)) {
      case GridSheetCellWidgetType.checkbox:
        return _buildCheckbox(cell, row, column);
      case GridSheetCellWidgetType.date:
        return _buildDateCell(context, cell, row, column);
      case GridSheetCellWidgetType.text:
      default:
        return _buildTextCell(context, cell, row, column);
    }
  }
}

Using Default and Custom Cell Widgets

You can use built-in default cell widgets for certain cell kinds and provide custom widgets only where needed. To do this, return a widget for the cell kinds you want to customize, and return null for the remaining kinds so the grid falls back to its default rendering.

GridSheet(
  // Example: If you want to use the default built-in widgets for filter cells, 
  // simply return `null` for the `filter` cell kind. 
  // The grid will automatically fall back to its default renderer.
  cellBuilder: (cell) {
    switch (cell.kind) {
      case GridSheetCellKind.selectAll:
        return _cellRenderer.buildSelectAll(context, cell);

      case GridSheetCellKind.indexing:
        return _cellRenderer.buildIndexes(context, cell);

      case GridSheetCellKind.header:
        return _cellRenderer.buildHeader(context, cell);

      case GridSheetCellKind.filter:
        return null; // fall back to its default renderer.

      case GridSheetCellKind.row:
        final row = cell.row!;
        final column = cell.column;
        return _cellRenderer.buildCell(context, column, row, cell);
    }
  },
)

Context Menus #

Add right-click context menus:

GridSheet(
  contextMenuWidget: (info) {
    if (info.target == GridSheetContextMenuTarget.cell) {
      final cellInfo = info as GridSheetCellContextInfo;
      return Card(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: Icon(Icons.copy),
              title: Text('Copy'),
              onTap: () {
                // Copy cell value
              },
            ),
            ListTile(
              leading: Icon(Icons.delete),
              title: Text('Clear'),
              onTap: () async {
                await cellInfo.gridManager.clearCellRange(
                  startRow: cellInfo.row!.index,
                  endRow: cellInfo.row!.index,
                  startColumn: cellInfo.column.index,
                  endColumn: cellInfo.column.index,
                );
              },
            ),
          ],
        ),
      );
    }
    return null;
  },
)

Context Menu Configuration

contextMenuWidget controls the menu's content; GridSheetContextMenuConfiguration controls everything wrapped around it — elevation, corner radius, background color, and the full-screen backdrop behind it (which also dismisses the menu on tap):

GridSheet(
  contextMenuWidget: (info) => MyMenu(info: info),
  contextMenuConfiguration: GridSheetContextMenuConfiguration(
    elevation: 8.0, // Material elevation (drop-shadow depth) of the menu popup
    borderRadius: BorderRadius.circular(4), // Corner radius of the menu popup
    backgroundColor: null,  // null → Material's own default (theme card/canvas color)
    // Set a translucent color for a dimming scrim behind the menu
    backdropColor: Colors.transparent, 
  ),
)

Pagination #

GridSheet provides built-in pagination support with both default UI and custom implementation options.

Default Pagination

GridSheet(
  columns: columns,
  rows: rows,
  configuration: GridSheetConfiguration(
    enableDefaultPagination: true,
  ),
  styleConfiguration: styleConfiguration,
),

Custom Pagination

GridSheet(
  columns: columns,
  rows: rows,
  configuration: const GridSheetConfiguration(
    // Only one of these should be enabled.
    enableCustomPagination: true,
    enableDefaultPagination: false,
  ),
  styleConfiguration: styleConfiguration,
  footerWidget: (manager) {
    // Use the built-in pagination widget or provide your own.
    return GridSheetPagination(
      gridManager: manager,
      paginationConfiguration: PaginationConfiguration(
        backgroundColor: styleConfiguration.rowColor,
        borderColor: styleConfiguration.rowBorderColor,
        selectionColor: styleConfiguration.selectionColor,
        textStyle: theme.textTheme.bodySmall!,
      ),
    );
  },
);

Server-Side Pagination

Fetch rows from the server on every page change. The grid keeps the existing pagination UI (page buttons, rows-per-page selector) and uses the server-reported total count to calculate the correct number of pages.

Set serverSidePagination: true alongside either enableDefaultPagination or enableCustomPagination, then provide onPageChange.

If rows is empty on mount, the grid automatically fires onPageChange for page 1.

onPageChange is called for every event that requires a server fetch:

The request carries the full current state so the server can apply sort and filter in one go:

GridSheet(
  columns: columns,
  rows: const [],  // start empty — grid auto-fetches page 1
  configuration: GridSheetConfiguration(
    enableDefaultPagination: true,
    serverSidePagination: true,
    rowsPerPage: 50,
  ),
  loadingWidget: const Center(child: CircularProgressIndicator()),
  onPageChange: (GridSheetPageRequest req) async {
    // req.page              — 1-based page number
    // req.rowsPerPage       — rows per page
    // req.offset            — zero-based offset (SQL OFFSET / REST skip)
    // req.sortKeys          — List<GridSheetSortState> in priority order
    // req.filters           — Map<columnName, List<values>>
    // req.filterComparisons — Map<columnName, GridSheetFilterComparison>

    var query = supabase
        .from('users')
        .select('*', const FetchOptions(count: CountOption.exact))
        .range(req.offset, req.offset + req.rowsPerPage - 1);

    for (final sort in req.sortKeys) {
      query = query.order(sort.columnName,
          ascending: sort.direction == GridSheetSortDirection.asc);
    }

    for (final entry in req.filters.entries) {
      final cmp = req.filterComparisons[entry.key]
          ?? GridSheetFilterComparison.contains;
      for (final value in entry.value) {
        query = cmp == GridSheetFilterComparison.exact
            ? query.eq(entry.key, value)
            : query.ilike(entry.key, '%$value%');
      }
    }

    final res = await query;
    return GridSheetPageData(
      rows: res.data!.map(rowFromMap).toList(),
      totalCount: res.count ?? 0,
    );
  },
)

Note: Prefer deleteRowsByKeys over deleteRowsAt in server-side scenarios. deleteRowsAt operates on the positional index within the currently loaded rows, which may not correspond to server-side row numbers. deleteRowsByKeys uses stable keys and is unambiguous regardless of which page is loaded.

Infinite Scroll

Rows are appended automatically as the user scrolls toward the bottom. The pagination footer is hidden. Fetching stops once the number of loaded rows equals GridSheetPageData.totalCount.

GridSheet(
  columns: columns,
  rows: const [],  // start empty — first batch is fetched automatically
  configuration: GridSheetConfiguration(
    infiniteScroll: true,
    rowsPerPage: 30,              // batch size per fetch
    infiniteScrollThreshold: 300, // start loading 300 px before bottom (default 200)
  ),
  loadingWidget: const Center(child: CircularProgressIndicator()),
  onPageChange: (GridSheetPageRequest req) async {
    // req.offset      — number of already-loaded rows (reliable server OFFSET)
    // req.sortKeys    — active sorts (re-sent on sort change after full reset)
    // req.filters     — active filters (re-sent on filter change after full reset)
    var query = supabase
        .from('users')
        .select('*', const FetchOptions(count: CountOption.exact))
        .range(req.offset, req.offset + req.rowsPerPage - 1);

    for (final sort in req.sortKeys) {
      query = query.order(sort.columnName,
          ascending: sort.direction == GridSheetSortDirection.asc);
    }

    final res = await query;
    return GridSheetPageData(
      rows: res.data!.map(rowFromMap).toList(),
      totalCount: res.count ?? 0,
    );
  },
  footerWidget: (manager) {
    if (!manager.hasMoreRows) {
      return const Padding(
        padding: EdgeInsets.all(12),
        child: Text('All rows loaded'),
      );
    }
    return const SizedBox.shrink();
  },
)

Key points:

  • onPageChange is the single callback for page navigation, sort, filter, and infinite-scroll triggers — no separate API needed.
  • manager.hasMoreRows becomes false once all server rows are loaded.

Selection Events #

GridSheet provides comprehensive selection callbacks for rows, columns, and cells. Each callback provides detailed information about the selection state and selected data.

Row Selection Event

Triggered when row selection changes (click on row index or first column). To enable the Select All Rows feature from the serial number column header, ensure the following property is set: enableRowSelectionOnFirstColumnTap: true

The header checkbox is tri-state: unchecked when no rows are selected, indeterminate (a dash) when some but not all are selected, and checked when every row is selected. Tapping it always selects every row unless all are already selected, in which case it deselects all — the indeterminate state is only ever a display of the current selection, never something you tap into directly.

GridSheet(
  configuration: GridSheetConfiguration(
    enableRowSelectionOnFirstColumnTap: true,
    enableMultiSelection: true,  // Allow selecting multiple rows
  ),
  onRowsSelected: (event) {
    // Access selection data
    debugPrint('Selected ${event.selectedRowKeys.length} rows');
    
    // Get selected row objects
    for (final row in event.selectedRows) {
      debugPrint('Row ${row.index}: ${row.data}');
      debugPrint('Row state: ${row.state}');  // existing, inserted, modified, deleted
    }
    
    // Get selected rows as maps (column name → value)
    for (final rowMap in event.selectedRowsData) {
      debugPrint('Name: ${rowMap['name']}, Age: ${rowMap['age']}');
    }
    
    // Access grid manager for further operations
    final manager = event.gridManager;
    
    // Example: Delete selected rows
    // await manager.deleteRowsByKeys(event.selectedRowKeys);
  },
)

Column Selection Event

Triggered when the column selection changes (via interaction with the column header):

  • Single click → Selects the column
  • Double click → Sorts the column (if sorting is enabled)
  • Long press → Reorders columns (if reordering is enabled)
GridSheet(
  configuration: GridSheetConfiguration(
    enableColumnSelection: true,
    enableMultiSelection: true, // Allow selecting multiple columns
  ),
  onColumnsSelected: (event) {
    // Access selection data
    debugPrint('Selected ${event.selectedColumnKeys.length} columns');
    
    // Get selected column objects
    for (final column in event.selectedColumns) {
      debugPrint('Column: ${column.name} (${column.title})');
      debugPrint('Type: ${column.type}, Width: ${column.width}');
    }
    
    // Get column names
    debugPrint('Selected columns: ${event.selectedColumnNames.join(", ")}');
    
    // Get all values from selected columns
    event.selectedColumnsData.forEach((columnName, values) {
      debugPrint('$columnName has ${values.length} values');
      debugPrint('Values: $values');
    });
    
    // Access grid manager
    final manager = event.gridManager;
    
    // Example: Hide selected columns
    // await manager.hideColumns(event.selectedColumnNames);
    
    // Example: Auto-fit selected columns
    // await manager.autoFitColumnWidths(event.selectedColumnNames);
  },
)

Cell Selection Event

Triggered when cell selection changes (click on individual cells).


GridSheet(
  configuration: GridSheetConfiguration(
    enableCellSelection: true,
    enableMultiSelection: true, // Allow selecting multiple cells
  ),
  onCellsSelected: (event) {
    // Access selection data
    debugPrint('Selected ${event.selectedCells.length} cells');
    
    // Get selected cell keys
    for (final cellKey in event.selectedCells) {
      debugPrint('Cell: Row=${cellKey.rowKey}, Column=${cellKey.columnKey}');
    }
    
    // Get selected cell data grouped by column
    event.selectedCellsData.forEach((columnName, values) {
      debugPrint('Column "$columnName": $values');
      // Example: ['Active', 'Pending', 'Active']
    });
    
    // Access grid manager
    final manager = event.gridManager;
    
    // Example: Clear selected cells
    // manager.clearCellsByKeys(event.selectedCells);
    
    // Example: Fill selected cells with a value
    // manager.fillCellsByKeys(event.selectedCells, 'New Value');
  },
)

No Rows Custom Widget #

Provide a custom widget to display when the grid has no rows. This allows you to show personalized messages or visuals instead of leaving the grid empty. If no custom widget is supplied, the grid automatically falls back to its default built-in widget.

GridSheet(
  columns: columns,
  rows: rows,
  styleConfiguration: styleConfiguration,
  onLoaded: (event) {
    gridManager = event.gridManager;
  },
  noRowsWidget: Padding(
    padding: EdgeInsets.all(4),
    child: Text(
      'No data to display',
      style: theme.textTheme.bodySmall,
    ),
  ),
);

No Columns Custom Widget #

Provide a custom widget to display when every column is hidden (via GridSheetColumn.visible: false, or hidden at runtime with GridSheetManager.hideColumns). It replaces the entire table — header, filters, and body — since there's nothing meaningful left to show in any of them; headerWidget/footerWidget stay visible regardless, since that's typically where the controls to show a column again live. The built-in serial-number column (GridSheet.indexColumn) doesn't count as a real column for this check — it can never be hidden, so a grid with every actual column hidden would otherwise render as a bare, unexplained strip of row numbers. If no custom widget is supplied, the grid automatically falls back to its default built-in widget.

GridSheet(
  columns: columns,
  rows: rows,
  styleConfiguration: styleConfiguration,
  onLoaded: (event) {
    gridManager = event.gridManager;
  },
  noColumnsWidget: Padding(
    padding: EdgeInsets.all(4),
    child: Text(
      'Every column is hidden',
      style: theme.textTheme.bodySmall,
    ),
  ),
);

Custom Loading Widget #

Provide your own custom loading widget to display while the grid is fetching or processing data, replacing the default loading indicator with a personalized UI for async operations. Note: If loadingWidget is not provided, no loading indicator will be shown, as there is no default loading widget in the grid.

GridSheet(
  columns: columns,
  rows: rows,
  styleConfiguration: styleConfiguration,
  onLoaded: (event) {
    gridManager = event.gridManager;
  },
  loadingWidget: loadingWidget,
);

Widget get loadingWidget {
  return Center(
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        CircularProgressIndicator(),
        SizedBox(height: 16),
        Text(
          'Processing...',
          style: TextStyle(color: Colors.white, fontSize: 16),
        ),
      ],
    ),
  );
}

Undo Delete #

Shows a "N row(s)/column(s) deleted — UNDO" prompt, instead of deleting permanently right away. Enabled by default (enableUndoDelete: true); set it to false to restore immediate, permanent deletion.

With no undoDeleteWidget supplied, a built-in banner is shown at the bottom of the grid for 5 seconds. Provide undoDeleteWidget for a fully custom prompt (own message, styling, position, and/or timeout):

GridSheet(
  columns: columns,
  rows: rows,
  configuration: GridSheetConfiguration(
    enableUndoDelete: true,
  ),
  onLoaded: (event) {
    gridManager = event.gridManager;
  },
  undoDeleteWidget: (context, event) {
    final count = event.isColumnDeletion
        ? event.deletedColumns.length
        : event.deletedRows.length;
    final noun = event.isColumnDeletion ? 'column' : 'row';

    return Material(
      color: Colors.black87,
      child: ListTile(
        title: Text('$count $noun${count == 1 ? '' : 's'} deleted', style: TextStyle(color: Colors.white)),
        trailing: TextButton(
          onPressed: event.onUndo,
          child: Text('UNDO'),
        ),
      ),
    );
  },
);
  • onUndo - call to restore the deleted row(s)/column(s)
  • onDismiss - call to finalize the deletion permanently (e.g. from your own timer or a close button)

Autofill (Cell Drag-to-Copy) #

This feature provides autofill cell drag to copy functionality, allowing users to quickly populate cells by dragging a fill handle.

When enabled, a small square handle appears at the bottom-right corner of selected cells. Users can drag this handle to copy data to adjacent cells, with automatic pattern detection and formula adjustment.

GridSheet(
  columns: columns,
  rows: rows,
  configuration: GridSheetConfiguration(
    enableCellSelection: true,  //Required for autofill
    enableMultiSelection: true, // Required for autofill pattern detection
  ),
  autofillConfiguration: GridSheetAutofillConfiguration(
    enabled: true,
    fillHandleSize: 8.0, 
    fillHandleColor: Colors.blue,
    showPreviewOverlay: true,
    enablePatternDetection: true, 
    enforceTypeMatching: true, // Strict type checking (horizontal)
  ),
);

How It Works:

  1. Cell Selection
  • Select one or more cells that contain the source data
  • A small fill handle appears at the bottom-right corner of the selection
  1. Drag Operation
  • Click and hold the fill handle
  • Drag in any direction (up, down, left, right)
  • A preview overlay shows which cells will be filled
  1. Release to Apply
  • Release the mouse button to apply the fill.
  • Target cells are populated with data based on the detected pattern.

Data Operations #

Change Tracking:

// Get changed data
final inserted = gridManager.insertedRows;
final modified = gridManager.modifiedRows;
final deleted = gridManager.deletedRows;
final allChanges = gridManager.changedRows;

// Persist changes
for (final row in inserted) {
  await api.createRow(gridManager.rowsDataToMapList([row]).first);
}

// Undo changes
await gridManager.undoRowChanges(rowKey);
await gridManager.undoAllRowsChanges();

Bulk Operations:

// Find cells
final highSalaries = gridManager.findCells(
  predicate: (value, row, column) => value is num && value > 100000,
  columnNames: ['salary'],
);

// Find and replace
final count = await gridManager.findAndReplace(
  findValue: 'Active',
  replaceValue: 'Completed',
  exactMatch: true,
);

// Fill pattern
await gridManager.fillCells(
  startRowKey: ValueKey('row_1'),
  endRowKey: ValueKey('row_10'),
  columnKey: ValueKey('col_1'),
  value: 1,
);

// fill vertical cells 
final skipRows = computeRowsToSkip();
await gridManager.autofillVerticalCells(
  startRowKey: ValueKey('row_1'),
  endRowKey: ValueKey('row_100'),
  columnKey: ValueKey('col_a'),
  sourceValues: [1, 2], // increment value
  enablePatternDetection: true, 
  skipRowKeys: skipRows,  // skip cells
);

// fill horizontal cells 
final skipColumns = computeColumnsToSkip();
await gridManager.autofillHorizontalCells(
  startRowKey: ValueKey('row_1'),
  endRowKey: ValueKey('row_5'),
  startColumnKey: ValueKey('col_q1'),
  endColumnKey: ValueKey('col_q4'),
  sourceValues: [1, 2], // increment value
  enablePatternDetection: true,
  skipRowKeys: skipRows,  // skip cells
  skipColumnKeys: skipColumns,  // skip cells
);

Auto-Sizing: Auto-fit calculations rely on text measurements. Ensure StyleConfiguration headerTextStyle and cellTextStyle are provided for correct results.

// Auto-fit column widths
await gridManager.autoFitColumnWidths(['firstName', 'lastName']);
await gridManager.autoFitAllColumnWidths();

// Auto-fit row heights
await gridManager.autoFitRowHeights([rowKey1, rowKey2]);
await gridManager.autoFitAllRowHeights();

Platform Support #

GridSheet works in web browsers and supports all Flutter platforms:

  • ✅ Android
  • ✅ iOS
  • ✅ Web (Chrome, Edge, Firefox, Safari)
  • ✅ Windows
  • ✅ macOS
  • ✅ Linux

Notes #

Performance Considerations #

  • Both rows and non-pinned columns are virtualized (windowed): only the rows and columns currently visible, plus a small overscan buffer, are actually built — scrolling in either direction stays smooth into the 100K-row / hundreds-of-column range without needing pagination purely for rendering performance.
  • Pinned-left and pinned-right columns are never windowed (there are typically few of them), and always fully built for every visible row.
  • Pagination (client-side, server-side, or infinite scroll) is still the right tool when the dataset itself — not just its on-screen rendering — is too large to hold in memory at once; use server-side pagination or infinite scroll for very large remote datasets so only one page of rows is fetched/held at a time.
  • Limit the number of conditional format rules for better performance
  • Avoid excessive column reordering in production
  • The controller pool automatically manages TextEditingControllers, bounding retention to the visible row window (plus overscan) rather than the whole current page — most impactful with pagination disabled, where the "current page" is the entire dataset
  • Column/row drag-resize and row-expansion animation frames update only the affected row or column's layout instead of rebuilding the whole grid's row/column extents
  • Formula expressions are parsed once and cached by their text, so repeated recalculation of the same formula doesn't re-tokenize/re-parse it

Limitations #

  • Browser storage APIs (localStorage, sessionStorage) are not supported in artifacts mode
  • Column expressions use the expressions package for evaluation
  • Custom evaluators can be provided for advanced expression handling
  • Multi-column sorting preserves order by priority (first added = highest priority)

Best Practices #

  1. Use stable keys - Always provide unique ValueKey<String> for rows and columns
  2. Leverage callbacks - Use onCellValueChanged to track changes in real-time
  3. Manage state externally - Use GridSheetManager from callbacks to update external state
  4. Optimize rendering - Use noTextControllerWidget: true for read-only columns or when using custom widgets that do not require a TextEditingController, preventing unnecessary controller creation and improving performance. showOverflowTooltip defaults to false (no per-cell overflow measurement); opt it in with showOverflowTooltip: true only on columns where truncated text is likely (e.g. free-form text columns) and the hover tooltip is worth the cost.
  5. Handle loading states - Provide a loadingWidget for better UX during async operations

Contributions #

If GridSheet has been helpful, please consider giving our Pub package a 👍. You can also support the project by spreading the word and sharing it with others.

We truly appreciate your support and contributions to the ongoing development of this project. We welcome discussions, feedback, and pull requests—feel free to share your ideas or suggestions on our GitHub repository. Issues Tracker

License #

This project is licensed under the MIT License - see the LICENSE file for details.

4
likes
160
points
516
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A powerful Flutter DataGrid/DataTable for large datasets, offering Excel-like features and fully customizable cells — ready with minimal setup. See README for full features

Homepage
View/report issues

Topics

#data-table #data-grid #table #excel #spreadsheet

License

MIT (license)

Dependencies

expressions, flutter

More

Packages that depend on grid_sheet