ultimate_grid 0.1.1 copy "ultimate_grid: ^0.1.1" to clipboard
ultimate_grid: ^0.1.1 copied to clipboard

Scalable, themable 2D data-grid for Flutter — 9-region freeze, canvas-paint body, merges, async data, selection, sort, filter, search.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:ultimate_grid/ultimate_grid.dart';

import 'screens/_themes.dart';
import 'screens/async_paging_screen.dart';
import 'screens/budget_screen.dart';
import 'screens/datagrid_screen.dart';
import 'screens/financial_sheet_screen.dart';
import 'screens/inventory_screen.dart';
import 'screens/office_time_log_screen.dart';
import 'screens/search_filters_screen.dart';
import 'screens/spreadsheet_screen.dart';
import 'screens/stress_test_screen.dart';

void main() => runApp(const _App());

class _App extends StatelessWidget {
  const _App();

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'ultimate_grid example',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(useMaterial3: true),
        home: const _Shell(),
      );
}

class _ExampleEntry {
  final String label;
  final String subtitle;
  final IconData icon;
  final Widget Function(GridTheme theme) build;
  const _ExampleEntry({
    required this.label,
    required this.subtitle,
    required this.icon,
    required this.build,
  });
}

final _examples = <_ExampleEntry>[
  _ExampleEntry(
    label: 'Inventory',
    subtitle: 'Minimal — 60 rows',
    icon: Icons.inventory_2_outlined,
    build: (t) => InventoryScreen(theme: t),
  ),
  _ExampleEntry(
    label: 'Financial sheet',
    subtitle: 'Merges + 9-region freeze',
    icon: Icons.calendar_view_month_outlined,
    build: (t) => FinancialSheetScreen(theme: t),
  ),
  _ExampleEntry(
    label: 'Async paging',
    subtitle: '100k rows, on-demand',
    icon: Icons.cloud_download_outlined,
    build: (t) => AsyncPagingScreen(theme: t),
  ),
  _ExampleEntry(
    label: 'Search & filters',
    subtitle: 'Highlight / filter + per-column',
    icon: Icons.search,
    build: (t) => SearchFiltersScreen(theme: t),
  ),
  _ExampleEntry(
    label: 'Office Time Log',
    subtitle: 'Engineers × sub-tasks, IT framing',
    icon: Icons.access_time_outlined,
    build: (t) => OfficeTimeLogScreen(theme: t),
  ),
  _ExampleEntry(
    label: 'Budget tracker',
    subtitle: 'Frozen cols + quarter merges',
    icon: Icons.account_balance_wallet_outlined,
    build: (_) => const BudgetExample(),
  ),
  _ExampleEntry(
    label: 'Datagrid',
    subtitle: '200 rows, sort / filter / pin',
    icon: Icons.table_rows_outlined,
    build: (_) => const DatagridExample(),
  ),
  _ExampleEntry(
    label: 'Spreadsheet',
    subtitle: 'Matrix table + merged headers',
    icon: Icons.grid_on_outlined,
    build: (_) => const SpreadsheetExample(),
  ),
  _ExampleEntry(
    label: 'Stress test',
    subtitle: 'Up to 5 M rows, canvas-paint',
    icon: Icons.speed_outlined,
    build: (_) => const StressTestExample(),
  ),
];

class _Shell extends StatefulWidget {
  const _Shell();
  @override
  State<_Shell> createState() => _ShellState();
}

class _ShellState extends State<_Shell> {
  int _selected = 0;
  DemoTheme _themeName = DemoTheme.elegant;
  Color? _accent;
  bool _sidebarCollapsed = false;
  bool _mobilePreview = false;

  static const _mobileMaxWidth = 380.0;

  GridTheme get _theme => themeFor(_themeName, accent: _accent);

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    final compact = _mobilePreview || width < 760;
    final example = _examples[_selected];

    final body = Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        _TopBar(
          example: example,
          themeName: _themeName,
          accent: _accent,
          compact: compact,
          mobilePreview: _mobilePreview,
          onThemeChanged: (t) => setState(() => _themeName = t),
          onAccentChanged: (c) => setState(() => _accent = c),
          onToggleMobile: () =>
              setState(() => _mobilePreview = !_mobilePreview),
          onOpenMenu: compact
              ? () => Scaffold.of(_scaffoldContext!).openDrawer()
              : null,
        ),
        Expanded(
          child: KeyedSubtree(
            // Re-key on theme change so each screen rebuilds its
            // internal table with the new theme.
            key: ValueKey('$_selected:${_themeName.name}:${_accent?.toARGB32()}'),
            child: example.build(_theme),
          ),
        ),
      ],
    );

    final mobileFramed = _mobilePreview
        ? Center(
            child: Container(
              width: _mobileMaxWidth,
              margin: const EdgeInsets.symmetric(vertical: 16),
              decoration: BoxDecoration(
                color: Colors.white,
                border: Border.all(color: const Color(0xFFCBD5E1), width: 1),
                borderRadius: BorderRadius.circular(16),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withValues(alpha: 0.06),
                    blurRadius: 16,
                    offset: const Offset(0, 4),
                  ),
                ],
              ),
              clipBehavior: Clip.antiAlias,
              child: body,
            ),
          )
        : body;

    return Scaffold(
      backgroundColor: const Color(0xFFF8FAFC),
      drawer: compact
          ? Drawer(
              child: SafeArea(
                child: _SidebarList(
                  selected: _selected,
                  collapsed: false,
                  onSelect: (i) {
                    setState(() => _selected = i);
                    Navigator.of(context).maybePop();
                  },
                ),
              ),
            )
          : null,
      body: SafeArea(
        child: Builder(builder: (ctx) {
          _scaffoldContext = ctx;
          return compact
              ? mobileFramed
              : Row(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    _Sidebar(
                      selected: _selected,
                      collapsed: _sidebarCollapsed,
                      onSelect: (i) => setState(() => _selected = i),
                      onToggleCollapsed: () => setState(
                          () => _sidebarCollapsed = !_sidebarCollapsed),
                    ),
                    const VerticalDivider(
                        width: 1, color: Color(0xFFE2E8F0)),
                    Expanded(child: mobileFramed),
                  ],
                );
        }),
      ),
    );
  }

  BuildContext? _scaffoldContext;
}

class _Sidebar extends StatelessWidget {
  final int selected;
  final bool collapsed;
  final ValueChanged<int> onSelect;
  final VoidCallback onToggleCollapsed;
  const _Sidebar({
    required this.selected,
    required this.collapsed,
    required this.onSelect,
    required this.onToggleCollapsed,
  });

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 180),
      curve: Curves.easeOut,
      width: collapsed ? 60 : 240,
      color: const Color(0xFFFFFBF5),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _SidebarHeader(
            collapsed: collapsed,
            onToggleCollapsed: onToggleCollapsed,
          ),
          Expanded(
            child: _SidebarList(
              selected: selected,
              collapsed: collapsed,
              onSelect: onSelect,
            ),
          ),
        ],
      ),
    );
  }
}

class _SidebarHeader extends StatelessWidget {
  final bool collapsed;
  final VoidCallback onToggleCollapsed;
  const _SidebarHeader({
    required this.collapsed,
    required this.onToggleCollapsed,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: collapsed
          ? const EdgeInsets.symmetric(horizontal: 8, vertical: 14)
          : const EdgeInsets.fromLTRB(16, 18, 8, 12),
      child: Row(
        children: [
          Container(
            width: 28,
            height: 28,
            decoration: BoxDecoration(
              color: const Color(0xFFEA580C),
              borderRadius: BorderRadius.circular(6),
            ),
            alignment: Alignment.center,
            child: const Icon(Icons.grid_view_rounded,
                size: 16, color: Colors.white),
          ),
          if (!collapsed) ...[
            const SizedBox(width: 10),
            const Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Ultimate Grid',
                    style: TextStyle(
                      fontSize: 15,
                      fontWeight: FontWeight.w700,
                      color: Color(0xFF0F172A),
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  Text(
                    'by CodeBigya · examples',
                    style: TextStyle(
                      fontSize: 11,
                      color: Color(0xFFC2410C),
                      fontWeight: FontWeight.w500,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              ),
            ),
          ],
          IconButton(
            onPressed: onToggleCollapsed,
            icon: Icon(
              collapsed ? Icons.menu_open : Icons.menu,
              size: 18,
              color: const Color(0xFF64748B),
            ),
            tooltip: collapsed ? 'Expand sidebar' : 'Collapse sidebar',
            visualDensity: VisualDensity.compact,
          ),
        ],
      ),
    );
  }
}

class _SidebarList extends StatelessWidget {
  final int selected;
  final bool collapsed;
  final ValueChanged<int> onSelect;
  const _SidebarList({
    required this.selected,
    required this.collapsed,
    required this.onSelect,
  });

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      itemCount: _examples.length,
      itemBuilder: (ctx, i) {
        final e = _examples[i];
        final active = i == selected;
        return Padding(
          padding: const EdgeInsets.symmetric(vertical: 2),
          child: Material(
            color: active ? const Color(0xFFFFF7ED) : Colors.transparent,
            borderRadius: BorderRadius.circular(8),
            child: InkWell(
              borderRadius: BorderRadius.circular(8),
              onTap: () => onSelect(i),
              child: Tooltip(
                message: collapsed ? e.label : '',
                child: Padding(
                  padding: collapsed
                      ? const EdgeInsets.symmetric(
                          horizontal: 10, vertical: 10)
                      : const EdgeInsets.symmetric(
                          horizontal: 10, vertical: 8),
                  child: collapsed
                      ? Center(
                          child: Icon(
                            e.icon,
                            size: 20,
                            color: active
                                ? const Color(0xFFEA580C)
                                : const Color(0xFF64748B),
                          ),
                        )
                      : Row(
                          children: [
                            Icon(
                              e.icon,
                              size: 18,
                              color: active
                                  ? const Color(0xFFEA580C)
                                  : const Color(0xFF64748B),
                            ),
                            const SizedBox(width: 10),
                            Expanded(
                              child: Column(
                                crossAxisAlignment:
                                    CrossAxisAlignment.start,
                                children: [
                                  Text(
                                    e.label,
                                    style: TextStyle(
                                      fontSize: 13,
                                      fontWeight: active
                                          ? FontWeight.w700
                                          : FontWeight.w500,
                                      color: active
                                          ? const Color(0xFFEA580C)
                                          : const Color(0xFF0F172A),
                                    ),
                                  ),
                                  Text(
                                    e.subtitle,
                                    style: const TextStyle(
                                      fontSize: 11,
                                      color: Color(0xFF64748B),
                                    ),
                                    maxLines: 1,
                                    overflow: TextOverflow.ellipsis,
                                  ),
                                ],
                              ),
                            ),
                          ],
                        ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

class _TopBar extends StatelessWidget {
  final _ExampleEntry example;
  final DemoTheme themeName;
  final Color? accent;
  final bool compact;
  final bool mobilePreview;
  final ValueChanged<DemoTheme> onThemeChanged;
  final ValueChanged<Color?> onAccentChanged;
  final VoidCallback onToggleMobile;
  final VoidCallback? onOpenMenu;
  const _TopBar({
    required this.example,
    required this.themeName,
    required this.accent,
    required this.compact,
    required this.mobilePreview,
    required this.onThemeChanged,
    required this.onAccentChanged,
    required this.onToggleMobile,
    required this.onOpenMenu,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
      decoration: const BoxDecoration(
        color: Colors.white,
        border: Border(bottom: BorderSide(color: Color(0xFFE2E8F0))),
      ),
      child: Row(
        children: [
          if (onOpenMenu != null) ...[
            IconButton(
              onPressed: onOpenMenu,
              icon: const Icon(Icons.menu, size: 20),
              visualDensity: VisualDensity.compact,
              tooltip: 'Examples',
            ),
            const SizedBox(width: 4),
          ],
          Container(
            width: 30,
            height: 30,
            decoration: BoxDecoration(
              color: const Color(0xFFFFF7ED),
              borderRadius: BorderRadius.circular(8),
            ),
            alignment: Alignment.center,
            child: Icon(example.icon,
                size: 16, color: const Color(0xFFEA580C)),
          ),
          const SizedBox(width: 10),
          Expanded(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  example.label,
                  style: const TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.w700,
                    color: Color(0xFF0F172A),
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                Text(
                  example.subtitle,
                  style: const TextStyle(
                    fontSize: 11,
                    color: Color(0xFF64748B),
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ],
            ),
          ),
          const SizedBox(width: 8),
          _ThemePicker(
            themeName: themeName,
            accent: accent,
            onThemeChanged: onThemeChanged,
            onAccentChanged: onAccentChanged,
          ),
          const SizedBox(width: 4),
          IconButton(
            onPressed: onToggleMobile,
            icon: Icon(
              mobilePreview
                  ? Icons.desktop_windows_outlined
                  : Icons.smartphone_outlined,
              size: 18,
            ),
            tooltip: mobilePreview
                ? 'Exit mobile preview'
                : 'Preview as mobile',
            visualDensity: VisualDensity.compact,
            color: mobilePreview ? const Color(0xFFEA580C) : null,
          ),
        ],
      ),
    );
  }
}

class _ThemePicker extends StatelessWidget {
  final DemoTheme themeName;
  final Color? accent;
  final ValueChanged<DemoTheme> onThemeChanged;
  final ValueChanged<Color?> onAccentChanged;
  const _ThemePicker({
    required this.themeName,
    required this.accent,
    required this.onThemeChanged,
    required this.onAccentChanged,
  });

  @override
  Widget build(BuildContext context) {
    return Wrap(
      crossAxisAlignment: WrapCrossAlignment.center,
      spacing: 6,
      children: [
        DropdownButton<DemoTheme>(
          value: themeName,
          isDense: true,
          underline: const SizedBox.shrink(),
          icon: const Icon(Icons.expand_more, size: 16),
          items: const [
            DropdownMenuItem(value: DemoTheme.raw, child: Text('Raw')),
            DropdownMenuItem(
                value: DemoTheme.elegant, child: Text('Elegant')),
            DropdownMenuItem(
                value: DemoTheme.professional, child: Text('Professional')),
          ],
          onChanged: (v) {
            if (v != null) onThemeChanged(v);
          },
        ),
        for (final s in accentSwatches)
          _AccentDot(
            color: s.color,
            label: s.label,
            selected: accent?.toARGB32() == s.color.toARGB32(),
            onTap: () => onAccentChanged(
              accent?.toARGB32() == s.color.toARGB32() ? null : s.color,
            ),
          ),
      ],
    );
  }
}

class _AccentDot extends StatelessWidget {
  final Color color;
  final String label;
  final bool selected;
  final VoidCallback onTap;
  const _AccentDot({
    required this.color,
    required this.label,
    required this.selected,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Tooltip(
      message: selected ? '$label (clear)' : label,
      child: InkResponse(
        onTap: onTap,
        radius: 14,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 1, vertical: 2),
          child: Container(
            width: 18,
            height: 18,
            decoration: BoxDecoration(
              color: color,
              shape: BoxShape.circle,
              border: Border.all(
                color: selected
                    ? const Color(0xFF0F172A)
                    : const Color(0xFFE2E8F0),
                width: selected ? 2 : 1,
              ),
            ),
          ),
        ),
      ),
    );
  }
}
3
likes
160
points
84
downloads

Documentation

API reference

Publisher

verified publishercodebigya.com

Weekly Downloads

Scalable, themable 2D data-grid for Flutter — 9-region freeze, canvas-paint body, merges, async data, selection, sort, filter, search.

Repository (GitHub)
View/report issues
Contributing

Topics

#grid #table #datagrid #spreadsheet #timesheet

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on ultimate_grid