Flutter - Using PaginatedDataTable Widget Examples

This tutorial shows you how to use the PaginatedDataTable widget in Flutter.

Table is a common way to display data. If a table has many rows, sometimes it would be better if the rows can be divided into several pages. In other words, a page only shows a limited number of rows. That kind of table needs to provide a navigation component so that the users can see the data on other pages. If your application uses Flutter, there is already a widget called PaginatedDataTable which can be used for creating such a table.

Using PaginatedDataTable

Below is the constructor of PaginatedDataTable. It has a lot of parameters. We are going to start with the required and important parameters first, followed by the parameters for customizing the look and behavior of the table.

  PaginatedDataTable({
    Key? key,
    Widget? header,
    List<Widget>? actions,
    required List<DataColumn>? columns,
    int? sortColumnIndex,
    bool sortAscending = true,
    ValueSetter<bool?>? onSelectAll,
    @Deprecated('Migrate to use dataRowMinHeight and dataRowMaxHeight instead. ' 'This feature   was   deprecated after v3.7.0-5.0.pre.') double? dataRowHeight,
    double? dataRowMinHeight,
    double? dataRowMaxHeight,
    double headingRowHeight = 56.0,
    double horizontalMargin = 24.0,
    double columnSpacing = 56.0,
    bool showCheckboxColumn = true,
    bool showFirstLastButtons = false,
    int? initialFirstRowIndex = 0,
    ValueChanged<int>? onPageChanged,
    int rowsPerPage = defaultRowsPerPage,
    List<int> availableRowsPerPage = const <int>[
      defaultRowsPerPage,
      defaultRowsPerPage * 2,
      defaultRowsPerPage * 5,
      defaultRowsPerPage * 10
    ],
    ValueChanged<int?>? onRowsPerPageChanged,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
    Color? arrowHeadColor,
    required DataTableSource source,
    double? checkboxHorizontalMargin,
    ScrollController? controller,
    bool? primary,
  })

Set Column List

A table usually has a header that contains the labels of each column. To use the PaginatedDataTable, you have to define the column list by passing a List of DataColumn as the columns argument.

Below is the constructor of DataColumn. It allows you to pass several arguments, but only the label argument that must be passed. For that argument, you have to pass a value whose type is a Widget, typically a Text widget.

  DataColumn DataColumn({
    required Widget label,
    String? tooltip,
    bool numeric = false,
    void Function(int, bool)? onSort,
    MaterialStateProperty? mouseCursor,
  })

The other arguments are optional. You can add a tooltip by passing a string as the tooltip argument. To set whether the column has numeric values or not, you can pass a boolean value as the numeric argument. You can also pass a function that handles sort as the onSort argument and a mouse cursor as the mouseCursor argument.

The list of columns have to be passed as the columns argument.

  class _MyTableState extends State<MyTable> {
  
    final List<DataColumn> _columns = [
      const DataColumn(label: Text('ID')),
      const DataColumn(label: Text('Name')),
      const DataColumn(label: Text('Score')),
    ];
  
    @override
    Widget build(BuildContext context) {
      return PaginatedDataTable(
        columns: _columns,
        // other arguments
      );
    }
  }

Set Data Source

If you use PaginatedDataTable, another argument that must be passed is source which represents the data to be displayed as rows. You must pass an object whose type is DataTableSource. Usually, you have to create a custom class that extends DataTableSource, which is an abstract class. By extending the class, there are several methods that you have to implement.

  DataRow getRow(int index)
  bool get isRowCountApproximate;
  int get rowCount;
  int get selectedRowCount;

The getRow method is used to return the row at a given index. Usually, the class has a List variable and the getRow needs to return a DataRow object according to the element of the List at the given index.

  DataRow DataRow({
    LocalKey? key,
    bool selected = false,
    void Function(bool?)? onSelectChanged,
    void Function()? onLongPress,
    MaterialStateProperty? color,
    MaterialStateProperty? mouseCursor,
    required List<DataCell> cells,
  })

The only required argument is cells, for which you have to pass a List of DataCell. Each DataCell represents a cell of a row which can be created using the constructor below. The number of cells on each row must be the same as the number of columns. Otherwise, you'll get an assertion error.

  DataCell(
    Widget child,
    {
      boolplaceholder=false,
      boolshowEditIcon=false,
      void Function()? onTap,
      void Function()? onLongPress,
      void Function(TapDownDetails)? onTapDown,
      void Function()? onDoubleTap,
      void Function()? onTapCancel
    }
  )

To avoid making this tutorial more complex, the example below only passes the positional argument which is a Widget that displays the content of the cell. In this case, the most common widget to use is a Text widget.

Back to the DataTableSource class. Besides the getRow method, you must implement three getters. The first one is isRowCountApproximate, which should return true if the row count is precise or false if it's over-estimated. The other getters are rowCount and selectedRowCount which return the total number of rows and the number of selected rows respectively.

The example below shows a basic example where the data is generated randomly. In real usages, you may need to handle fetching data from an external source when the user changes the page or rows per page.

  class Item {
  
    int id;
    String name;
    int price;
  
    Item({required this.id, required this.name, required this.price});
  }
  
  class MyData extends DataTableSource {
  
    final _data = List.generate(200, (index) =gt; Item(
      id: index,
      name: 'Item $index',
      price: Random().nextInt(100000),
    ));
  
    @override
    DataRow getRow(int index) {
      return DataRow(cells: [
        DataCell(Text(_data[index].id.toString())),
        DataCell(Text(_data[index].name)),
        DataCell(Text(_data[index].price.toString())),
      ]);
    }
  
    @override
    bool get isRowCountApproximate =gt; false;
  
    @override
    int get rowCount =gt; _data.length;
  
    @override
    int get selectedRowCount =gt; 0;
  }

After creating the class, you can pass an instance of it as the source argument. Below is an example of a State class that returns a PaginatedDataTable widget.

  class _MyTableState extends State<MyTable> {

    late MyData _myData;
  
    final List<DataColumn> _columns = [
      const DataColumn(label: Text('ID')),
      const DataColumn(label: Text('Name')),
      const DataColumn(label: Text('Score')),
    ];
  
    @override
    void initState() {
      super.initState();
      _myData= MyData();
    }
  
    @override
    Widget build(BuildContext context) {
      return PaginatedDataTable(
        columns: _columns,
        source: _myData,
        // other arguments
      );
    }
  }

Output:

Flutter - PaginatedDataTable

Handle Scroll

It's very common for a data table to have many rows that all rows cannot fit into the screen at the same time. The most simple solution is by using a SingleChildScrollView widget, which allows its child to be scrolled. Assuming MyTable is the class that has a PaginatedDataTable as its child, you can put it as the child of a SingleChildScrollView widget.

  const SingleChildScrollView(
    child: MyTable(),
  )

Handle Page Changed

When the user changes the page, you can handle the events by passing a function as the onPageChanged argument. The function takes one parameter which is an integer that represents the index of the first row on the currently displayed page.

  PaginatedDataTable(
    columns: _columns,
    source: MyData(),
    onPageChanged: (int startIndex) {
      setState(() {
        _startIndex = startIndex;
      });
    },
    // other arguments
  )

Set Initial First Row Index

By default, the first row to be displayed is the row at index 0 (the index starts from 0). You can change it by passing an integer value as the initialFirstRowIndex argument.

  PaginatedDataTable(
    columns: _columns,
    source: MyData(),
    initialFirstRowIndex: 10,
    // other arguments
  )

Output:

Flutter - PaginatedDataTable - Initial First Row Index

Set Rows per Page

To set how many rows are displayed on each page, pass an integer value as the rowsPerPage argument. The default value is 10.

  PaginatedDataTable(
    columns: _columns,
    source: MyData(),
    rowsPerPage: 5,
    // other arguments
  )

Output:

Flutter - PaginatedDataTable - Rows Per Page

Dynamic Rows Per Page

It's also possible to make the number of rows per page to be dynamic, allowing the user to select the value from a dropdown. First, instead of hard coding the value of rowsPerPage argument, you should pass a variable whose value can be updated for the argument. For example, use a state variable.

  int _rowsPerPage = 10

To display the dropdown for changing the rows per page, you have to pass a function as the onRowsPerPageChanged. The function takes a parameter whose type is int?. When the user changes the value using the dropdown, the function will be invoked with the new rows per page value as the argument. Inside the function, update the variable that's passed as the rowsPerPage argument. If the onRowsPerPageChanged argument is not passed or null, the dropdown for changing the rows per page will not be shown.

The default options on the dropdown are 10, 20, 50, and 100. It can be changed by passing a List of integer values as the availableRowsPerPage argument. You need to make sure that the initial value of rowsPerPage is included in the options to avoid getting an assertion error.

  PaginatedDataTable(
    columns: _columns,
    source: MyData(),
    rowsPerPage: _rowsPerPage,
    availableRowsPerPage: const [10, 50, 100],
    onRowsPerPageChanged: (int? updatedRowsPerPage) {
      if (updatedRowsPerPage != null) {
        setState(() {
          _rowsPerPage = updatedRowsPerPage;
        });
      }
    },
    // other arguments
  )

Output:

Flutter - PaginatedDataTable - Dynamic  Rows Per Page

Set Row Height

To add the height constraints for the row, there are two arguments that you can pass. The minimum height can be set by passing a double value as the dataRowMinHeight argument. For the maximum height, the argument is dataRowMaxHeight. The value of both arguments is 48.0 by default.

  PaginatedDataTable(
    columns: _columns,
    source: MyData(),
    dataRowMinHeight: 10,
    dataRowMaxHeight: 25,
    // other arguments
  )

Output:

Flutter - PaginatedDataTable - Row Height

Set Column Spacing

The spacing between each column can be set by passing a double value as the columnSpacing argument. The value defaults to 56.0 to adhere to the Material Design specifications.

  PaginatedDataTable(
    columns: _columns,
    source: MyData(),
    columnSpacing: 30,
    // other arguments
  )

Output:

Flutter - PaginatedDataTable - Column Spacing

Show First & Last Buttons

Besides the previous and next buttons, the widget also supports showing the first and last buttons. It can be done by passing an argument named showFirstLastButtons with true as the value.

  PaginatedDataTable(
    columns: _columns,
    source: MyData(),
    showFirstLastButtons: true,
    // other arguments
  )

Output:

Flutter - PaginatedDataTable - Show First & Last Buttons

Set Header Above the Table

To add a header above the table, pass a Widget as the header argument.

  PaginatedDataTable(
    columns: _columns,
    source: MyData(),
    header: const Text('Items'),
    // other arguments
  )

Output:

Flutter - PaginatedDataTable - Header

PaginatedDataTable Parameters

  • Key? key: The widget's key, used to control how a widget is replaced with another.
  • Widget? header: The header of the table card.
  • List<Widget>? actions: Icon buttons to be displayed at the top end side of the table.
  • required List<DataColumn> columns: The configuration and labels for the columns.
  • int? sortColumnIndex: The index of the column set as the current primary sort key.
  • bool sortAscending: Whether the column at sortColumnIndex is sorted in ascending order. Defaults to true.
  • ValueSetter<bool?>? onSelectAll: A function to be called when the user selects or unselects every row.
  • double? dataRowHeight: The height of each row (excluding the row that contains column headings).
  • double? dataRowMinHeight: The minimum height of each row (excluding the row that contains column headings).
  • double? dataRowMaxHeight: The maximum height of each row (excluding the row that contains column headings).
  • double headingRowHeight: The height of the heading row. Defaults to 56.0.
  • double horizontalMargin: The horizontal margin between the edges of the table and the content in the first and last cells of each row. Defaults to 24.0.
  • double columnSpacing: The horizontal margin between the contents of each data column. Defaults to 56.0.
  • bool showCheckboxColumn: Whether to display checkbox on selectable rows. Defaults to true.
  • bool showFirstLastButtons: Whether to display buttons to go to the first and last pages. Defaults to false.
  • int? initialFirstRowIndex: The initial index of the first row to be displayed. Defaults to 0.
  • ValueChanged<int>? onPageChanged: Invoked when the user switches to another page.
  • int rowsPerPage: The number of rows on each page. Defaults to defaultRowsPerPage
  • List<int> availableRowsPerPage: List of options for rowsPerPage. Defaults to const <int>[defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10]
  • ValueChanged<int?>? onRowsPerPageChanged: A function to be called when the number of rows per page is changed.
  • DragStartBehavior dragStartBehavior: How drag start behavior is handled. Defaults to DragStartBehavior.start
  • Color? arrowHeadColor: The color of the arrow heads in the footer.
  • required DataTableSource source: The data to be shown in the rows.
  • double? checkboxHorizontalMargin: Horizontal margin around the checkbox if displayed.
  • ScrollController? controller: Used to control the position to which this scroll view is scrolled.
  • bool? primary: Whether this is the primary scroll view associated with the parent PrimaryScrollController.

Full Code

  import 'dart:math';
  
  import 'package:flutter/material.dart';
  
  void main() => runApp(const MyApp());
  
  class MyApp extends StatelessWidget {
  
    const MyApp({super.key});
  
    @override
    Widget build(BuildContext context) {
      return const MaterialApp(
        title: 'Woolha.com Flutter Tutorial',
        home: MyPage(),
      );
    }
  }
  
  class MyPage extends StatelessWidget {
  
    const MyPage({super.key});
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: const Text('Woolha.com Flutter Tutorial'),
          backgroundColor: Colors.teal,
        ),
        body: const SingleChildScrollView(
          child: MyTable(),
        ),
      );
    }
  }
  
  class MyTable extends StatefulWidget {
  
    const MyTable({super.key});
  
    @override
    State<MyTable> createState() => _MyTableState();
  }
  
  class _MyTableState extends State<MyTable> {
  
    int _rowsPerPage = 10;
    int _startIndex = 0;
    late MyData _myData;
  
    final List<DataColumn> _columns = [
      const DataColumn(label: Text('ID')),
      const DataColumn(label: Text('Name')),
      const DataColumn(label: Text('Score')),
    ];
  
    @override
    void initState() {
      super.initState();
      _myData= MyData();
    }
  
    @override
    Widget build(BuildContext context) {
      return PaginatedDataTable(
        columns: _columns,
        source: _myData,
        onPageChanged: (int startIndex) {
          setState(() {
            _startIndex = startIndex;
          });
        },
        rowsPerPage: _rowsPerPage,
        availableRowsPerPage: const [10, 50, 100],
        onRowsPerPageChanged: (int? updatedRowsPerPage) {
          if (updatedRowsPerPage != null) {
            setState(() {
              _rowsPerPage = updatedRowsPerPage;
            });
          }
        },
        showCheckboxColumn: true,
        dataRowMinHeight: 10,
        dataRowMaxHeight: 25,
        columnSpacing: 30,
        initialFirstRowIndex: 10,
        showFirstLastButtons: true,
        header: const Text('Items'),
      );
    }
  }
  
  class Item {
  
    int id;
    String name;
    int price;
  
    Item({required this.id, required this.name, required this.price});
  }
  
  class MyData extends DataTableSource {
  
    final _data = List.generate(200, (index) => Item(
      id: index + 1,
      name: 'Item $index',
      price: Random().nextInt(100000),
    ));
  
    @override
    DataRow getRow(int index) {
      return DataRow(cells: [
        DataCell(Text(_data[index].id.toString())),
        DataCell(Text(_data[index].name)),
        DataCell(Text(_data[index].price.toString())),
      ]);
    }
  
    @override
    bool get isRowCountApproximate => false;
  
    @override
    int get rowCount => _data.length;
  
    @override
    int get selectedRowCount => 0;
  }

Summary

The PaginatedDataTable widget can be the solution if you need to display a table with a pagination in your Flutter application. To use the widget, you need to specify the column and the data source. It allows you to set how many rows are displayed per page including a dropdown for changing the number of rows. The widget also supports other customizations such as setting the row height, setting the column spacing, as well showing first & last buttons.

You can also read about: