VooDataGrid
A powerful and flexible data grid widget for Flutter with advanced features like sorting, filtering, pagination, and remote data support.
Features
- State Management Agnostic: Works with any state management solution (Cubit, BLoC, Provider, Riverpod, GetX, etc.)
- Type-Safe Generic Support: Full compile-time type safety with generic type parameters
- Flexible Data Display: Display tabular data with customizable columns and rows
- Sorting: Built-in column sorting with custom comparators
- Advanced Filtering:
- Multiple filter types (string, int, date, decimal)
- Secondary filters with AND/OR logic
- Complex compound filters
- Legacy filter format support
- Built-in filter UI widget
- Filters remain visible during error states
- Pagination: Server-side and client-side pagination support
- Selection: Row selection with single and multi-select modes
- Remote Data: Built-in support for fetching data from REST APIs
- Synchronized Scrolling: Uniform horizontal scrolling between header and body
- Performance: Optimized for large datasets with efficient rendering
- API Standards: Support for 7 API standards (Voo, Simple REST, JSON:API, OData, MongoDB, GraphQL, Custom)
- Field Prefix Support: Automatic field name prefixing for nested properties
- Action Columns: Support for clickable cells and action buttons
- Enhanced Error Handling: Comprehensive error handling with graceful fallbacks
- Customization: Highly customizable headers, cells, and styling
- Responsive: Adapts to different screen sizes
- Empty State: Headers and filters remain visible with empty data for better UX
Installation
Add this to your package's pubspec.yaml
file:
dependencies:
voo_data_grid: ^0.6.0
voo_ui_core: ^0.1.0
Then run:
flutter pub get
Usage
Three Ways to Use VooDataGrid
VooDataGrid offers three different approaches to suit your state management needs:
VooDataGrid
- Traditional controller-based widget (works with Provider/ChangeNotifier)StatelessVooDataGrid
- Completely state-agnostic widget (works with ANY state management)VooDataGridStateController
- Provider-compatible controller for existing users
Using StatelessVooDataGrid (NEW - Recommended for Cubit/BLoC/Riverpod)
The new StatelessVooDataGrid
widget accepts state and callbacks directly, making it perfect for use with any state management solution:
// With Cubit
BlocBuilder<OrderGridCubit, VooDataGridState<OrderList>>(
builder: (context, state) {
return StatelessVooDataGrid<OrderList>(
state: state,
columns: columns,
onPageChanged: (page) => context.read<OrderGridCubit>().changePage(page),
onFilterChanged: (field, filter) =>
context.read<OrderGridCubit>().applyFilter(field, filter),
onSortChanged: (field, direction) =>
context.read<OrderGridCubit>().applySort(field, direction),
onRowTap: (order) => _showOrderDetails(order),
);
},
)
// With Riverpod
Consumer(
builder: (context, ref, child) {
final state = ref.watch(orderGridProvider);
return StatelessVooDataGrid<OrderList>(
state: state,
columns: columns,
onPageChanged: (page) =>
ref.read(orderGridProvider.notifier).changePage(page),
onFilterChanged: (field, filter) =>
ref.read(orderGridProvider.notifier).applyFilter(field, filter),
// ... other callbacks
);
},
)
Basic Example (Traditional Controller)
import 'package:flutter/material.dart';
import 'package:voo_data_grid/voo_data_grid.dart';
import 'package:voo_ui_core/voo_ui_core.dart';
class DataGridExample extends StatefulWidget {
@override
State<DataGridExample> createState() => _DataGridExampleState();
}
class _DataGridExampleState extends State<DataGridExample> {
late VooDataGridController controller;
@override
void initState() {
super.initState();
controller = VooDataGridController(
dataSource: VooLocalDataSource(data: _generateData()),
columns: _buildColumns(),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
List<VooDataGridColumn> _buildColumns() {
return [
VooDataGridColumn(
key: 'id',
label: 'ID',
width: 80,
getValue: (item) => item['id'].toString(),
),
VooDataGridColumn(
key: 'name',
label: 'Name',
getValue: (item) => item['name'],
sortable: true,
),
VooDataGridColumn(
key: 'email',
label: 'Email',
getValue: (item) => item['email'],
sortable: true,
filterable: true,
),
VooDataGridColumn(
key: 'status',
label: 'Status',
getValue: (item) => item['status'],
cellBuilder: (context, item, column) {
final status = column.getValue(item);
final color = status == 'Active' ? Colors.green : Colors.orange;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(status),
);
},
),
];
}
List<Map<String, dynamic>> _generateData() {
return List.generate(100, (index) => {
'id': index + 1,
'name': 'User ${index + 1}',
'email': 'user${index + 1}@example.com',
'status': index % 3 == 0 ? 'Inactive' : 'Active',
});
}
@override
Widget build(BuildContext context) {
return VooDesignSystem(
data: VooDesignSystemData.defaultSystem,
child: Scaffold(
appBar: AppBar(title: const Text('Data Grid Example')),
body: VooDataGrid(
controller: controller,
onRowTap: (item) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped: ${item['name']}')),
);
},
),
),
);
}
}
Using Typed Objects (Generic Support)
VooDataGrid fully supports typed objects with compile-time type safety:
// Define your data model
class OrderModel {
final int id;
final String orderNumber;
final String customerName;
final double amount;
final FileStatus? status;
OrderModel({
required this.id,
required this.orderNumber,
required this.customerName,
required this.amount,
this.status,
});
}
class FileStatus {
final String code;
final String name;
final Color color;
FileStatus({required this.code, required this.name, required this.color});
}
// Create strongly typed controller
class TypedDataExample extends StatefulWidget {
@override
State<TypedDataExample> createState() => _TypedDataExampleState();
}
class _TypedDataExampleState extends State<TypedDataExample> {
late VooDataGridController<OrderModel> controller;
@override
void initState() {
super.initState();
// Create typed data source
final dataSource = TypedDataSource();
dataSource.setLocalData([
OrderModel(
id: 1,
orderNumber: 'ORD-001',
customerName: 'John Doe',
amount: 1500.00,
status: FileStatus(code: 'ACTIVE', name: 'Active', color: Colors.green),
),
// ... more data
]);
controller = VooDataGridController<OrderModel>(
dataSource: dataSource,
);
// Define columns with type-safe valueGetter
controller.setColumns([
VooDataColumn<OrderModel>(
field: 'orderNumber',
label: 'Order #',
// No casting needed - row is already typed as OrderModel
valueGetter: (row) => row.orderNumber,
),
VooDataColumn<OrderModel>(
field: 'customerName',
label: 'Customer',
valueGetter: (row) => row.customerName,
),
VooDataColumn<OrderModel>(
field: 'amount',
label: 'Amount',
valueGetter: (row) => row.amount,
valueFormatter: (value) => '\$${value.toStringAsFixed(2)}',
),
VooDataColumn<OrderModel>(
field: 'status',
label: 'Status',
// Handle nullable nested properties safely
valueGetter: (row) => row.status?.name,
cellBuilder: (context, value, row) {
final status = row.status;
if (status == null) return const Text('N/A');
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: status.color.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
status.name,
style: TextStyle(color: status.color),
),
);
},
),
]);
}
@override
Widget build(BuildContext context) {
return VooDataGrid<OrderModel>(
controller: controller,
onRowTap: (OrderModel order) {
// Type-safe row access
print('Order ${order.orderNumber} tapped');
},
);
}
}
// Custom data source for typed objects
class TypedDataSource extends VooDataGridSource<OrderModel> {
TypedDataSource() : super(mode: VooDataGridMode.local);
@override
Future<VooDataGridResponse<OrderModel>> fetchRemoteData({
required int page,
required int pageSize,
required Map<String, VooDataFilter> filters,
required List<VooColumnSort> sorts,
}) async {
// Implement remote fetching if needed
return VooDataGridResponse<OrderModel>(
rows: [],
totalRows: 0,
page: page,
pageSize: pageSize,
);
}
}
Important Notes for Typed Objects:
- You MUST provide a
valueGetter
function for each column when using typed objects - The grid cannot use reflection to access properties dynamically
- All callbacks (onRowTap, cellBuilder, etc.) receive properly typed data
- Map objects work without valueGetter (backward compatibility)
Remote Data Example
class RemoteDataExample extends StatefulWidget {
@override
State<RemoteDataExample> createState() => _RemoteDataExampleState();
}
class _RemoteDataExampleState extends State<RemoteDataExample> {
late VooDataGridController controller;
@override
void initState() {
super.initState();
controller = VooDataGridController(
dataSource: RemoteDataGridSource(
apiEndpoint: 'https://api.example.com/users',
apiStandard: ApiFilterStandard.jsonApi,
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
},
httpClient: (url, requestData, headers) async {
// Use your preferred HTTP client (dio, http, etc.)
final response = await dio.get(url,
queryParameters: requestData['params'],
options: Options(headers: headers),
);
return response.data;
},
),
columns: _buildColumns(),
);
}
// ... rest of the implementation
}
With Pagination
VooDataGrid(
controller: VooDataGridController(
dataSource: dataSource,
columns: columns,
paginationConfig: VooPaginationConfig(
enabled: true,
pageSize: 20,
pageSizeOptions: [10, 20, 50, 100],
),
),
)
With Filtering
List<VooDataGridColumn> _buildColumns() {
return [
VooDataGridColumn(
key: 'name',
label: 'Name',
getValue: (item) => item['name'],
filterable: true,
filterType: FilterType.text,
),
VooDataGridColumn(
key: 'age',
label: 'Age',
getValue: (item) => item['age'].toString(),
filterable: true,
filterType: FilterType.number,
),
VooDataGridColumn(
key: 'created_at',
label: 'Created',
getValue: (item) => item['created_at'],
filterable: true,
filterType: FilterType.date,
),
VooDataGridColumn(
key: 'status',
label: 'Status',
getValue: (item) => item['status'],
filterable: true,
filterType: FilterType.select,
filterOptions: ['Active', 'Inactive', 'Pending'],
),
];
}
With Row Selection
VooDataGrid(
controller: VooDataGridController(
dataSource: dataSource,
columns: columns,
selectionMode: SelectionMode.multiple,
),
onSelectionChanged: (selectedItems) {
print('Selected ${selectedItems.length} items');
},
)
Custom Cell Rendering
VooDataGridColumn(
key: 'avatar',
label: 'Avatar',
getValue: (item) => item['avatar_url'],
cellBuilder: (context, item, column) {
final url = column.getValue(item);
return CircleAvatar(
backgroundImage: NetworkImage(url),
radius: 16,
);
},
)
Custom Header
VooDataGridColumn(
key: 'actions',
label: 'Actions',
headerBuilder: (context, column) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.settings, size: 16),
const SizedBox(width: 4),
Text(column.label),
],
);
},
cellBuilder: (context, item, column) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.edit, size: 16),
onPressed: () => _editItem(item),
),
IconButton(
icon: const Icon(Icons.delete, size: 16),
onPressed: () => _deleteItem(item),
),
],
);
},
)
Advanced Filtering
Understanding the Filters Map Structure
The filters in VooDataGrid are stored as a Map<String, VooDataFilter>
where:
- Key: The field name (e.g., 'name', 'status', 'price')
- Value: A
VooDataFilter
object containing the operator and value
// Example of what the filters map looks like internally:
Map<String, VooDataFilter> filters = {
'name': VooDataFilter(
operator: VooFilterOperator.contains,
value: 'John',
),
'status': VooDataFilter(
operator: VooFilterOperator.equals,
value: 'active',
),
'price': VooDataFilter(
operator: VooFilterOperator.greaterThan,
value: 100,
),
};
Applying Filters Programmatically
You can apply filters directly to the data source:
// Apply a single filter
controller.dataSource.applyFilter(
'status',
VooDataFilter(
operator: VooFilterOperator.equals,
value: 'active',
),
);
// Apply multiple filters at once
controller.dataSource.applyFilters({
'name': VooDataFilter(
operator: VooFilterOperator.startsWith,
value: 'A',
),
'age': VooDataFilter(
operator: VooFilterOperator.greaterThanOrEqual,
value: 18,
),
});
// Clear a specific filter
controller.dataSource.clearFilter('name');
// Clear all filters
controller.dataSource.clearAllFilters();
Using Primary Filters
Primary filters are pre-configured filters that users can quickly apply. They work with the same filter map structure:
VooDataGrid(
controller: controller,
showPrimaryFilters: true,
primaryFilters: [
PrimaryFilter(
label: 'Active Only',
field: 'status',
filter: VooDataFilter(
operator: VooFilterOperator.equals,
value: 'active',
),
),
PrimaryFilter(
label: 'High Priority',
field: 'priority',
filter: VooDataFilter(
operator: VooFilterOperator.greaterThanOrEqual,
value: 8,
),
),
PrimaryFilter(
label: 'Recent Items',
field: 'createdAt',
filter: VooDataFilter(
operator: VooFilterOperator.greaterThan,
value: DateTime.now().subtract(Duration(days: 7)),
),
),
],
onFilterChanged: (field, filter) {
// This callback is triggered when any filter changes
// field: The field name (e.g., 'status')
// filter: The VooDataFilter object or null if clearing
print('Filter changed: $field = ${filter?.value}');
},
);
Available Filter Operators
enum VooFilterOperator {
equals, // Exact match
notEquals, // Not equal to
contains, // String contains
notContains, // String does not contain
startsWith, // String starts with
endsWith, // String ends with
greaterThan, // Greater than (numbers/dates)
greaterThanOrEqual, // Greater than or equal
lessThan, // Less than (numbers/dates)
lessThanOrEqual, // Less than or equal
between, // Between two values (requires List value)
inList, // Value in list
notInList, // Value not in list
isEmpty, // Field is empty/null
isNotEmpty, // Field has value
}
Complex Filter with Secondary Conditions
// Using AdvancedRemoteDataSource for complex filtering
final dataSource = AdvancedRemoteDataSource(
apiEndpoint: '/api/orders',
httpClient: httpClient,
useAdvancedFilters: true,
);
// Apply complex filter programmatically
final filterRequest = AdvancedFilterRequest(
stringFilters: [
StringFilter(
fieldName: 'Site.Name',
value: 'Tech',
operator: 'Contains',
secondaryFilter: SecondaryFilter(
logic: FilterLogic.and,
value: 'Park',
operator: 'NotContains',
),
),
],
intFilters: [
IntFilter(
fieldName: 'Site.SiteNumber',
value: 1006,
operator: 'GreaterThan',
secondaryFilter: SecondaryFilter(
logic: FilterLogic.and,
value: 1011,
operator: 'LessThan',
),
),
],
logic: FilterLogic.and,
pageNumber: 1,
pageSize: 20,
);
dataSource.setAdvancedFilterRequest(filterRequest);
Using Advanced Filter Widget
AdvancedFilterWidget(
dataSource: dataSource,
fields: [
FilterFieldConfig(
fieldName: 'Site.SiteNumber',
displayName: 'Site Number',
type: FilterType.int,
),
FilterFieldConfig(
fieldName: 'Client.CompanyName',
displayName: 'Client',
type: FilterType.string,
),
FilterFieldConfig(
fieldName: 'OrderDate',
displayName: 'Order Date',
type: FilterType.date,
),
FilterFieldConfig(
fieldName: 'OrderCost',
displayName: 'Cost',
type: FilterType.decimal,
),
],
onFilterApplied: (request) {
print('Applied filters: ${request.toJson()}');
},
)
State Management Compatibility
VooDataGrid now supports any state management solution through a flexible, state-agnostic architecture. Whether you use Cubit, BLoC, Provider, Riverpod, GetX, or any other state management, VooDataGrid integrates seamlessly.
New Architecture (v0.5.9+)
The package provides three key components for state management flexibility:
VooDataGridDataSource<T>
- Clean interface for data fetching without forcing inheritanceVooDataGridState<T>
- Immutable state class with copyWith methodVooDataGridStateController<T>
- ChangeNotifier-based controller for Provider users
Using with Cubit/BLoC
// Repository for data fetching
class OrderRepository extends VooDataGridDataSource<Order> {
@override
Future<VooDataGridResponse<Order>> fetchRemoteData({
required int page,
required int pageSize,
required Map<String, VooDataFilter> filters,
required List<VooColumnSort> sorts,
}) async {
// Fetch from your API
final response = await api.getOrders(page, pageSize, filters, sorts);
return VooDataGridResponse<Order>(
rows: response.items,
totalRows: response.total,
page: page,
pageSize: pageSize,
);
}
}
// Cubit for state management
class OrderGridCubit extends Cubit<VooDataGridState<Order>> {
final OrderRepository repository;
OrderGridCubit({required this.repository})
: super(const VooDataGridState<Order>());
Future<void> loadData() async {
emit(state.copyWith(isLoading: true));
try {
final response = await repository.fetchRemoteData(
page: state.currentPage,
pageSize: state.pageSize,
filters: state.filters,
sorts: state.sorts,
);
emit(state.copyWith(
rows: response.rows,
totalRows: response.totalRows,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(error: e.toString(), isLoading: false));
}
}
void changePage(int page) {
emit(state.copyWith(currentPage: page));
loadData();
}
}
Using with Provider
// Use the provided controller
final controller = VooDataGridStateController<Order>(
dataSource: OrderRepository(),
mode: VooDataGridMode.remote,
);
// In your widget
ChangeNotifierProvider(
create: (_) => controller..loadData(),
child: Consumer<VooDataGridStateController<Order>>(
builder: (context, controller, _) {
return VooDataGrid<Order>(
state: controller.state,
columns: [...],
onPageChanged: controller.changePage,
onFilterChanged: controller.applyFilter,
);
},
),
)
Backward Compatibility
The original VooDataGridSource
with ChangeNotifier is still available and fully supported:
// Legacy approach still works
class MyDataSource extends VooDataGridSource<Map<String, dynamic>> {
MyDataSource() : super(mode: VooDataGridMode.local);
// ... implementation
}
For detailed migration examples with Riverpod, GetX, and other state management solutions, see MIGRATION_GUIDE.md.
API Standards Support
VooDataGrid includes integrated support for 7 different API standards through the DataGridRequestBuilder
class. Each data source can have its own HTTP client with custom interceptors and authentication.
Voo API Standard
The Voo API Standard is designed for enterprise applications with typed filters:
final dataSource = RemoteDataGridSource(
apiEndpoint: '/api/data',
apiStandard: ApiFilterStandard.voo,
fieldPrefix: 'Site', // Optional: prefix for nested properties
httpClient: (url, requestData, headers) async {
final response = await dio.post(url, data: requestData);
return response.data;
},
);
// Example request format for Voo API Standard:
{
"pageNumber": 1,
"pageSize": 20,
"logic": "And",
"intFilters": [
{
"fieldName": "Site.siteNumber",
"value": 100,
"operator": "GreaterThanOrEqual"
},
{
"fieldName": "Site.siteNumber",
"value": 200,
"operator": "LessThanOrEqual"
}
],
"stringFilters": [
{
"fieldName": "Site.name",
"value": "Tech",
"operator": "Contains"
}
]
}
Important Notes for Voo API Standard:
- Number ranges are sent as two separate filters (GreaterThanOrEqual and LessThanOrEqual)
- The "Between" operator is not supported - it's automatically converted to two filters
- Filters are organized by type: stringFilters, intFilters, dateFilters, decimalFilters
- Field prefix is automatically applied to all field names when set
Simple REST Standard
final dataSource = RemoteDataGridSource(
apiEndpoint: 'https://api.example.com/products',
apiStandard: ApiFilterStandard.simple,
httpClient: (url, requestData, headers) async {
// Generates: ?page=0&limit=20&status=active&age_gt=25&sort=-created_at
final params = requestData['params'] as Map<String, String>;
final response = await dio.get(url, queryParameters: params);
return response.data;
},
);
JSON:API Standard
final dataSource = RemoteDataGridSource(
apiEndpoint: 'https://api.example.com/products',
apiStandard: ApiFilterStandard.jsonApi,
httpClient: (url, requestData, headers) async {
// Generates: ?page[number]=1&page[size]=20&filter[status]=active&sort=-created_at
final params = requestData['params'] as Map<String, String>;
final response = await dio.get(url, queryParameters: params);
return response.data;
},
);
OData Standard
final dataSource = RemoteDataGridSource(
apiEndpoint: 'https://api.example.com/odata/products',
apiStandard: ApiFilterStandard.odata,
httpClient: (url, requestData, headers) async {
// Generates: ?$top=20&$skip=0&$filter=status eq 'active'&$orderby=name asc
final params = requestData['params'] as Map<String, String>;
final response = await dio.get(url, queryParameters: params);
return response.data;
},
);
MongoDB/Elasticsearch Standard
final dataSource = RemoteDataGridSource(
apiEndpoint: 'https://api.example.com/search',
apiStandard: ApiFilterStandard.mongodb,
httpClient: (url, requestData, headers) async {
// Sends POST body with MongoDB-style query
final body = requestData['body'];
final response = await dio.post(url, data: body);
return response.data;
},
);
GraphQL Standard
final dataSource = RemoteDataGridSource(
apiEndpoint: 'https://api.example.com/graphql',
apiStandard: ApiFilterStandard.graphql,
httpClient: (url, requestData, headers) async {
// Sends GraphQL query with variables
final response = await dio.post(url, data: {
'query': requestData['query'],
'variables': requestData['variables'],
});
return response.data;
},
);
Custom Standard (Default)
final dataSource = RemoteDataGridSource(
apiEndpoint: 'https://api.example.com/data',
apiStandard: ApiFilterStandard.custom,
httpClient: (url, requestData, headers) async {
// Uses VooDataGrid's default format
final response = await dio.post(url, data: requestData);
return response.data;
},
);
Response Format
{
"data": [...],
"current_page": 1,
"total": 100,
"per_page": 20,
"last_page": 5
}
Spring Boot Format
{
"content": [...],
"pageable": {
"pageNumber": 0,
"pageSize": 20
},
"totalElements": 100,
"totalPages": 5
}
Custom Format
VooRemoteDataSource(
url: 'https://api.example.com/data',
apiStandard: ApiStandard.custom,
responseParser: (response) {
return DataGridResponse(
data: response['items'],
total: response['count'],
page: response['page'],
pageSize: response['limit'],
);
},
)
Controller API
final controller = VooDataGridController(...);
// Refresh data
await controller.refresh();
// Navigate pages
controller.nextPage();
controller.previousPage();
controller.goToPage(3);
// Change page size
controller.setPageSize(50);
// Sorting
controller.sortBy('name', ascending: true);
controller.clearSort();
// Filtering
controller.setFilter('name', 'John');
controller.setFilters({
'name': 'John',
'status': 'Active',
});
controller.clearFilter('name');
controller.clearAllFilters();
// Selection
controller.selectAll();
controller.clearSelection();
controller.toggleSelection(item);
final selected = controller.selectedItems;
// Get current state
final currentPage = controller.currentPage;
final totalPages = controller.totalPages;
final isLoading = controller.isLoading;
final hasError = controller.hasError;
Customization
Styling
VooDataGrid(
controller: controller,
headerStyle: const DataGridHeaderStyle(
backgroundColor: Colors.blue,
textStyle: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
height: 56,
),
rowStyle: DataGridRowStyle(
height: 48,
alternateColor: Colors.grey.shade50,
hoverColor: Colors.blue.shade50,
),
borderStyle: const DataGridBorderStyle(
horizontal: BorderSide(color: Colors.grey),
vertical: BorderSide(color: Colors.grey),
),
)
Empty State
VooDataGrid(
controller: controller,
emptyStateBuilder: (context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text('No data available'),
],
),
);
},
)
Loading State
VooDataGrid(
controller: controller,
loadingBuilder: (context) {
return const Center(
child: CircularProgressIndicator(),
);
},
)
Error State
VooDataGrid(
controller: controller,
errorBuilder: (context, error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text('Error: $error'),
ElevatedButton(
onPressed: controller.refresh,
child: const Text('Retry'),
),
],
),
);
},
)
Error Handling
VooDataGrid provides comprehensive error handling with graceful fallbacks:
Enhanced Error Recovery
- Filters remain visible during errors: Headers and filter row stay accessible even when data loading fails
- Type-safe error handling: Comprehensive try-catch blocks around valueGetter functions
- Detailed error logging: Debug mode provides detailed type information for troubleshooting
Handling Type Mismatches
// The grid gracefully handles type mismatches
VooDataColumn<OrderModel>(
field: 'status',
label: 'Status',
valueGetter: (row) {
// This is protected with error handling
return row.status?.name;
},
// Fallback for null or error cases
valueFormatter: (value) => value ?? 'N/A',
)
Debug Logging
In debug mode, detailed error information is logged:
[VooDataGrid Error] Failed to get value for column "status":
Error: type 'Null' is not a subtype of type 'String'
Row type: OrderModel
Column field: status
ValueGetter type: (OrderModel) => String?
Widget Previews\n\nVooDataGrid includes comprehensive preview widgets for testing and development:\n\n### Available Previews\n\n1. VooDataGridPreview - Large dataset demo with 200+ rows and 15+ columns\n2. VooDataGridApiStandardsPreview - Interactive API standards configuration tool\n3. VooDataGridEmptyStatePreview - Demonstrates empty state with persistent headers\n4. VooDataGridTypedObjectsPreview - Demonstrates using VooDataGrid with typed objects\n\n### Using Previews\n\ndart\nimport 'package:voo_data_grid/voo_data_grid.dart';\n\n// In your development/testing environment\nvoid main() {\n runApp(const VooDataGridApiStandardsPreview());\n}\n
\n\n### Interactive API Standards Preview\n\nThe API standards preview provides:\n- Live switching between all 6 API standards\n- Real-time request format viewer\n- Copy-paste ready code examples\n- Interactive filter and sort testing\n\n## Performance Tips
- Use pagination for large datasets
- Implement virtual scrolling for very large lists
- Use
const
constructors where possible - Optimize cell builders to be lightweight
- Cache remote data when appropriate
- Use proper keys for columns to optimize rebuilds
Migration Guide
From v0.5.1 to v0.5.2
Bug Fixes & Improvements:
- Fixed VooApiStandard number range filtering (now uses GreaterThanOrEqual/LessThanOrEqual instead of Between)
- Fixed filters disappearing when data loading fails
- Enhanced error handling for type mismatches in valueGetter functions
- Added comprehensive error logging in debug mode
From v0.5.0 to v0.5.1
New Features:
- Added field prefix support for nested properties
- Added action columns with
excludeFromApi
flag - Added
onCellTap
callback for clickable cells
From v0.4.0 to v0.5.0
Breaking Changes:
- All components now use generic type parameters for type safety
valueGetter
now has proper type signature:dynamic Function(T row)
- No casting needed in valueGetter functions anymore
- Compile-time type checking for all row data
From v0.2.0 to v0.3.0
Breaking Changes:
StandardApiRequestBuilder
renamed toDataGridRequestBuilder
- API standards now integrated directly into
DataGridRequestBuilder
- Preview files moved from
/preview
to/lib/preview
From voo_ui
If you're migrating from the monolithic voo_ui
package:
- Update your dependencies:
dependencies:
voo_data_grid: ^0.5.4
voo_ui_core: ^0.1.0 # Required for design system
- Update imports:
// Old
import 'package:voo_ui/voo_ui.dart';
// New
import 'package:voo_data_grid/voo_data_grid.dart';
import 'package:voo_ui_core/voo_ui_core.dart';
- The API has been improved but remains similar. Main changes:
- Controller is now required
- Column configuration is more flexible
- Better TypeScript support
Troubleshooting
Sorting not working with VooApiStandard
If sorting isn't being applied when clicking column headers with VooApiStandard:
- Ensure columns are sortable:
VooDataColumn<OrderList>(
field: 'siteNumber',
label: 'Site Number',
sortable: true, // <-- REQUIRED for sorting
valueGetter: (row) => row.siteNumber, // <-- REQUIRED for typed objects
),
- Verify API standard is set:
// In your data source:
final request = const DataGridRequestBuilder(
standard: ApiFilterStandard.voo, // <-- REQUIRED for VooApiStandard
fieldPrefix: 'Site', // Optional prefix
).buildRequest(
page: page + 1,
pageSize: pageSize,
filters: filters,
sorts: sorts, // <-- Will be converted to sortBy and sortDescending
);
- Check debug output:
// Add logging to your fetchRemoteData:
debugPrint('Sorts received: $sorts');
debugPrint('Request built: $request');
If sorts are empty, verify:
- Column has
sortable: true
- Column has
valueGetter
for typed objects - Controller is properly initialized with columns
See example/lib/voo_api_standard_example.dart
for a complete working example.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see the LICENSE file for details