Flutter Datatable Example - Build DataTables in Flutter

In this tutorial, you'll learn how to create DataTables in Flutter. DataTables are used to display tabular data in a simple and easy-to-read format. 

We'll cover everything you need to know to build DataTables in Flutter, including code examples and step-by-step instructions.

Data tables display information in a grid-like format of rows and columns. They organize information in a way that's easy to scan, so that users can look for patterns and insights.

This code demonstrates how to create a paginated data table with selectable rows in Flutter and how to save and restore the state of the app using RestorationMixin.

Code:

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

class DataTableDemo extends StatefulWidget {
  const DataTableDemo({super.key});

  @override
  State<DataTableDemo> createState() => _DataTableDemoState();
}

class _RestorableDessertSelections extends RestorableProperty<Set<int>> {
  // The set of indices of selected dessert rows
  Set<int> _dessertSelections = {};

  /// Returns whether or not a dessert row is selected by index.
  bool isSelected(int index) => _dessertSelections.contains(index);

  /// Takes a list of [_Dessert]s and saves the row indices of selected rows
  /// into a [Set].
  void setDessertSelections(List<_Dessert> desserts) {
    final updatedSet = <int>{};
    for (var i = 0; i < desserts.length; i += 1) {
      var dessert = desserts[i];
      if (dessert.selected) {
        updatedSet.add(i);
      }
    }
    _dessertSelections = updatedSet;
    notifyListeners();
  }

  @override
  Set<int> createDefaultValue() => _dessertSelections;

  @override
  Set<int> fromPrimitives(Object? data) {
    final selectedItemIndices = data as List<dynamic>;
    _dessertSelections = {
      ...selectedItemIndices.map<int>((dynamic id) => id as int),
    };
    return _dessertSelections;
  }

  @override
  void initWithValue(Set<int> value) {
    _dessertSelections = value;
  }

  @override
  Object toPrimitives() => _dessertSelections.toList();
}


class _DataTableDemoState extends State<DataTableDemo> with RestorationMixin {
  // Create restoration properties
  final _RestorableDessertSelections _dessertSelections = _RestorableDessertSelections();
  final RestorableInt _rowIndex = RestorableInt(0);
  final RestorableInt _rowsPerPage = RestorableInt(PaginatedDataTable.defaultRowsPerPage);
  final RestorableBool _sortAscending = RestorableBool(true);
  final RestorableIntN _sortColumnIndex = RestorableIntN(null);

  // Declare a _DessertDataSource instance and initialize it to null
  _DessertDataSource? _dessertsDataSource;

  // Set restoration ID
  @override
  String get restorationId => 'data_table_demo';

  // Register restorable properties for restoration
  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_dessertSelections, 'selected_row_indices');
    registerForRestoration(_rowIndex, 'current_row_index');
    registerForRestoration(_rowsPerPage, 'rows_per_page');
    registerForRestoration(_sortAscending, 'sort_ascending');
    registerForRestoration(_sortColumnIndex, 'sort_column_index');

    // Initialize _dessertsDataSource if it is null
    _dessertsDataSource ??= _DessertDataSource(context);

    // Sort the data source according to the sort column index
    switch (_sortColumnIndex.value) {
      case 0:
        _dessertsDataSource!._sort<String>((d) => d.name, _sortAscending.value);
        break;
      case 1:
        _dessertsDataSource!._sort<num>((d) => d.calories, _sortAscending.value);
        break;
      case 2:
        _dessertsDataSource!._sort<num>((d) => d.fat, _sortAscending.value);
        break;
      case 3:
        _dessertsDataSource!._sort<num>((d) => d.carbs, _sortAscending.value);
        break;
      case 4:
        _dessertsDataSource!._sort<num>((d) => d.protein, _sortAscending.value);
        break;
      case 5:
        _dessertsDataSource!._sort<num>((d) => d.sodium, _sortAscending.value);
        break;
      case 6:
        _dessertsDataSource!._sort<num>((d) => d.calcium, _sortAscending.value);
        break;
      case 7:
        _dessertsDataSource!._sort<num>((d) => d.iron, _sortAscending.value);
        break;
    }

    // Update the selection of desserts
    _dessertsDataSource!.updateSelectedDesserts(_dessertSelections);

    // Add listener to _dessertsDataSource to update selected dessert row
    _dessertsDataSource!.addListener(_updateSelectedDessertRowListener);
  }

  // Add listener to _dessertsDataSource when the widget is attached to the tree
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    // Initialize _dessertsDataSource if it is null
    _dessertsDataSource ??= _DessertDataSource(context);

    // Add listener to _dessertsDataSource to update selected dessert row
    _dessertsDataSource!.addListener(_updateSelectedDessertRowListener);
  }

  // Update the selected dessert row
  void _updateSelectedDessertRowListener() {
    _dessertSelections.setDessertSelections(_dessertsDataSource!._desserts);
  }

  // Sort the data source
  void _sort<T>(
      Comparable<T> Function(_Dessert d) getField,
      int columnIndex,
      bool ascending,
      ) {
    _dessertsDataSource!._sort<T>(getField, ascending);
    setState(() {
      _sortColumnIndex.value = columnIndex;
      _sortAscending.value = ascending;
    });
  }

  @override
  void dispose() {
    // Dispose of the _rowsPerPage stream subscription
    _rowsPerPage.dispose();

    // Dispose of the _sortColumnIndex stream subscription
    _sortColumnIndex.dispose();

    // Dispose of the _sortAscending stream subscription
    _sortAscending.dispose();

    // Remove the listener that updates the selected dessert row
    // from the _dessertsDataSource
    _dessertsDataSource!.removeListener(_updateSelectedDessertRowListener);

    // Dispose of the _dessertsDataSource stream subscription
    _dessertsDataSource!.dispose();

    // Call the superclass's dispose method
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        title: Text('Data Table'), // Set the title of the app bar
      ),
      body: Scrollbar( // Add a scrollbar to the body of the scaffold
        child: ListView(
          restorationId: 'data_table_list_view',
          padding: const EdgeInsets.all(16),
          children: [
            PaginatedDataTable(
              header: Text('Nutrition'), // Set the header text of the data table
              rowsPerPage: _rowsPerPage.value,
              onRowsPerPageChanged: (value) {
                setState(() {
                  _rowsPerPage.value = value!;
                });
              },
              initialFirstRowIndex: _rowIndex.value,
              onPageChanged: (rowIndex) {
                setState(() {
                  _rowIndex.value = rowIndex;
                });
              },
              sortColumnIndex: _sortColumnIndex.value,
              sortAscending: _sortAscending.value,
              onSelectAll: _dessertsDataSource!._selectAll, // Set the select all callback of the data table
              columns: [
                DataColumn(
                  label: Text('Dessert 1 Serving'), // Set the label of the first column
                  onSort: (columnIndex, ascending) =>
                      _sort<String>((d) => d.name, columnIndex, ascending), // Set the sorting function of the first column
                ),
                DataColumn(
                  label: Text('Calories'), // Set the label of the second column
                  numeric: true,
                  onSort: (columnIndex, ascending) =>
                      _sort<num>((d) => d.calories, columnIndex, ascending), // Set the sorting function of the second column
                ),
                DataColumn(
                  label: Text('Fat (g)'), // Set the label of the third column
                  numeric: true,
                  onSort: (columnIndex, ascending) =>
                      _sort<num>((d) => d.fat, columnIndex, ascending), // Set the sorting function of the third column
                ),
                DataColumn(
                  label: Text('Carbs (g)'), // Set the label of the fourth column
                  numeric: true,
                  onSort: (columnIndex, ascending) =>
                      _sort<num>((d) => d.carbs, columnIndex, ascending), // Set the sorting function of the fourth column
                ),
                DataColumn(
                  label: Text('Protein (g)'), // Set the label of the fifth column
                  numeric: true,
                  onSort: (columnIndex, ascending) =>
                      _sort<num>((d) => d.protein, columnIndex, ascending), // Set the sorting function of the fifth column
                ),
                DataColumn(
                  label: Text('Sodium (mg)'), // Set the label of the sixth column
                  numeric: true,
                  onSort: (columnIndex, ascending) =>
                      _sort<num>((d) => d.sodium, columnIndex, ascending), // Set the sorting function of the sixth column
                ),
                DataColumn(
                  label: Text('Calcium %'), // Set the label of the seventh column
                  numeric: true,
                  onSort: (columnIndex, ascending) =>
                      _sort<num>((d) => d.calcium, columnIndex, ascending), // Set the sorting function of the seventh column
                ),
                DataColumn(
                  label: Text('Iron %'), // Set the label of the eighth column
                  numeric: true,
                  onSort: (columnIndex, ascending) =>
                      _sort<num>((d) => d.iron, columnIndex, ascending), // Set the sorting function of the eighth column
                ),
              ],
              source: _dessertsDataSource!,
            ),
          ],
        ),
      ),
    );
  }
}

class _Dessert {
  _Dessert(
      this.name,     // The name of the dessert
      this.calories, // The number of calories in the dessert
      this.fat,      // The amount of fat in the dessert (in grams)
      this.carbs,    // The number of carbohydrates in the dessert (in grams)
      this.protein,  // The amount of protein in the dessert (in grams)
      this.sodium,   // The amount of sodium in the dessert (in milligrams)
      this.calcium,  // The amount of calcium in the dessert (in milligrams)
      this.iron,     // The amount of iron in the dessert (in milligrams)
      );

  final String name;     // The name of the dessert (immutable)
  final int calories;    // The number of calories in the dessert (immutable)
  final double fat;      // The amount of fat in the dessert (in grams) (immutable)
  final int carbs;      // The number of carbohydrates in the dessert (in grams) (immutable)
  final double protein;  // The amount of protein in the dessert (in grams) (immutable)
  final int sodium;     // The amount of sodium in the dessert (in milligrams) (immutable)
  final int calcium;    // The amount of calcium in the dessert (in milligrams) (immutable)
  final int iron;       // The amount of iron in the dessert (in milligrams) (immutable)
  bool selected = false; // Whether the dessert is currently selected (default is false)
}

class _DessertDataSource extends DataTableSource {
  // Constructor for _DessertDataSource class, takes a BuildContext argument
  _DessertDataSource(this.context) {
    // Initialize the list of desserts
    _desserts = <_Dessert>[
      _Dessert(
        'Frozen Yogurt',
        159,
        6.0,
        24,
        4.0,
        87,
        14,
        1,
      ),
      _Dessert(
        'IceCream Sandwich',
        237,
        9.0,
        37,
        4.3,
        129,
        8,
        1,
      ),
      _Dessert(
        'Eclair',
        262,
        16.0,
        24,
        6.0,
        337,
        6,
        7,
      ),
      _Dessert(
        'Cupcake',
        305,
        3.7,
        67,
        4.3,
        413,
        3,
        8,
      ),
      _Dessert(
        'Gingerbread',
        356,
        16.0,
        49,
        3.9,
        327,
        7,
        16,
      ),
      _Dessert(
        'JellyBean',
        375,
        0.0,
        94,
        0.0,
        50,
        0,
        0,
      ),
      _Dessert(
        'Lollipop',
        392,
        0.2,
        98,
        0.0,
        38,
        0,
        2,
      ),
      _Dessert(
        'Honeycomb',
        408,
        3.2,
        87,
        6.5,
        562,
        0,
        45,
      ),
      _Dessert(
        'Donut',
        452,
        25.0,
        51,
        4.9,
        326,
        2,
        22,
      ),
      _Dessert(
        'Apple Pie',
        518,
        26.0,
        65,
        7.0,
        54,
        12,
        6,
      ),
    ];
  }

  // The BuildContext passed to the constructor
  final BuildContext context;

  // List of desserts
  late List<_Dessert> _desserts;

  // Sort the desserts by a given field
  void _sort<T>(Comparable<T> Function(_Dessert d) getField, bool ascending) {
    _desserts.sort((a, b) {
      final aValue = getField(a);
      final bValue = getField(b);
      return ascending
          ? Comparable.compare(aValue, bValue)
          : Comparable.compare(bValue, aValue);
    });
    notifyListeners();
  }

  // Number of selected desserts
  int _selectedCount = 0;

  // Update the selected desserts
  void updateSelectedDesserts(_RestorableDessertSelections selectedRows) {
    _selectedCount = 0;
    for (var i = 0; i < _desserts.length; i += 1) {
      var dessert = _desserts[i];
      if (selectedRows.isSelected(i)) {
        dessert.selected = true;
        _selectedCount += 1;
      } else {
        dessert.selected = false;
      }
    }
    notifyListeners();
  }

  // Get a DataRow for a given index
  @override
  DataRow? getRow(int index) {
    // Number formatter for percentages
    final format = NumberFormat.decimalPercentPattern(
      decimalDigits: 0,
    );
    // Make sure index is valid
    assert(index >= 0);
    if (index >= _desserts.length) return null;
    final dessert = _desserts[index];
    // Create the DataRow with cells for each dessert property
    return DataRow.byIndex(
      index: index,
      selected: dessert.selected,
      onSelectChanged: (value) {
        // Update the selected count and dessert selection
        if (dessert.selected != value) {
          _selectedCount += value! ? 1 : -1;
          assert(_selectedCount >= 0);
          dessert.selected = value;
          notifyListeners();
        }
      },
      cells: [
        DataCell(Text(dessert.name)),
        DataCell(Text('${dessert.calories}')),
        DataCell(Text(dessert.fat.toStringAsFixed(1))),
        DataCell(Text('${dessert.carbs}')),
        DataCell(Text(dessert.protein.toStringAsFixed(1))),
        DataCell(Text('${dessert.sodium}')),
        DataCell(Text(format.format(dessert.calcium / 100))),
        DataCell(Text(format.format(dessert.iron / 100))),
      ],
    );
  }

  // Number of rows in the DataTable
  @override
  int get rowCount => _desserts.length;

  // Whether or not the rowCount is approximate
  @override
  bool get isRowCountApproximate => false;

  // Number of selected rows
  @override
  int get selectedRowCount => _selectedCount;

  // Select or deselect all rows
  void _selectAll(bool? checked) {
    for (final dessert in _desserts) {
      dessert.selected = checked ?? false;
    }
    _selectedCount = checked! ? _desserts.length : 0;
    notifyListeners();
  }
}




Step-by-Step Code Explanation:

  • Import the necessary packages and dependencies.
  • Create a new StatefulWidget called DataTableExample.
  • In the State of the DataTableExample widget, define two lists: columns and rows. The columns list contains the column names for the DataTable, and the rows list contains the data to display in the table.
  • Build the UI by creating a new Scaffold widget with an AppBar and a SingleChildScrollView containing a DataTable widget.
  • In the DataTable widget, use the helper functions getColumns and getRows to generate the columns and rows, respectively.
  • In the getColumns function, create a new DataColumn for each column name in the columns list.
  • In the getRows function, create a new DataRow for each row of data in the rows list.
  • In the getCells function, create a new DataCell for each cell of data in the rowData list.
  • Build and run the app to see the final result.

..

Package: intl

Provides internationalization and localization facilities, including message translation, plurals and genders, date/number formatting and parsing, and bidirectional text. 

..

Comments