Flutter Table Plus

pub version license Flutter platforms

A highly customizable, type-safe Flutter table widget with synchronized scrolling, sorting, selection, editing, and more.

Flutter Table Plus Demo


✨ Features

Feature Description
Type-Safe Generics Works with any data model <T> — no more Map<String, dynamic>
Synchronized Scrolling Header and body scroll together seamlessly
Sorting Multi-state column sorting (ascending ↔ descending ↔ none)
Selection Single or multiple row selection with checkboxes
Inline Editing Click-to-edit cells with auto-save
Column Reordering Drag-and-drop columns, drop on empty space to move to last
Column Resizing Drag header edges to resize columns with min/max constraints and width persistence
Auto-Fit Columns Double-tap resize handle to auto-fit, with custom override callback
Stretch Last Column Last column fills remaining space when all columns are fixed
Drag Selection Mouse drag to select row ranges with auto-scroll and a configurable rubber band rectangle (Finder/Explorer-style marquee)
Merged Rows Group rows with custom merged content
Hover Buttons Action buttons on row hover
Dynamic Row Heights Support for variable height rows
Smart Tooltips Text, widget, and whole-row tooltips, anchored beside the widget or beside the cursor
Dim Rows Style inactive rows differently
Smooth Wheel Scrolling Animated mouse wheel scrolling, on by default and tuned or turned off through wheelMotion, with the header and scrollbars in step
Scale / Zoom Ctrl/Cmd+wheel zoom with platform-aware modifier key, scroll-safe physics, automatic position correction, and optional blockModifierScroll control
Deep Theming One call from your app's ColorScheme, or nested theme classes down to individual borders, dividers and placeholder text
Minimal Dependencies Only just_tooltip, flutter_checkbox and flutter_smooth_wheel_scroll

📦 Installation

dependencies:
  flutter_table_plus: ^2.19.0
flutter pub get

🚀 Quick Start

1. Define Your Data Model

class User {
  final String id;
  final String name;
  final String email;
  final int age;

  const User({
    required this.id,
    required this.name,
    required this.email,
    required this.age,
  });
}

2. Create the Table

import 'package:flutter_table_plus/flutter_table_plus.dart';

final columns = TableColumnsBuilder<User>()
  ..addColumn('name', TablePlusColumn(
    key: 'name',
    label: 'Name',
    order: 1,
    width: 150,
    valueAccessor: (user) => user.name,
    sortable: true,
  ))
  ..addColumn('email', TablePlusColumn(
    key: 'email',
    label: 'Email',
    order: 2,
    width: 200,
    valueAccessor: (user) => user.email,
  ))
  ..addColumn('age', TablePlusColumn(
    key: 'age',
    label: 'Age',
    order: 3,
    width: 80,
    valueAccessor: (user) => user.age,
    sortable: true,
  ));

final users = [
  User(id: '1', name: 'John Doe', email: 'john@example.com', age: 28),
  User(id: '2', name: 'Jane Smith', email: 'jane@example.com', age: 34),
];

FlutterTablePlus<User>(
  columns: columns.build(),
  data: users,
  rowId: (user) => user.id,  // Unique identifier for each row

  // Colours from your app's ColorScheme, so a dark app gets a dark table
  theme: TablePlusTheme.fromColorScheme(Theme.of(context).colorScheme),

  // Sorting
  sortColumnKey: _sortColumn,
  sortDirection: _sortDirection,
  onSort: (columnKey, direction) {
    setState(() {
      _sortColumn = columnKey;
      _sortDirection = direction;
      // Sort your data here
    });
  },

  // Selection
  isSelectable: true,
  selectionMode: SelectionMode.multiple,
  selectedRows: _selectedRows,
  onRowSelectionChanged: (rowId, isSelected) {
    setState(() {
      isSelected ? _selectedRows.add(rowId) : _selectedRows.remove(rowId);
    });
  },
)

3. Persist Column Widths

Save and restore user-resized column widths across sessions:

FlutterTablePlus<User>(
  columns: columns.build(),
  data: users,
  rowId: (user) => user.id,
  resizable: true,

  // Restore saved widths (reacts to changes — safe with Riverpod watch)
  initialResizedWidths: savedWidths, // e.g. {'name': 200, 'email': 150}

  // Save widths when user resizes
  onColumnResized: (columnKey, newWidth) {
    savedWidths[columnKey] = newWidth;
    // Persist to DB / SharedPreferences / etc.
  },
)

4. Tune or Turn Off Wheel Scrolling

The mouse wheel animates by default, as WheelMotion.spring() (400ms, no bounce). wheelMotion changes it, and can change at runtime:

FlutterTablePlus<User>(
  columns: columns.build(),
  data: users,
  rowId: (user) => user.id,

  // Pick one:
  wheelMotion: const WheelMotion.spring(
    duration: Duration(milliseconds: 250),
    bounce: 0.1,
  ),
  // wheelMotion: const WheelMotion.curve(curve: Curves.easeOut), // any Curve, your own too
  // wheelMotion: const WheelMotion.lerp(
  //   timeConstant: Duration(milliseconds: 80),
  // ),
  // wheelMotion: null, // no animation: every notch moves at once
)

WheelMotion is exported by this package; there is no second import. See Smooth Wheel Scrolling for every option.

5. Match Your App's Colours

TablePlusTheme.fromColorScheme builds the whole table theme from the ColorScheme your app already has: surfaces, dividers, the selected row, the editing cell, the checkbox. Adjust the result with copyWith:

final base = TablePlusTheme.fromColorScheme(Theme.of(context).colorScheme);

FlutterTablePlus<User>(
  // ...
  theme: base.copyWith(
    bodyTheme: base.bodyTheme.copyWith(rowHeight: 40),
  ),
)

To set every value yourself instead, build a TablePlusTheme(...) directly. Without a theme, the table uses the package's own light defaults, as it always has. The Theming Guide lists which role each colour takes.


💡 Core Philosophy

Flutter Table Plus follows a UI-only, data-agnostic design:

  • ✅ You own your data — The package never stores or mutates your data
  • ✅ Callback-driven — All interactions flow through callbacks you control
  • ✅ State management agnostic — Works with setState, Provider, Riverpod, Bloc, etc.
  • ✅ Maximum flexibility — No assumptions about your data structure or business logic
// You handle sorting
onSort: (columnKey, direction) {
  // Sort your data however you want -- into a NEW list.
  // `data` and `rowId` are read as one snapshot and invalidated on the
  // list's identity, so an in-place `_myData.sort(...)` is not seen.
  _myData = List.of(_myData)..sort(...);
}

// You handle selection
onRowSelectionChanged: (rowId, isSelected) {
  // Update your selection state
  _selectedIds.add(rowId);
}

// You handle editing
onCellChanged: (row, columnKey, rowIndex, oldValue, newValue) {
  // Update your data model -- again into a NEW list. An in-place
  // `_myData[rowIndex] = ...` renders the new value but leaves the cached
  // row height, so a `calculateRowHeight` that measures the cell keeps the
  // pre-edit height.
  _myData = List.of(_myData)..[rowIndex] = row.copyWith(name: newValue);
}

📖 Documentation

Guide Description
Features Guide Sorting, Selection, Editing, Merged Rows, Hover Buttons, Smooth Wheel Scrolling, and more
Theming Guide Every theme class, what each field reaches, and what scaledBy scales
Migration Guide Migrating from v1.x (Map) to v2.x (Generic<T>)


📄 License

MIT License — see LICENSE for details.

Libraries

flutter_table_plus
A highly customizable and efficient table widget for Flutter.