paged_datatable 2.2.0-dev1.1 paged_datatable: ^2.2.0-dev1.1 copied to clipboard
A brand new way of creating paginated DataTables in Flutter with sorting and filters
PagedDataTable #
PagedDataTable is a fully customizable and powerful data table widget for Flutter, supporting both cursor-based and offset-based pagination. It’s built entirely from scratch, without dependencies on Flutter’s native DataTable
or Table
. It follows Google’s Material You design principles. Whether you're building dashboards, admin panels, or any app with complex tabular data, this library makes it easy to display, sort, and filter your data.
Online Demo #
Explore a live demo of the table in action:
Check it out here
Why Use PagedDataTable? #
The library offers:
- Horizontal scrolling: Handle columns wider than the screen by scrolling.
- Expandable rows: Toggle row details on demand.
- Fixed columns: Lock specific columns during horizontal scrolling.
- In-place row updates: Edit objects directly within the table without needing separate views.
- Cursor and offset pagination: Choose the pagination style that fits your use case.
- Dynamic filtering and sorting: Filter by text, date, numbers, and more.
- Controller-based modifications: Add or remove rows without reloading the entire table.
- Custom themes: Easily change fonts, colors, and other styles to match your app's theme.
Table of Contents #
- PagedDataTable
Quick Start #
PagedDataTable<int, User>(
fetcher: (pageSize, sortModel, filterModel, pageToken) async {
final result = await UserService.getUsers(
filterByName: filterModel["name"],
sortBy: sortModel.fieldName,
sortByDirection: sortModel.descending ? 'desc' : 'asc',
pageToken: pageToken,
pageSize: pageSize,
);
return (result.data, result.nextPageToken);
},
columns: [
TableColumn(
title: const Text("Name"),
cellBuilder: (context, user, _) => Text(user.name),
),
EditableTableColumn(
title: const Text("Age"),
getter: (user) => user.age.toString(),
setter: (user, value) {
user.age = int.tryParse(value) ?? user.age;
return true;
},
),
],
filters: [
TextTableFilter(id: "name", name: "Name"),
],
);
This minimal setup gives you a functional data table with pagination, filtering, and editable rows. Adjust the fetcher
to fit your data source and watch the table handle your data seamlessly.
Setup #
To get started, create a PagedDataTable<K, T> widget, where:
- K is the key type used for pagination.
- T is the type of data you want to display in the table.
PagedDataTable<String, Post>(
fetcher: (int pageSize, SortModel? sortModel, FilterModel filterModel, String? pageToken) => ...,
columns: [...],
)
Important: K
must extend Comparable
.
Fetcher #
The fetcher
function loads new pages when needed. It must return a FutureOr<(List<T>, K?)>
, where the first value is the list of items and the second value is the token for the next page (if available).
PagedDataTable<String, Post>(
fetcher: (int pageSize, SortModel? sortModel, FilterModel filterModel, String? pageToken) async {
final result = await FetchService.listPosts();
return (result.data, result.nextPageToken);
},
columns: [...],
)
By default, the table does not cache pages.
Expansible Rows #
To use expandable rows, create a new PagedDataTable
with the expansible
constructor. The properties remain the same as in the standard constructor, but the key difference lies in the fetcher function.
Instead of returning a FutureOr<(List<T>, K?)>
, the fetcher must return a FutureOr<(Map<T, List<T>?>, K?)>
. In this case:
- The first element of the returned tuple is a Map, where:
- The
key
represents the "main" entry. - The
value
is an optional list containing the expandable rows associated with that entry. - If the
value
is null, expandable rows will not be enabled for that key.
- The
K
represents the next page token (if available).
To display a column that allows expanding or collapsing rows, include CollapsibleRowColumn
in the columns property.
child: PagedDataTable<String, Post>.expansible(
fetcher: (pageSize, sortModel, filterModel, pageToken) async {
// This is just an example
final data = await PostsRepository.getPosts(
pageSize: pageSize,
pageToken: pageToken,
);
final resultset = <Post, List<Post>?>{};
for (final post in data.items) {
// getRelatedPosts will return null if there are no related posts for post
resultset[post] = PostsRepository.getRelatedPosts(post);
}
return (resultset, data.nextPageToken);
},
columns: [
CollapsibleRowColumn(),
// ...other columns
],
)
Header #
The header contains both column names and a filter bar. You can customize it by passing a widget to the filterBarChild
property (e.g., a PopupMenuButton
).
Footer #
The footer
property allows you to fully customize the footer. If not provided, a DefaultFooter
will be rendered with the following widgets:
RefreshTable
: Reloads the current dataset.PageSizeSelector
: Adjusts the number of items per page.CurrentPage
: Shows the current page number.NavigationButtons
: Go to the next or previous page.
Custom Footer Example
Use built-in widgets like RefreshTable
and NavigationButtons
to build your own footer:
PagedDataTable<String, Post>(
fetcher: ...,
columns: ...,
footer: Row(
children: [
RefreshButton(),
CurrentPage(),
NavigationButtons(),
],
),
)
Columns #
PagedDataTable offers two main column types:
- ReadOnlyTableColumn<K, T>: Displays data without allowing edits.
- EditableTableColumn<K, T, V>: Allows in-place editing of cell content.
PagedDataTable<String, Post>(
columns: [
TableColumn(
title: const Text("Author"),
cellBuilder: (context, item, index) => Text(item.author),
),
EditableTableColumn(
title: const Text("Post Title"),
getter: (item) => item.title,
setter: (item, newValue) {
item.title = newValue;
return true;
},
),
],
)
K
andT
are the same parameters defined in thePagedDataTable
widget.
Every column has:
- title: the column's title. A widget used to display column's name (e.g.
Text
). - size: configures the column's size. By default, it is a
FractionalColumnSize(.1)
, which means it will take 10% of the available width. You can useFixedColumnSize
,FractionalColumnSize
,RemainingColumnSize
andMaxColumnSize
. - format: applies a transformation to the cell's widget. You have
NumericColumnFormat
, which aligns content to the right andAlignColumnFormat
which aligns cell's content to thealignment
property. You can create your own implemeting theColumnFormat
interface. - sort and id: both properties are used to indicate that a column can be used for sorting. The
id
is what you get in the Fetcher'sSortModel
. - There are other properties that you can use to play around and modify your columns. Check out the
ReadOnlyTableColumn
's documentation.
If you want to fix columns at the left, you can specify the amount of columns to fix using the
fixedColumnCount
property.
TableColumn<K, T>
Is the default ReadOnlyTableColumn
that renders a cell using the cellBuilder
property.
PagedDataTable<String, Post>(
columns: [
TableColumn(
title: const Text("Author"),
cellBuilder: (context, item, index) => Text(item.author),
),
],
)
EditableTableColumn<K, T, V>
This abstract class introduces two key properties:
getter
: Retrieves the valueV
to be displayed in the cell.setter
: A function that sets a new value. It must return a boolean indicating if the operation was successful. If true, the cell updates with the new value; otherwise, it retains the original one.
There are four built-in editable columns:
DropdownTableColumn
: Renders a dropdown menu for selecting values.TextTableColumn
: Initially displays aText
widget. Upon double-click, it switches to aTextField
for in-place editing.LargeTextTableColumn
: Works likeTextTableColumn
but opens an overlay on double-click, providing a more spacious interface for editing large text content.RowSelectorColumn
: displays a checkbox used to select/unselect rows.
Creating Custom Columns
Extend ReadOnlyTableColumn
or EditableTableColumn
to define your own column types:
class MyCustomColumn<K, T> extends ReadOnlyTableColumn<K, T> {
@override
Widget build(BuildContext context, T item, int index) {
return MyCustomCellWidget(item);
}
}
Filters #
PagedDataTable supports multiple filter types, such as text fields, dropdowns, and date pickers. Use the filters
property to define them:
PagedDataTable<String, Post>(
filters: [
TextTableFilter(id: "content", name: "Content", chipFormatter: (value) => 'Contains "$value"'),
DropdownTableFilter<Gender>(
id: "authorGender",
name: "Author's Gender",
chipFormatter: (value) => 'Gender: ${value.name}',
items: Gender.values.map((e) => DropdownMenuItem(value: e, child: Text(e.name))).toList(),
),
],
)
The library offers five built-in filter types:
TextTableFilter
: Displays a TextField for filtering based on raw text input.DropdownTableFilter
: Shows a DropdownButton with predefined options for selection.DateTimePickerTableFilter
: Provides a TextField that opens a DateTime picker dialog when tapped.DateRangePickerTableFilter
: Similar to DateTimePickerTableFilter but allows selecting a DateTimeRange.ProgrammingTextFilter
: This filter doesn't render anything in the filter dialog but can be configured programmatically using the controller.
Each filter must define the following core properties:
id
: A unique identifier for the filter, referenced by the fetcher's FilterMode.name
: The label displayed in the filter picker.chipFormatter
: A function that converts the selected value into a user-friendly string, shown in the filter’s chip.
Creating Custom Filters
To create a custom filter, extend TableFilter<T>
and implement buildPicker
:
class CustomTextFilter extends TableFilter<String> {
@override
Widget buildPicker(BuildContext context, FilterState<String> state) {
return TextFormField(
initialValue: state.value,
onSaved: (newValue) => state.value = newValue,
);
}
}
Controller #
Use the PagedDataTableController<K, V>
to programmatically manage rows, filters, sorting, and pagination.
final controller = PagedDataTableController<String, Post>();
controller.addRow(Post(...));
controller.nextPage();
Internationalization #
Add the following line PagedDataTableLocalization.delegate
to your MaterialApp
or CupertinoApp
to enable localization:
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
PagedDataTableLocalization.delegate
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
]
Supported Locales #
- es: Spanish
- en: English
- de: German
- nl: Dutch
- tr: Turkish
- fr: French
- it: Italian
- zh: Chinese
- ru: Russian
- th: Thai
Contribute #
All contributions are welcome! Feel free to open an issue or submit a pull request with your improvements.