Building Production Flutter Data Tables with Voo Data Grid
Flutter's built-in DataTable widget falls short when you need real data grid functionality. No remote pagination. Limited sorting options. Poor performance with large datasets. Voo Data Grid fills these gaps with a widget designed for actual production workloads.
We built Voo Data Grid while working on AgileStack dashboards that needed to display thousands of project records with real-time filtering. The existing Flutter table solutions couldn't handle our performance requirements or complex data interactions.
What Problem Does This Actually Solve?
Most Flutter apps eventually need to display tabular data. User lists, transaction histories, inventory management, analytics dashboards. Flutter's DataTable works for simple cases, but breaks down when you need:
- Server-side pagination with 10,000+ records
- Multiple column sorting with type-aware comparisons
- Real-time filtering that doesn't block the UI
- Custom cell renderers for complex data types
- Responsive layouts that work on mobile and desktop
Voo Data Grid handles these scenarios without forcing you into a web view or platform-specific implementation.
When to use it: You're building business apps, admin panels, or data-heavy interfaces where users need to browse, sort, and filter large datasets.
When to skip it: Simple tables with < 100 rows, read-only data displays, or apps where bundle size matters more than functionality.
Find the package at: https://pub.dev/packages/voo_data_grid
Quick Start: Get Running in Minutes
Add the dependency:
flutter pub add voo_data_grid
Here's a minimal working example with local data:
import 'package:flutter/material.dart';
import 'package:voo_data_grid/voo_data_grid.dart';
class UserGrid extends StatefulWidget {
@override
_UserGridState createState() => _UserGridState();
}
class _UserGridState extends State<UserGrid> {
final List<User> users = [
User(id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin'),
User(id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User'),
// ... more users
];
@override
Widget build(BuildContext context) {
return VooDataGrid<User>(
columns: [
DataGridColumn<User>(
field: 'name',
label: 'Name',
sortable: true,
getValue: (user) => user.name,
),
DataGridColumn<User>(
field: 'email',
label: 'Email',
sortable: true,
getValue: (user) => user.email,
),
DataGridColumn<User>(
field: 'role',
label: 'Role',
sortable: true,
getValue: (user) => user.role,
),
],
dataSource: users,
pageSize: 20,
showPagination: true,
);
}
}
This creates a sortable, paginated table in about 30 lines of code. Click column headers to sort, use the pagination controls to browse pages.
Core Concepts and Mental Model
Voo Data Grid separates concerns into three main pieces:
Columns: Define what data to show and how to display it. Each column specifies a field name, display label, and getter function. Columns handle sorting logic, custom renderers, and responsive behavior.
Data Sources: Provide the actual data, either as a local list or remote API calls. The grid doesn't care where data comes from, it just needs a consistent interface.
State Management: Tracks current page, sort order, filters, and selection state. You can hook into state changes to sync with URL parameters or save user preferences.
The mental model: Think of it as a view layer over your data. The grid handles UI interactions (clicks, scrolling, input) and translates them into data operations (sort, filter, paginate). Your job is defining the columns and providing the data.
Realistic Usage: Remote Data with API Integration
Here's how you'd implement server-side pagination with a REST API:
class ApiUserDataSource extends DataGridDataSource<User> {
final ApiService apiService;
ApiUserDataSource(this.apiService);
@override
Future<DataGridResult<User>> loadData(DataGridRequest request) async {
try {
final response = await apiService.getUsers(
page: request.page,
pageSize: request.pageSize,
sortBy: request.sortField,
sortOrder: request.sortDirection,
filters: request.filters,
);
return DataGridResult<User>(
data: response.users,
totalCount: response.totalCount,
hasMore: response.hasNextPage,
);
} catch (error) {
return DataGridResult<User>.error(error.toString());
}
}
}
class UserManagementPage extends StatefulWidget {
@override
_UserManagementPageState createState() => _UserManagementPageState();
}
class _UserManagementPageState extends State<UserManagementPage> {
late ApiUserDataSource dataSource;
@override
void initState() {
super.initState();
dataSource = ApiUserDataSource(ApiService());
}
@override
Widget build(BuildContext context) {
return VooDataGrid<User>(
columns: [
DataGridColumn<User>(
field: 'name',
label: 'Name',
sortable: true,
getValue: (user) => user.name,
width: 200,
),
DataGridColumn<User>(
field: 'email',
label: 'Email',
sortable: true,
getValue: (user) => user.email,
width: 250,
),
DataGridColumn<User>(
field: 'lastActive',
label: 'Last Active',
sortable: true,
getValue: (user) => user.lastActive,
cellBuilder: (context, user) {
return Text(
timeago.format(user.lastActive),
style: TextStyle(
color: user.lastActive.isBefore(
DateTime.now().subtract(Duration(days: 30))
) ? Colors.red : Colors.green,
),
);
},
),
],
dataSource: dataSource,
pageSize: 25,
showPagination: true,
showFilters: true,
loadingIndicator: CircularProgressIndicator(),
onRowTap: (user) => _editUser(user),
);
}
void _editUser(User user) {
// Navigate to edit page
}
}
This example shows several production patterns:
- Custom data source that talks to your API
- Error handling for network failures
- Custom cell renderers for formatted dates
- Row tap handling for navigation
- Loading states during API calls
Advanced Usage: Real-Time Updates with WebSockets
For dashboards that need live data updates, you can integrate WebSocket streams:
class LiveOrderDataSource extends DataGridDataSource<Order> {
final OrderService orderService;
late StreamSubscription _subscription;
LiveOrderDataSource(this.orderService) {
_subscription = orderService.orderUpdates.listen((update) {
// Update specific rows without full reload
notifyRowChanged(update.orderId, update.order);
});
}
@override
Future<DataGridResult<Order>> loadData(DataGridRequest request) async {
final orders = await orderService.getOrders(
page: request.page,
pageSize: request.pageSize,
);
return DataGridResult<Order>(
data: orders.items,
totalCount: orders.total,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
The grid automatically updates individual rows when data changes, avoiding expensive full reloads.
Performance Characteristics and Scale Tradeoffs
Voo Data Grid performs well up to certain limits, but you'll hit constraints:
Client-side data: Works smoothly with 1,000-5,000 rows depending on column complexity. Beyond that, scrolling gets janky and memory usage grows. Use server-side pagination for larger datasets.
Server-side pagination: Handles millions of rows since only the current page stays in memory. API response time becomes the bottleneck, not widget performance.
Column rendering: Simple text columns are fast. Custom cell widgets with images, buttons, or complex layouts slow things down. Budget 2-3 complex columns max per grid.
Sorting and filtering: Client-side operations are instant but limited to current page data. Server-side operations are slower but work across all data.
Mobile vs desktop: Touch scrolling works well on phones, but small screens limit useful column count. Consider hiding less important columns on mobile breakpoints.
In our AgileStack dashboards, we keep page sizes at 50 rows and preload the next page in background for smooth pagination.
Common Gotchas and Integration Tips
Version pinning: Lock to a specific minor version like voo_data_grid: ^0.10.4 to avoid breaking changes. The API is still evolving pre-1.0.
State management conflicts: Don't wrap the grid in unnecessary state management. It handles its own state internally. Use callbacks to sync external state when needed.
Column width calculation: Auto-sizing columns can cause layout thrashing on data updates. Set explicit widths for stable layouts:
DataGridColumn<User>(
field: 'email',
label: 'Email',
width: 200, // Explicit width prevents layout shifts
minWidth: 150,
maxWidth: 300,
)
Memory leaks with custom data sources: Always implement dispose() properly:
@override
void dispose() {
dataSource.dispose(); // Cancel streams, close connections
super.dispose();
}
Responsive breakpoints: The grid doesn't automatically hide columns on small screens. Handle this yourself:
columns: [
// Always show essential columns
nameColumn,
emailColumn,
// Hide optional columns on mobile
if (MediaQuery.of(context).size.width > 600)
lastActiveColumn,
if (MediaQuery.of(context).size.width > 800)
statisticsColumn,
]
Where It Shines and Where It Doesn't
Strengths:
- Server-side pagination that actually works
- Type-safe column definitions with good IntelliSense
- Reasonable performance with large datasets
- Customizable without being overwhelming
- Works well in business app contexts
Weaknesses:
- Limited mobile optimization out of the box
- No built-in export functionality (CSV, PDF)
- Documentation could be more comprehensive
- Smaller community compared to alternatives
- Some advanced features require custom implementation
Alternatives to consider:
pluto_grid: More features but heavier and complexsyncfusion_flutter_datagrid: Enterprise features but expensive licensingflutter_data_table: Lighter weight but fewer features- Custom DataTable: Maximum control but significant development time
For most business applications that need solid data grid functionality without enterprise licensing costs, Voo Data Grid hits the sweet spot.
What This Means for Your Next Project
Voo Data Grid solves real problems that most Flutter data table solutions ignore. Server-side pagination, custom cell rendering, and reasonable performance at scale. It's not perfect, but it's production-ready for business applications.
Start with the basic implementation, then add remote data sources and custom columns as needed. Keep page sizes reasonable, handle errors gracefully, and test on mobile devices early.
The package is actively maintained by our team at VooStack, so expect continued improvements and bug fixes as we use it in our own client projects.
Want to dig deeper into voo_data_grid? Check out the package page for full docs and live stats. Need help integrating it into your stack? AgileStack helps teams adopt the right tools without the consulting-firm overhead. Book a 30-minute call.