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

A Flutter DataGrid/DataTable with minimal configuration and powerful features like filtering, formulas, pagination, editing, frozen columns, CRUD, and full customization.

Grid Sheet #

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, column resizing, column formula's and advanced cell customization, making it a robust alternative to Flutter’s built-in DataTable.

With Grid Sheet, you can fully customize table headers, rows, filter 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

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
  • Column Freezing - Pin columns to the left side for horizontal scrolling
  • Column Reordering - Drag-and-drop column reordering with visual feedback
  • Column Resizing - Resize columns by dragging column borders
  • Cell Editing - Inline cell editing with type-specific input formatters

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
  • 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 #

  • 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: ^1.1.9

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 - Layout and behavior settings
  • styleConfiguration - Visual styling options
  • scrollConfiguration - Scrollbar customization
  • 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
  • onCellValueChange - Callback when cell value changes
  • 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
await gridManager.insertRowsAt(index, rows);
await gridManager.deleteRowsByKeys(rowKeys);
gridManager.updateRowByKey(rowKey, newData);

// Column operations
await gridManager.insertColumnsAt(index, columns);
await gridManager.deleteColumnsByKeys(columnKeys);
await gridManager.hideColumns(['COL_NAME']);
await gridManager.showColumns(['COL_NAME']);

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

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
await gridManager.goToPage(3);
gridManager.nextPage();
gridManager.previousPage();

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

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

Data Export:

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

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

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

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
  type: GridSheetColumnType.text,
  width: 150,
  visible: true,
  frozen: false,               // Pin to left
  editable: true,
  sortable: true,
  resize: true,
  textAlign: TextAlign.left,
  conditionalEditExpression: 'age > 18',  // Control editability
)

GridSheetRow

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

GridSheetCell

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

Configuration #

Layout Configuration #

GridSheetLayoutConfiguration(
  // Pagination
  enableDefaultPagination: true,
  enableCustomPagination: false,
  rowsPerPage: 20,
  
  // Selection
  enableRowSelectionOnFirstColumnTap: true,
  enableColumnSelection: true,
  enableCellSelection: true,
  enableMultiSelection: true,
  enableRowSelectionOnFirstColumnTap: true,
  selectOnlyPageRows: false,
  
  // Visual
  enableRowHover: true,
  showBorderOnSelection: false,
  serialNumberColumn: true,
  serialNumberColumnWidth: 60,
  showRowBorders: true,
  showColumnBorders: true,
  showColumnFilters: true,
  
  // Interaction
  enableReorder: true,
  enableRowContextMenu: true,
  enableColumnContextMenu: true,
  enableCellContextMenu: true,
  
  // Sizing
  headerHeight: 40,
  filterHeight: 40,
  rowHeight: 40,
)

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]!,
  
  // 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),
  
  // Padding
  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

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,
  frozen: 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"',
)

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 of column is enabled by long-pressing on a column header and dragging it to the desired position.

// To enable column reordering
GridSheetLayoutConfiguration(
  enableReorder: true,
)

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'.
rows = [
  ['=F1 * 2'],
  ['=SUM(E1:F3)'],
];

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

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.

GridSheet(
  conditionalFormatRules: [
    // Row-level formatting
    GridSheetConditionalFormatRule(
      name: 'HighlightHighValues',
      expression: 'age > 50',
      scope: GridSheetFormatScope.row,
      backgroundColor: Colors.red[100],
    ),
    
    // Column-level formatting
    GridSheetConditionalFormatRule(
      name: 'status',
      expression: 'status == "Active"',
      scope: GridSheetFormatScope.column,
      backgroundColor: Colors.green[100],
    ),
    
    // Cell-level formatting
    GridSheetConditionalFormatRule(
      name: 'salary',
      expression: 'salary > 100000',
      scope: GridSheetFormatScope.cell,
      backgroundColor: Colors.orange[100],
      fontStyle: TextStyle(fontWeight: FontWeight.bold),
    ),
  ],
)

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

// Add rule
await gridManager.addConditionalFormatRule(rule);

// Remove rule
await gridManager.removeConditionalFormatRule(
  expression: 'age > 50',
  scope: GridSheetFormatScope.row,
);

// Update rule
await gridManager.updateConditionalFormatRule(
  oldExpression: 'age > 50',
  scope: GridSheetFormatScope.row,
  newRule: updatedRule,
);

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(
        leftPadding: 16,   // Use these for horizontal padding
        rightPadding: 16,
    );

    GridSheet(
      styleConfiguration: styleConfiguration,
      headerWrapper: (context, child, isFrozen) {
        return Padding(
          padding: isFrozen
              ? 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: isFrozen
                  ? 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, isFrozen) {
        return Padding(
          padding: isFrozen
              ? EdgeInsets.only(left: styleConfiguration.leftPadding)
              : EdgeInsets.only(right: styleConfiguration.rightPadding),
          child: Card(
            clipBehavior: Clip.antiAlias,
            margin: EdgeInsets.only(
              bottom: 4,
            ),
            shape: RoundedRectangleBorder(
              borderRadius: isFrozen
                  ? 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, isFrozen, isHovered) {
        return Padding(
          padding: isFrozen
              ? EdgeInsets.only(left: styleConfiguration.leftPadding)
              : EdgeInsets.only(right: styleConfiguration.rightPadding),
          child: Card(
            clipBehavior: Clip.antiAlias,
            margin: EdgeInsets.only(bottom: 4),
            shape: RoundedRectangleBorder(
              borderRadius: isFrozen
                  ? 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: _getColumns(),
      rows: _getRows(),
      configuration: GridSheetLayoutConfiguration(
        enableDefaultPagination: true,
      ),
      styleConfiguration: _styleConfiguration,
   ),

Custom Pagination

   GridSheet(
      columns: _getColumns(),
      rows: _getRows(),
      configuration: GridSheetLayoutConfiguration(
        // 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,
          styleConfiguration: PaginationConfiguration(
            backgroundColor: _styleConfiguration.rowColor,
            borderColor: _styleConfiguration.rowBorderColor,
            selectionColor: _styleConfiguration.selectionColor,
            textStyle: theme.textTheme.bodySmall!,
          ),
        );
      },
   ),

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: GridSheetLayoutConfiguration(
    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 column selection changes (click on column header).

GridSheet(
  configuration: GridSheetLayoutConfiguration(
    enableColumnSelection: true,
    enableMultiSelection: true,
  ),
  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: GridSheetLayoutConfiguration(
    enableCellSelection: true,
    enableMultiSelection: true,
  ),
  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: _getColumns(),
      rows: _getRows(),
      configuration: GridSheetLayoutConfiguration(
        enableDefaultPagination: true,
      ),
      styleConfiguration: styleConfiguration,
      conditionalFormatRules: getFormattingRules(),
      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: _getColumns(),
      rows: _getRows(),
      configuration: GridSheetLayoutConfiguration(
        enableDefaultPagination: true,
      ),
      styleConfiguration: styleConfiguration,
      conditionalFormatRules: getFormattingRules(),
      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),
          ),
        ],
      ),
    );
  }

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_sequence'),
  value: 1,
  increment: true,  // Auto-increment numbers
);

Auto-Sizing:

// 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 supports all Flutter platforms:

  • ✅ Android
  • ✅ iOS
  • ✅ Web
  • ✅ Windows
  • ✅ macOS
  • ✅ Linux

Notes #

Performance Considerations #

  • Use pagination for large datasets (recommended: 50-100 rows per page)
  • 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 onCellValueChange 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 Grid Sheet 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.

1
likes
160
points
489
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter DataGrid/DataTable with minimal configuration and powerful features like filtering, formulas, pagination, editing, frozen columns, CRUD, and full customization.

Homepage
View/report issues

Topics

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

Documentation

API reference

License

MIT (license)

Dependencies

cupertino_icons, expressions, flutter

More

Packages that depend on grid_sheet