Migrating from Flutter Table Plus v1.x to v2.x.
Version 2.0 introduces type-safe generics to replace the previous Map<String, dynamic> approach. This provides:
- ✅ Compile-time type checking
- ✅ IDE autocompletion for your data model
- ✅ No more string-based key lookups
- ✅ Cleaner, more maintainable code
| v1.x | v2.x |
|---|---|
FlutterTablePlus(...) |
FlutterTablePlus<YourModel>(...) |
data: List<Map<String, dynamic>> |
data: List<YourModel> |
rowIdKey: 'id' |
rowId: (row) => row.id |
dimRowKey: 'isActive' |
isDimRow: (row) => !row.isActive |
TablePlusColumn(key: 'name', ...) |
TablePlusColumn(valueAccessor: (row) => row.name, ...) |
onCellChanged: (rowId, columnKey, newValue) |
onCellChanged: (row, columnKey, rowIndex, oldValue, newValue) |
Before (v1.x):
final data = [
{'id': '1', 'name': 'John', 'email': 'john@example.com', 'age': 28},
{'id': '2', 'name': 'Jane', 'email': 'jane@example.com', 'age': 34},
];After (v2.x):
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,
});
User copyWith({String? name, String? email, int? age}) {
return User(
id: id,
name: name ?? this.name,
email: email ?? this.email,
age: age ?? this.age,
);
}
}
final data = [
User(id: '1', name: 'John', email: 'john@example.com', age: 28),
User(id: '2', name: 'Jane', email: 'jane@example.com', age: 34),
];Before (v1.x):
final columns = {
'name': TablePlusColumn(
key: 'name',
label: 'Name',
order: 1,
width: 150,
sortable: true,
),
'email': TablePlusColumn(
key: 'email',
label: 'Email',
order: 2,
width: 200,
),
};After (v2.x):
final columns = {
'name': TablePlusColumn<User>(
key: 'name',
label: 'Name',
order: 1,
width: 150,
valueAccessor: (user) => user.name, // NEW: Extract value from model
sortable: true,
),
'email': TablePlusColumn<User>(
key: 'email',
label: 'Email',
order: 2,
width: 200,
valueAccessor: (user) => user.email, // NEW
),
};
// Or use TableColumnsBuilder
final builder = TableColumnsBuilder<User>()
..addColumn('name', TablePlusColumn(
key: 'name',
label: 'Name',
order: 1,
valueAccessor: (user) => user.name,
))
..addColumn('email', TablePlusColumn(
key: 'email',
label: 'Email',
order: 2,
valueAccessor: (user) => user.email,
));
final columns = builder.build();Before (v1.x):
FlutterTablePlus(
columns: columns,
data: data,
rowIdKey: 'id', // String-based key
)After (v2.x):
FlutterTablePlus<User>( // Add generic type
columns: columns,
data: data,
rowId: (user) => user.id, // Function-based accessor
)rowId is called on demand and is not compared across rebuilds, so data
and rowId are read as one snapshot: changing the id space means passing a
new data list, the same way any other data change is signalled. See the
doc-comment on FlutterTablePlus.rowId.
Before (v1.x):
FlutterTablePlus(
dimRowKey: 'isActive',
invertDimRow: true, // Dim when isActive is false
)After (v2.x):
FlutterTablePlus<User>(
isDimRow: (user) => !user.isActive, // Direct boolean function
)Before (v1.x):
onCellChanged: (String rowId, String columnKey, dynamic newValue) {
final index = data.indexWhere((row) => row['id'] == rowId);
if (index != -1) {
setState(() {
data[index][columnKey] = newValue;
});
}
}After (v2.x):
onCellChanged: (User row, String columnKey, int rowIndex, dynamic oldValue, dynamic newValue) {
setState(() {
// A new list: `data` is invalidated on the list's identity, so an
// in-place `data[rowIndex] = ...` renders the new value and keeps the
// cached row height.
switch (columnKey) {
case 'name':
data = List.of(data)..[rowIndex] = row.copyWith(name: newValue as String);
break;
case 'email':
data = List.of(data)..[rowIndex] = row.copyWith(email: newValue as String);
break;
case 'age':
data = List.of(data)..[rowIndex] = row.copyWith(age: int.parse(newValue.toString()));
break;
}
});
}Before (v1.x):
TablePlusColumn(
key: 'salary',
label: 'Salary',
cellBuilder: (context, rowData) {
return Text('\$${rowData['salary']}');
},
)After (v2.x):
TablePlusColumn<User>(
key: 'salary',
label: 'Salary',
valueAccessor: (user) => user.salary,
statefulCellBuilder: (context, user, isSelected, isDim) {
return Text('\$${user.salary}');
},
)Note:
cellBuilderwas removed in v2.5.0. UsestatefulCellBuilderwhich provides additionalisSelectedandisDimparameters.
Before (v1.x):
TablePlusColumn(
tooltipFormatter: (rowData) => 'Name: ${rowData['name']}',
tooltipBuilder: (context, rowData) => Text(rowData['name']),
)After (v2.x):
TablePlusColumn<User>(
tooltipFormatter: (user) => 'Name: ${user.name}', // Typed
tooltipBuilder: (context, user) => Text(user.name), // Typed
)Before (v1.x):
MergedRowGroup(
groupId: 'group-1',
rowKeys: ['1', '2', '3'],
mergeConfig: {...},
)After (v2.x):
MergedRowGroup<User>( // Add generic type
groupId: 'group-1',
rowKeys: ['1', '2', '3'],
mergeConfig: {...},
)Before (v1.x):
hoverButtonBuilder: (context, rowData) {
return IconButton(
icon: Icon(Icons.edit),
onPressed: () => editRow(rowData['id']),
);
}After (v2.x):
hoverButtonBuilder: (String rowId, User user) { // New signature
return IconButton(
icon: Icon(Icons.edit),
onPressed: () => editRow(user),
);
}Before (v1.x):
calculateRowHeight: (int rowIndex, Map<String, dynamic> rowData) {
return rowData['description'].length > 100 ? 80.0 : null;
}After (v2.x):
calculateRowHeight: (int rowIndex, User user) {
return user.description.length > 100 ? 80.0 : null;
}// Selection
onRowSelectionChanged: (String rowId, bool isSelected) // Same
onCheckboxChanged: (String rowId, bool isSelected) // Same
onSelectAll: (bool selectAll) // Same
// Sorting
onSort: (String columnKey, SortDirection direction) // Same
// Editing
onCellChanged: (String rowId, String columnKey, dynamic newValue)
↓
onCellChanged: (T row, String columnKey, int rowIndex, dynamic oldValue, dynamic newValue)
// Merged Rows
onMergedCellChanged: (String groupId, String columnKey, dynamic newValue) // Same
// Context Menu
onRowSecondaryTapDown: (String rowId, TapDownDetails, RenderBox, bool isSelected) // Same
// Double Tap
onRowDoubleTap: (String rowId) // Same
// Drag Selection (new in v2.2.0)
enableDragSelection: bool
onDragSelectionUpdate: (Set<String> draggedRowIds)
onDragSelectionEnd: (Set<String> draggedRowIds)
// Column Resizing (new in v2.1.0)
resizable: bool
onColumnResized: (String columnKey, double newWidth)
// Hover Buttons
hoverButtonBuilder: (BuildContext context, Map<String, dynamic> rowData)
↓
hoverButtonBuilder: (String rowId, T rowData)
// Row Height
calculateRowHeight: (int rowIndex, Map<String, dynamic> rowData)
↓
calculateRowHeight: (int rowIndex, T rowData)- Type Safety: Catch errors at compile time instead of runtime
- Autocompletion: IDE suggests available properties
- Refactoring: Rename properties across your codebase safely
- Documentation: Types serve as documentation
- Performance: No string lookups at runtime
If you encounter issues during migration, please open an issue on GitHub.