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:
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:
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:
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:
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:
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:
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:
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:
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 atsortColumnIndex
is sorted in ascending order. Defaults totrue
.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 to56.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 to24.0
.double columnSpacing
: The horizontal margin between the contents of each data column. Defaults to56.0
.bool showCheckboxColumn
: Whether to display checkbox on selectable rows. Defaults totrue
.bool showFirstLastButtons
: Whether to display buttons to go to the first and last pages. Defaults tofalse
.int? initialFirstRowIndex
: The initial index of the first row to be displayed. Defaults to0
.ValueChanged<int>? onPageChanged
: Invoked when the user switches to another page.int rowsPerPage
: The number of rows on each page. Defaults todefaultRowsPerPage
List<int> availableRowsPerPage
: List of options forrowsPerPage
. Defaults toconst <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 toDragStartBehavior.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 parentPrimaryScrollController
.
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: