import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/legacy.dart'; import 'data_grid_config.dart'; import 'data_grid_provider.dart'; import 'data_grid_state.dart'; import 'widgets/data_grid_bulk_actions.dart'; import 'widgets/data_grid_empty_state.dart'; import 'widgets/data_grid_footer.dart'; import 'widgets/data_grid_header.dart'; import 'widgets/data_grid_row.dart'; import 'widgets/data_grid_search_bar.dart'; /// A configurable data grid widget for displaying tabular data. /// /// Features: /// - Sortable columns /// - Row selection (single and bulk) /// - Per-row and bulk actions /// - Search/filtering /// - Pagination or infinite scroll /// - Custom cell rendering /// - Empty, loading, and error states /// /// Usage: /// ```dart /// DataGrid( /// provider: containersGridProvider, /// config: containersGridConfig, /// idSelector: (c) => c.id, /// ) /// ``` class DataGrid extends ConsumerWidget { const DataGrid({ super.key, required this.provider, required this.config, required this.idSelector, this.toolbarActions, }); /// The Riverpod provider for this grid's state. final StateNotifierProvider, DataGridState> provider; /// Grid configuration. final DataGridConfig config; /// Function to extract unique ID from an item. final Object Function(T item) idSelector; /// Additional actions to show in the toolbar (next to search). final List? toolbarActions; @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(provider); final controller = ref.read(provider.notifier); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Toolbar: search + bulk actions + custom actions if (config.enableSearch || (state.selectedCount > 0 && config.bulkActions.isNotEmpty) || toolbarActions != null) _buildToolbar(context, state, controller), // Header if (config.showHeader) DataGridHeader( config: config, sortColumnIndex: state.sortColumnIndex, sortDescending: state.sortDescending, onSort: controller.sortBy, showCheckbox: config.rowsSelectable, allSelected: state.allSelected, someSelected: state.someSelected, onSelectAll: controller.toggleSelectAll, ), // Content area Expanded( child: _buildContent(context, state, controller), ), // Footer if (config.showFooter) DataGridFooter( totalCount: state.totalCount, displayedCount: state.items.length, dataMode: config.dataMode, currentPage: state.currentPage, onPageChange: controller.goToPage, isLoading: state.isLoading, ), ], ); } Widget _buildToolbar( BuildContext context, DataGridState state, DataGridController controller, ) { final colorScheme = Theme.of(context).colorScheme; return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, border: Border( bottom: BorderSide(color: colorScheme.outlineVariant), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (config.enableSearch) DataGridSearchBar( onSearch: controller.search, onClear: controller.clearSearch, hintText: config.searchHint, initialValue: state.searchQuery, ), if (state.selectedCount > 0 && config.bulkActions.isNotEmpty) ...[ const SizedBox(width: 16), Expanded( child: DataGridBulkActions( selectedCount: state.selectedCount, bulkActions: config.bulkActions, onClearSelection: controller.clearSelection, getSelectedItems: controller.getSelectedItems, ), ), ] else ...[ const Spacer(), ], if (toolbarActions != null) ...toolbarActions!, ], ), ); } Widget _buildContent( BuildContext context, DataGridState state, DataGridController controller, ) { // Initial loading state if (state.isInitialLoad && state.isLoading) { return config.loadingBuilder?.call(context) ?? const DataGridLoadingState(); } // Error state if (state.hasError && state.items.isEmpty) { return config.errorBuilder?.call(context, state.error!, controller.refresh) ?? DataGridErrorState( error: state.error!, onRetry: controller.refresh, ); } // Empty state if (state.isEmpty) { return config.emptyStateBuilder?.call(context) ?? DataGridEmptyState( title: state.searchQuery.isNotEmpty ? 'No results found' : 'No items found', subtitle: state.searchQuery.isNotEmpty ? 'Try a different search term' : null, ); } // Data rows return _buildListView(state, controller); } Widget _buildListView( DataGridState state, DataGridController controller, ) { final isInfiniteScroll = config.dataMode is InfiniteDataMode; return NotificationListener( onNotification: (notification) { if (isInfiniteScroll && notification is ScrollEndNotification && notification.metrics.extentAfter < 200) { controller.loadMore(); } return false; }, child: ListView.builder( itemCount: state.items.length + (state.isLoading && !state.isInitialLoad ? 1 : 0), itemBuilder: (context, index) { // Loading indicator at bottom for infinite scroll if (index >= state.items.length) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } final item = state.items[index]; final isSelected = controller.isSelected(item); return DataGridRow( item: item, index: index, config: config, isSelected: isSelected, onSelect: () => controller.toggleSelection(item), onTap: config.onRowTap != null ? () => config.onRowTap!(item) : null, backgroundColor: config.rowColor?.call(context, item, index), ); }, ), ); } }