grid_sheet 2.0.3 copy "grid_sheet: ^2.0.3" to clipboard
grid_sheet: ^2.0.3 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 #

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 Freezing - Pin columns to the left side for 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 using arrow keys
  • Select All - Checkbox 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 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.0.3

Then run:

flutter pub get

Import #

import 'package:grid_sheet/grid_sheet.dart';

Usage #

Basic Setup #

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!');
	},
),       
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
  • configuration - GridSheet Layout and behavior settings
  • 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
  • loadingWidget - Widget shown during async operations
  • 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,
    serialNumberColumn: true,
    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
  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)
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
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,
  conditionalFormatIncludesPinnedLeftCells: false,

  // Position based insertion
  rowInsertPosition: GridSheetRowInsertPosition.aboveSelection,
  columnInsertPosition: GridSheetColumnInsertPosition.beforeSelection,
  
  // Visual
  enableRowHover: true,
  enableRowResize: false,
  showBorderOnSelection: false,
  serialNumberColumn: true,
  serialNumberColumnWidth: 60,
  showRowBorders: true,
  showColumnBorders: true,
  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
)

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
    type: GridSheetColumnType.integer, // Data type applied to all generated columns
    width: 150.0,                // Default width for each generated column
    rowState: GridSheetRowState.inserted, // Displays row state indicators in the serial number column
  ),
);

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. If null, the theme default is used.
  showPreviewOverlay: true, // Whether to show a visual preview while dragging to auto-fill.
  enablePatternDetection: true, // Automatically detects and continues patterns (e.g., numbers, dates).
  enforceTypeMatching: false, // Prevents auto-fill if source and target cell value column types do not match.
  dragCursor: SystemMouseCursors.cell, //Mouse cursor displayed while dragging the fill handle.
)

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.
)

Row Expansion Configuration #

GridSheetRowExpansionConfiguration(
  enabled: true,              // Show expansion chevron in the serial-number column
  panelHeight: 300.0,         // Fixed height of the expansion panel in pixels
  animationDuration: Duration(milliseconds: 200), // Open/close animation speed
  backgroundColor: null,      // Panel background; null → inherits styleConfiguration.rowColor
  icon: Icons.chevron_right,  // Icon shown in the serial-number column (rotates 90° when open)
  showOnlyEditableColumns: true, // Only show columns editable for that row
)

Column Animation Configuration #

GridSheetAnimationConfiguration(
  enabled: false,                              // Off by default
  animationDuration: Duration(milliseconds: 700), // Total time for the whole cascade, not a single column
  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
  animateRowsOnSortOrFilter: false,             // Replay the same cascade per-row after a sort/filter
  cellChangeFlashEnabled: false,               // Flash a cell's background when its value changes
  cellChangeFlashColor: Colors.amber,          // Flash color
  cellChangeFlashDuration: Duration(milliseconds: 800), // How long the flash takes to fade out
  columnVisibilityFadeEnabled: false,          // Fade a column in when it's shown via showColumns
)
  • 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.

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]!,
  rowBorderColor: Colors.grey[300]!,
  columnBorderColor: Colors.grey[300]!,
  gridBorderRadius: BorderRadius.all(Radius.circular(4)),
  
  // 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,
  visible: true,
  noTextControllerWidget: false,  // 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.
  
  // Appearance
  width: 150,
  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 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:

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 Show/hide the serial-number (index) column
serialNumberColumnWidth Width of the serial-number column
showRowBorders Show/hide horizontal row borders
showColumnBorders Show/hide vertical column borders
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

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,
  ),
);

// Example: toggle the serial-number column
gridManager.updateConfiguration(
  gridManager.configuration.copyWith(
    serialNumberColumn: !gridManager.configuration.serialNumberColumn,
  ),
);

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 columns are excluded from conditional formatting by default — set conditionalFormatIncludesPinnedLeftCells: 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',
  stopIfTrue: true, // errors always win outright — no lower-priority rule adds to this cell
)

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.

  GridSheetStyleConfiguration styleConfiguration = GridSheetStyleConfiguration(
    // Horizontal padding — use it mostly when wrappers are provided.
    leftPadding: 16, 
    rightPadding: 16,
  );

  GridSheet(
    styleConfiguration: styleConfiguration,
    headerWrapper: (context, child, isPinnedLeft) {
      return Padding(
        padding: isPinnedLeft
            ? EdgeInsets.only(left: styleConfiguration.leftPadding)
            : EdgeInsets.only(right: styleConfiguration.rightPadding),
        child: Card(
          clipBehavior: Clip.antiAlias,
          margin: EdgeInsets.only(
            top: 4,
            bottom: 4,
          ),
          shape: RoundedRectangleBorder(
            borderRadius: isPinnedLeft
                ? BorderRadius.only(
                    topLeft: Radius.circular(8),
                    bottomLeft: Radius.circular(8),
                  )
                : BorderRadius.only(
                    topRight: Radius.circular(8),
                    bottomRight: Radius.circular(8),
                  ),
          ),
          child: child,
        ),
      );
    },
    filterWrapper: (context, child, isPinnedLeft) {
      return Padding(
        padding: isPinnedLeft
            ? EdgeInsets.only(left: styleConfiguration.leftPadding)
            : EdgeInsets.only(right: styleConfiguration.rightPadding),
        child: Card(
          clipBehavior: Clip.antiAlias,
          margin: EdgeInsets.only(
            bottom: 4,
          ),
          shape: RoundedRectangleBorder(
            borderRadius: isPinnedLeft
                ? BorderRadius.only(
                    topLeft: Radius.circular(8),
                    bottomLeft: Radius.circular(8),
                  )
                : BorderRadius.only(
                    topRight: Radius.circular(8),
                    bottomRight: Radius.circular(8),
                  ),
          ),
          child: child,
        ),
      );
    },
    rowWrapper: (context, child, isPinnedLeft, isHovered) {
      return Padding(
        padding: isPinnedLeft
            ? EdgeInsets.only(left: styleConfiguration.leftPadding)
            : EdgeInsets.only(right: styleConfiguration.rightPadding),
        child: Card(
          clipBehavior: Clip.antiAlias,
          margin: EdgeInsets.only(bottom: 4),
          shape: RoundedRectangleBorder(
            borderRadius: isPinnedLeft
                ? BorderRadius.only(
                    topLeft: Radius.circular(8),
                    bottomLeft: Radius.circular(8),
                  )
                : 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;
  },
)

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: GridSheetConfiguration(
      // both should not be true
      enableCustomPagination: true,
      enableDefaultPagination: false,
    ),
    styleConfiguration: styleConfiguration,
    footerWidget: (manager) {
      /* you can utilize built-in widget or your own custom widget */
      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

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,
    ),
  ),
);

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),
        ),
      ],
    ),
  );
}

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 #

  • Use pagination for large datasets (recommended: 50-100 rows per page)
  • For very large remote datasets use server-side pagination or infinite scroll so only one page of rows is held in memory 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 to prevent memory leaks

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.
  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.

3
likes
160
points
762
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