├── .pubignore
├── test
└── paged_datatable_test.dart
├── gen_locales.sh
├── analysis_options.yaml
├── example
├── web
│ ├── favicon.png
│ ├── manifest.json
│ └── index.html
├── macos
│ ├── Runner
│ │ ├── Configs
│ │ │ ├── Debug.xcconfig
│ │ │ ├── Release.xcconfig
│ │ │ ├── Warnings.xcconfig
│ │ │ └── AppInfo.xcconfig
│ │ ├── Assets.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── app_icon_16.png
│ │ │ │ ├── app_icon_32.png
│ │ │ │ ├── app_icon_64.png
│ │ │ │ ├── app_icon_1024.png
│ │ │ │ ├── app_icon_128.png
│ │ │ │ ├── app_icon_256.png
│ │ │ │ ├── app_icon_512.png
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.swift
│ │ ├── Release.entitlements
│ │ ├── DebugProfile.entitlements
│ │ ├── MainFlutterWindow.swift
│ │ └── Info.plist
│ ├── .gitignore
│ ├── Flutter
│ │ ├── Flutter-Debug.xcconfig
│ │ ├── Flutter-Release.xcconfig
│ │ └── GeneratedPluginRegistrant.swift
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Runner.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── RunnerTests
│ │ └── RunnerTests.swift
│ ├── Podfile.lock
│ └── Podfile
├── pubspec.yaml
├── .gitignore
├── .metadata
├── lib
│ ├── post.dart
│ └── main.dart
└── pubspec.lock
├── resources
├── screenshot1.png
├── screenshot2.png
├── screenshot3.png
├── screenshot4.png
└── screenshot5.png
├── lib
├── paged_datatable.dart
├── src
│ ├── filter_state.dart
│ ├── utils.dart
│ ├── configuration.dart
│ ├── sort_model.dart
│ ├── column_format.dart
│ ├── table_controller_notifier.dart
│ ├── footer.dart
│ ├── filter_model.dart
│ ├── column_size.dart
│ ├── table_view_rows.dart
│ ├── double_list_rows.dart
│ ├── filter_widgets.dart
│ ├── row.dart
│ ├── theme.dart
│ ├── header.dart
│ ├── filter.dart
│ ├── footer_widgets.dart
│ ├── column.dart
│ ├── paged_datatable.dart
│ ├── filter_bar.dart
│ ├── linked_scroll_controller.dart
│ └── controller.dart
└── l10n
│ ├── intl_en.arb
│ ├── intl_es.arb
│ ├── intl_it.arb
│ ├── intl_de.arb
│ └── generated
│ ├── intl
│ ├── messages_en.dart
│ ├── messages_all.dart
│ ├── messages_es.dart
│ ├── messages_it.dart
│ └── messages_de.dart
│ └── l10n.dart
├── .metadata
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── pubspec.yaml
├── LICENSE
├── CHANGELOG.md
└── README.md
/.pubignore:
--------------------------------------------------------------------------------
1 | resources
2 | gen_locales
3 | example/macos
--------------------------------------------------------------------------------
/test/paged_datatable_test.dart:
--------------------------------------------------------------------------------
1 | void main() {}
2 |
--------------------------------------------------------------------------------
/gen_locales.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | dart run intl_utils:generate
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:flutter_lints/flutter.yaml
2 |
--------------------------------------------------------------------------------
/example/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/example/web/favicon.png
--------------------------------------------------------------------------------
/resources/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/resources/screenshot1.png
--------------------------------------------------------------------------------
/resources/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/resources/screenshot2.png
--------------------------------------------------------------------------------
/resources/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/resources/screenshot3.png
--------------------------------------------------------------------------------
/resources/screenshot4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/resources/screenshot4.png
--------------------------------------------------------------------------------
/resources/screenshot5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/resources/screenshot5.png
--------------------------------------------------------------------------------
/example/macos/Runner/Configs/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Debug.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/macos/Runner/Configs/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Release.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/macos/.gitignore:
--------------------------------------------------------------------------------
1 | # Flutter-related
2 | **/Flutter/ephemeral/
3 | **/Pods/
4 |
5 | # Xcode-related
6 | **/dgph
7 | **/xcuserdata/
8 |
--------------------------------------------------------------------------------
/example/macos/Flutter/Flutter-Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "ephemeral/Flutter-Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/macos/Flutter/Flutter-Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "ephemeral/Flutter-Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomasweigenast/paged-datatable/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
--------------------------------------------------------------------------------
/example/macos/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | @NSApplicationMain
5 | class AppDelegate: FlutterAppDelegate {
6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
7 | return true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/example/macos/Runner/Release.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lib/paged_datatable.dart:
--------------------------------------------------------------------------------
1 | library paged_datatable;
2 |
3 | export './l10n/generated/l10n.dart';
4 | export './src/paged_datatable.dart';
5 | export './src/column_size.dart';
6 | export './src/column_format.dart';
7 | export './src/configuration.dart';
8 | export './src/footer.dart';
9 | export './src/theme.dart';
10 |
--------------------------------------------------------------------------------
/example/macos/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 400608f101bcfb21db99ac4d5df763a80c279337
8 | channel: beta
9 |
10 | project_type: package
11 |
--------------------------------------------------------------------------------
/example/macos/Flutter/GeneratedPluginRegistrant.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | import FlutterMacOS
6 | import Foundation
7 |
8 | import path_provider_foundation
9 |
10 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
11 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
12 | }
13 |
--------------------------------------------------------------------------------
/lib/src/filter_state.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | /// Represents the state of a [TableFilter].
4 | final class FilterState {
5 | final TableFilter _filter;
6 | T? value;
7 |
8 | FilterState._(this._filter)
9 | : value = _filter
10 | .initialValue; // Set the initial value to the filter's initial value.
11 | }
12 |
--------------------------------------------------------------------------------
/example/macos/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import FlutterMacOS
2 | import Cocoa
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/lib/src/utils.dart:
--------------------------------------------------------------------------------
1 | bool listEquals(List? list1, List? list2) {
2 | if (identical(list1, list2)) return true;
3 | if (list1 == null || list2 == null) return false;
4 | final length = list1.length;
5 | if (length != list2.length) return false;
6 | for (var i = 0; i < length; i++) {
7 | if (list1[i] != list2[i]) return false;
8 | }
9 | return true;
10 | }
11 |
12 | const kEmptyString = "";
13 |
--------------------------------------------------------------------------------
/example/macos/Runner/DebugProfile.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 | com.apple.security.network.server
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/lib/src/configuration.dart:
--------------------------------------------------------------------------------
1 | /// A set of properties used to configure a [PagedDataTable]
2 | final class PagedDataTableConfiguration {
3 | /// A flag that indicates if the table should copy the list of items returned
4 | /// by a fetch callback.
5 | ///
6 | /// This is useful when you don't want to accidentally modify the returned list.
7 | final bool copyItems;
8 |
9 | const PagedDataTableConfiguration({this.copyItems = false});
10 | }
11 |
--------------------------------------------------------------------------------
/example/macos/Runner/MainFlutterWindow.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | class MainFlutterWindow: NSWindow {
5 | override func awakeFromNib() {
6 | let flutterViewController = FlutterViewController()
7 | let windowFrame = self.frame
8 | self.contentViewController = flutterViewController
9 | self.setFrame(windowFrame, display: true)
10 |
11 | RegisterGeneratedPlugins(registry: flutterViewController)
12 |
13 | super.awakeFromNib()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to pub.dev
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v[0-9]+.[0-9]+.[0-9]+*'
7 |
8 | jobs:
9 | publish:
10 | permissions:
11 | id-token: write
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: dart-lang/setup-dart@v1
16 | - name: Install dependencies
17 | run: flutter pub get
18 | - name: Format code
19 | run: dart format .
20 | - name: Publish
21 | run: dart pub publish --force
22 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: paged_datatable_example
2 | description: A new Flutter project.
3 |
4 | publish_to: "none"
5 | version: 1.0.0+1
6 |
7 | environment:
8 | sdk: "^3.0.0"
9 |
10 | dependencies:
11 | paged_datatable:
12 | path: ../
13 | faker: any
14 | darq: any
15 | intl: any
16 | equatable: any
17 | google_fonts: any
18 | flutter:
19 | sdk: flutter
20 | flutter_localizations:
21 | sdk: flutter
22 |
23 | dev_dependencies:
24 | flutter_test:
25 | sdk: flutter
26 | flutter_lints: ^1.0.0
27 |
28 | flutter:
29 | uses-material-design: true
30 |
--------------------------------------------------------------------------------
/lib/src/sort_model.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | /// SortModel indicates the current field the table is using to sort values.
4 | final class SortModel {
5 | final String fieldName;
6 | final bool descending;
7 |
8 | const SortModel._({required this.fieldName, required this.descending});
9 |
10 | @override
11 | int get hashCode => Object.hash(fieldName, descending);
12 |
13 | @override
14 | bool operator ==(Object other) =>
15 | identical(other, this) ||
16 | (other is SortModel &&
17 | other.fieldName == fieldName &&
18 | other.descending == descending);
19 | }
20 |
--------------------------------------------------------------------------------
/example/macos/Runner/Configs/Warnings.xcconfig:
--------------------------------------------------------------------------------
1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
2 | GCC_WARN_UNDECLARED_SELECTOR = YES
3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
6 | CLANG_WARN_PRAGMA_PACK = YES
7 | CLANG_WARN_STRICT_PROTOTYPES = YES
8 | CLANG_WARN_COMMA = YES
9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES
10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
12 | GCC_WARN_SHADOW = YES
13 | CLANG_WARN_UNREACHABLE_CODE = YES
14 |
--------------------------------------------------------------------------------
/example/macos/Runner/Configs/AppInfo.xcconfig:
--------------------------------------------------------------------------------
1 | // Application-level settings for the Runner target.
2 | //
3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
4 | // future. If not, the values below would default to using the project name when this becomes a
5 | // 'flutter create' template.
6 |
7 | // The application's name. By default this is also the title of the Flutter window.
8 | PRODUCT_NAME = example
9 |
10 | // The application's bundle identifier
11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example
12 |
13 | // The copyright displayed in application information
14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved.
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 | .fvm/flutter_sdk
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
26 | /pubspec.lock
27 | **/doc/api/
28 | .dart_tool/
29 | .packages
30 | .fvm/
31 | .vscode/
32 | build/
33 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: paged_datatable
2 | description: A brand new way of creating paginated DataTables in Flutter with sorting and filters
3 | version: 2.1.1
4 | homepage: https://github.com/tomasweigenast/paged-datatable
5 |
6 | environment:
7 | sdk: "^3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 | flutter_localizations:
13 | sdk: flutter
14 | intl: ^0.19.0
15 |
16 | dev_dependencies:
17 | build_runner: ^2.4.8
18 | flutter_lints: ^4.0.0
19 | intl_utils: ^2.8.5
20 |
21 | flutter:
22 | generate: true
23 |
24 | flutter_intl:
25 | enabled: true
26 | class_name: PagedDataTableLocalization
27 | main_locale: en
28 | arb_dir: lib/l10n
29 | output_dir: lib/l10n/generated
30 | use_deferred_loading: false
31 |
--------------------------------------------------------------------------------
/example/macos/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - FlutterMacOS (1.0.0)
3 | - path_provider_foundation (0.0.1):
4 | - Flutter
5 | - FlutterMacOS
6 |
7 | DEPENDENCIES:
8 | - FlutterMacOS (from `Flutter/ephemeral`)
9 | - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
10 |
11 | EXTERNAL SOURCES:
12 | FlutterMacOS:
13 | :path: Flutter/ephemeral
14 | path_provider_foundation:
15 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
16 |
17 | SPEC CHECKSUMS:
18 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
19 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
20 |
21 | PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
22 |
23 | COCOAPODS: 1.15.2
24 |
--------------------------------------------------------------------------------
/lib/src/column_format.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | abstract interface class ColumnFormat {
4 | const ColumnFormat();
5 |
6 | Widget transform(Widget cell);
7 | }
8 |
9 | /// Applies a numeric format to the column. That is, the cell content is aligned to the right.
10 | class NumericColumnFormat implements ColumnFormat {
11 | const NumericColumnFormat();
12 |
13 | @override
14 | Widget transform(Widget cell) =>
15 | Align(alignment: Alignment.centerRight, child: cell);
16 | }
17 |
18 | /// Applies [alignment] to the cell content.
19 | class AlignColumnFormat implements ColumnFormat {
20 | final AlignmentGeometry alignment;
21 |
22 | const AlignColumnFormat({required this.alignment});
23 |
24 | @override
25 | Widget transform(Widget cell) => Align(alignment: alignment, child: cell);
26 | }
27 |
--------------------------------------------------------------------------------
/lib/src/table_controller_notifier.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:paged_datatable/paged_datatable.dart';
3 |
4 | final class TableControllerProvider, T>
5 | extends InheritedWidget {
6 | final PagedDataTableController controller;
7 |
8 | const TableControllerProvider(
9 | {required this.controller, required super.child, super.key});
10 |
11 | @override
12 | bool updateShouldNotify(covariant InheritedWidget oldWidget) =>
13 | false; //controller != (oldWidget as TableControllerProvider).controller;
14 |
15 | static PagedDataTableController of, T>(
16 | BuildContext context) =>
17 | context
18 | .dependOnInheritedWidgetOfExactType>()!
19 | .controller;
20 | }
21 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | **/ios/Flutter/.last_build_id
26 | .dart_tool/
27 | .flutter-plugins
28 | .flutter-plugins-dependencies
29 | .packages
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Web related
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Android Studio will place build artifacts here
43 | /android/app/debug
44 | /android/app/profile
45 | /android/app/release
46 |
--------------------------------------------------------------------------------
/example/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "short_name": "example",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "A new Flutter project.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [
12 | {
13 | "src": "icons/Icon-192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "icons/Icon-512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | },
22 | {
23 | "src": "icons/Icon-maskable-192.png",
24 | "sizes": "192x192",
25 | "type": "image/png",
26 | "purpose": "maskable"
27 | },
28 | {
29 | "src": "icons/Icon-maskable-512.png",
30 | "sizes": "512x512",
31 | "type": "image/png",
32 | "purpose": "maskable"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/example/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1"
8 | channel: "stable"
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
17 | base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
18 | - platform: web
19 | create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
20 | base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tomás Weigenast
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/l10n/intl_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "showFilterMenuTooltip": "Filter",
3 | "filterByTitle": "Filter by",
4 | "applyFilterButtonText": "Apply",
5 | "cancelFilteringButtonText": "Cancel",
6 | "removeAllFiltersButtonText": "Remove",
7 | "editableColumnSaveChangesButtonText": "Save changes",
8 | "editableColumnCancelButtonText": "Cancel",
9 | "removeFilterButtonText": "Remove this filter",
10 | "refreshText": "Refresh",
11 | "refreshedAtText": "Last refreshed at {time}",
12 | "@refreshedAtText": {
13 | "placeholders": {
14 | "time": {}
15 | }
16 | },
17 | "rowsPerPageText": "Rows per page",
18 | "pageIndicatorText": "Page {currentPage}",
19 | "@pageIndicatorText": {
20 | "placeholders": {
21 | "currentPage": {}
22 | }
23 | },
24 | "totalElementsText": "Showing {totalElements} elements",
25 | "@totalElementsText": {
26 | "placeholders": {
27 | "totalElements": {}
28 | }
29 | },
30 | "nextPageButtonText": "Next page",
31 | "previousPageButtonText": "Previous page",
32 | "noItemsFoundText": "No items found"
33 | }
--------------------------------------------------------------------------------
/lib/l10n/intl_es.arb:
--------------------------------------------------------------------------------
1 | {
2 | "showFilterMenuTooltip": "Filtrar",
3 | "filterByTitle": "Filtrar por",
4 | "applyFilterButtonText": "Aplicar",
5 | "cancelFilteringButtonText": "Cancelar",
6 | "removeAllFiltersButtonText": "Borrar",
7 | "removeFilterButtonText": "Quitar este filtro",
8 | "refreshText": "Actualizar",
9 | "editableColumnSaveChangesButtonText": "Guardar cambios",
10 | "editableColumnCancelButtonText": "Cancelar",
11 | "refreshedAtText": "Ultima actualización {time}",
12 | "@refreshedAtText": {
13 | "placeholders": {
14 | "time": {}
15 | }
16 | },
17 | "rowsPerPageText": "Filas por página",
18 | "pageIndicatorText": "Página {currentPage}",
19 | "@pageIndicatorText": {
20 | "placeholders": {
21 | "currentPage": {}
22 | }
23 | },
24 | "totalElementsText": "Mostrando {totalElements} elementos",
25 | "@totalElementsText": {
26 | "placeholders": {
27 | "totalElements": {}
28 | }
29 | },
30 | "nextPageButtonText": "Siguiente",
31 | "previousPageButtonText": "Anterior",
32 | "noItemsFoundText": "No se han encontrado elementos"
33 | }
--------------------------------------------------------------------------------
/lib/l10n/intl_it.arb:
--------------------------------------------------------------------------------
1 | {
2 | "showFilterMenuTooltip": "Filtro",
3 | "filterByTitle": "Filtra per",
4 | "applyFilterButtonText": "Applica",
5 | "cancelFilteringButtonText": "Annulla",
6 | "removeAllFiltersButtonText": "Rimuovi",
7 | "editableColumnSaveChangesButtonText": "Salva",
8 | "editableColumnCancelButtonText": "Annulla",
9 | "removeFilterButtonText": "Rimuovi filtro",
10 | "refreshText": "Aggiorna",
11 | "refreshedAtText": "Ultimo aggiornamento alle {time}",
12 | "@refreshedAtText": {
13 | "placeholders": {
14 | "time": {}
15 | }
16 | },
17 | "rowsPerPageText": "Righe per pagina",
18 | "pageIndicatorText": "Pagina {currentPage}",
19 | "@pageIndicatorText": {
20 | "placeholders": {
21 | "currentPage": {}
22 | }
23 | },
24 | "totalElementsText": "Visualizzazione di {totalElements} elementi",
25 | "@totalElementsText": {
26 | "placeholders": {
27 | "totalElements": {}
28 | }
29 | },
30 | "nextPageButtonText": "Pagina successiva",
31 | "previousPageButtonText": "Pagina precedente",
32 | "noItemsFoundText": "Nessun elemento trovato"
33 | }
--------------------------------------------------------------------------------
/example/macos/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | $(PRODUCT_COPYRIGHT)
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/lib/l10n/intl_de.arb:
--------------------------------------------------------------------------------
1 | {
2 | "showFilterMenuTooltip": "Filter",
3 | "filterByTitle": "Filtern nach",
4 | "applyFilterButtonText": "Anwenden",
5 | "cancelFilteringButtonText": "Abbrechen",
6 | "removeAllFiltersButtonText": "Entfernen",
7 | "removeFilterButtonText": "Diesen Filter entfernen",
8 | "refreshText": "Aktualisieren",
9 | "editableColumnSaveChangesButtonText": "Änderungen speichern",
10 | "editableColumnCancelButtonText": "Abbrechen",
11 | "refreshedAtText": "Zuletzt aktualisiert um {time}",
12 | "@refreshedAtText": {
13 | "placeholders": {
14 | "time": {}
15 | }
16 | },
17 | "rowsPerPageText": "Zeilen pro Seite",
18 | "pageIndicatorText": "Seite {currentPage}",
19 | "@pageIndicatorText": {
20 | "placeholders": {
21 | "currentPage": {}
22 | }
23 | },
24 | "totalElementsText": "Zeigt {totalElements} Elemente an",
25 | "@totalElementsText": {
26 | "placeholders": {
27 | "totalElements": {}
28 | }
29 | },
30 | "nextPageButtonText": "Nächste Seite",
31 | "previousPageButtonText": "Vorherige Seite",
32 | "noItemsFoundText": "Keine Elemente gefunden"
33 | }
--------------------------------------------------------------------------------
/lib/src/footer.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:paged_datatable/paged_datatable.dart';
3 |
4 | /// The default footer renderer for [PagedDataTable].
5 | ///
6 | /// It renders the [RefreshTable], [PageSizeSelector], [CurrentPage] and [NavigationButtons] widgets.
7 | class DefaultFooter, T> extends StatelessWidget {
8 | /// An additional widget to render at the left of the footer
9 | final Widget? child;
10 |
11 | const DefaultFooter({this.child, super.key});
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return Row(
16 | children: [
17 | if (child != null) Expanded(child: child!) else const Spacer(),
18 | RefreshTable(),
19 | const VerticalDivider(
20 | color: Color(0xFFD6D6D6), width: 3, indent: 10, endIndent: 10),
21 | PageSizeSelector(),
22 | const VerticalDivider(
23 | color: Color(0xFFD6D6D6), width: 3, indent: 10, endIndent: 10),
24 | CurrentPage(),
25 | const VerticalDivider(
26 | color: Color(0xFFD6D6D6), width: 3, indent: 10, endIndent: 10),
27 | NavigationButtons(),
28 | ],
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | example
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/lib/src/filter_model.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | /// Represents the filter model of the table.
4 | ///
5 | /// Calling [Map] conventional methods should be sufficient to try to get values for filters as this class
6 | /// extends Map and because value is dynamic, you can access it like so:
7 | ///
8 | /// ```dart
9 | /// final String? myFilterValue = filterModel["filterId"];
10 | /// ```
11 | ///
12 | /// If the filter does not have any value set, this won't throw an error as it will return null and,
13 | /// if there is a value, it is in fact an String, so the conversion is made by Dart automatically.
14 | ///
15 | /// Obviously if filter's value is not an String an error will be thrown.
16 | final class FilterModel extends UnmodifiableMapBase {
17 | final Map _inner;
18 |
19 | FilterModel._(this._inner);
20 |
21 | @override
22 | operator [](Object? key) => _inner[key];
23 |
24 | @override
25 | Iterable get keys => _inner.keys;
26 |
27 | /// Tries to get the value of [filterId] and converts it to [T].
28 | ///
29 | /// If the filter does not have a value set, [orElse] will get called if not null, otherwise an
30 | /// error will be thrown.
31 | T valueAs(String filterId, {T Function()? orElse}) {
32 | final value = _inner[filterId];
33 | if (value == null) {
34 | if (orElse != null) return orElse();
35 | throw StateError("Value for filter $filterId was not found.");
36 | }
37 |
38 | return value as T;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/example/macos/Podfile:
--------------------------------------------------------------------------------
1 | platform :osx, '10.14'
2 |
3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
5 |
6 | project 'Runner', {
7 | 'Debug' => :debug,
8 | 'Profile' => :release,
9 | 'Release' => :release,
10 | }
11 |
12 | def flutter_root
13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
14 | unless File.exist?(generated_xcode_build_settings_path)
15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
16 | end
17 |
18 | File.foreach(generated_xcode_build_settings_path) do |line|
19 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
20 | return matches[1].strip if matches
21 | end
22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
23 | end
24 |
25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
26 |
27 | flutter_macos_podfile_setup
28 |
29 | target 'Runner' do
30 | use_frameworks!
31 | use_modular_headers!
32 |
33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
34 | target 'RunnerTests' do
35 | inherit! :search_paths
36 | end
37 | end
38 |
39 | post_install do |installer|
40 | installer.pods_project.targets.each do |target|
41 | flutter_additional_macos_build_settings(target)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "app_icon_16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "app_icon_32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "app_icon_32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "app_icon_64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "app_icon_128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "app_icon_256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "app_icon_256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "app_icon_512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "app_icon_512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "app_icon_1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/l10n/generated/intl/messages_en.dart:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
2 | // This is a library that provides messages for a en locale. All the
3 | // messages from the main program should be duplicated here with the same
4 | // function name.
5 |
6 | // Ignore issues from commonly used lints in this file.
7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
12 |
13 | import 'package:intl/intl.dart';
14 | import 'package:intl/message_lookup_by_library.dart';
15 |
16 | final messages = new MessageLookup();
17 |
18 | typedef String MessageIfAbsent(String messageStr, List args);
19 |
20 | class MessageLookup extends MessageLookupByLibrary {
21 | String get localeName => 'en';
22 |
23 | static String m0(currentPage) => "Page ${currentPage}";
24 |
25 | static String m1(time) => "Last refreshed at ${time}";
26 |
27 | static String m2(totalElements) => "Showing ${totalElements} elements";
28 |
29 | final messages = _notInlinedMessages(_notInlinedMessages);
30 | static Map _notInlinedMessages(_) => {
31 | "applyFilterButtonText": MessageLookupByLibrary.simpleMessage("Apply"),
32 | "cancelFilteringButtonText":
33 | MessageLookupByLibrary.simpleMessage("Cancel"),
34 | "editableColumnCancelButtonText":
35 | MessageLookupByLibrary.simpleMessage("Cancel"),
36 | "editableColumnSaveChangesButtonText":
37 | MessageLookupByLibrary.simpleMessage("Save changes"),
38 | "filterByTitle": MessageLookupByLibrary.simpleMessage("Filter by"),
39 | "nextPageButtonText": MessageLookupByLibrary.simpleMessage("Next page"),
40 | "noItemsFoundText":
41 | MessageLookupByLibrary.simpleMessage("No items found"),
42 | "pageIndicatorText": m0,
43 | "previousPageButtonText":
44 | MessageLookupByLibrary.simpleMessage("Previous page"),
45 | "refreshText": MessageLookupByLibrary.simpleMessage("Refresh"),
46 | "refreshedAtText": m1,
47 | "removeAllFiltersButtonText":
48 | MessageLookupByLibrary.simpleMessage("Remove"),
49 | "removeFilterButtonText":
50 | MessageLookupByLibrary.simpleMessage("Remove this filter"),
51 | "rowsPerPageText":
52 | MessageLookupByLibrary.simpleMessage("Rows per page"),
53 | "showFilterMenuTooltip": MessageLookupByLibrary.simpleMessage("Filter"),
54 | "totalElementsText": m2
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/lib/l10n/generated/intl/messages_all.dart:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
2 | // This is a library that looks up messages for specific locales by
3 | // delegating to the appropriate library.
4 |
5 | // Ignore issues from commonly used lints in this file.
6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new
7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment
9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
10 | // ignore_for_file:comment_references
11 |
12 | import 'dart:async';
13 |
14 | import 'package:flutter/foundation.dart';
15 | import 'package:intl/intl.dart';
16 | import 'package:intl/message_lookup_by_library.dart';
17 | import 'package:intl/src/intl_helpers.dart';
18 |
19 | import 'messages_de.dart' as messages_de;
20 | import 'messages_en.dart' as messages_en;
21 | import 'messages_es.dart' as messages_es;
22 | import 'messages_it.dart' as messages_it;
23 |
24 | typedef Future LibraryLoader();
25 | Map _deferredLibraries = {
26 | 'de': () => new SynchronousFuture(null),
27 | 'en': () => new SynchronousFuture(null),
28 | 'es': () => new SynchronousFuture(null),
29 | 'it': () => new SynchronousFuture(null),
30 | };
31 |
32 | MessageLookupByLibrary? _findExact(String localeName) {
33 | switch (localeName) {
34 | case 'de':
35 | return messages_de.messages;
36 | case 'en':
37 | return messages_en.messages;
38 | case 'es':
39 | return messages_es.messages;
40 | case 'it':
41 | return messages_it.messages;
42 | default:
43 | return null;
44 | }
45 | }
46 |
47 | /// User programs should call this before using [localeName] for messages.
48 | Future initializeMessages(String localeName) {
49 | var availableLocale = Intl.verifiedLocale(
50 | localeName, (locale) => _deferredLibraries[locale] != null,
51 | onFailure: (_) => null);
52 | if (availableLocale == null) {
53 | return new SynchronousFuture(false);
54 | }
55 | var lib = _deferredLibraries[availableLocale];
56 | lib == null ? new SynchronousFuture(false) : lib();
57 | initializeInternalMessageLookup(() => new CompositeMessageLookup());
58 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
59 | return new SynchronousFuture(true);
60 | }
61 |
62 | bool _messagesExistFor(String locale) {
63 | try {
64 | return _findExact(locale) != null;
65 | } catch (e) {
66 | return false;
67 | }
68 | }
69 |
70 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
71 | var actualLocale =
72 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
73 | if (actualLocale == null) return null;
74 | return _findExact(actualLocale);
75 | }
76 |
--------------------------------------------------------------------------------
/lib/l10n/generated/intl/messages_es.dart:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
2 | // This is a library that provides messages for a es locale. All the
3 | // messages from the main program should be duplicated here with the same
4 | // function name.
5 |
6 | // Ignore issues from commonly used lints in this file.
7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
12 |
13 | import 'package:intl/intl.dart';
14 | import 'package:intl/message_lookup_by_library.dart';
15 |
16 | final messages = new MessageLookup();
17 |
18 | typedef String MessageIfAbsent(String messageStr, List args);
19 |
20 | class MessageLookup extends MessageLookupByLibrary {
21 | String get localeName => 'es';
22 |
23 | static String m0(currentPage) => "Página ${currentPage}";
24 |
25 | static String m1(time) => "Ultima actualización ${time}";
26 |
27 | static String m2(totalElements) => "Mostrando ${totalElements} elementos";
28 |
29 | final messages = _notInlinedMessages(_notInlinedMessages);
30 | static Map _notInlinedMessages(_) => {
31 | "applyFilterButtonText":
32 | MessageLookupByLibrary.simpleMessage("Aplicar"),
33 | "cancelFilteringButtonText":
34 | MessageLookupByLibrary.simpleMessage("Cancelar"),
35 | "editableColumnCancelButtonText":
36 | MessageLookupByLibrary.simpleMessage("Cancelar"),
37 | "editableColumnSaveChangesButtonText":
38 | MessageLookupByLibrary.simpleMessage("Guardar cambios"),
39 | "filterByTitle": MessageLookupByLibrary.simpleMessage("Filtrar por"),
40 | "nextPageButtonText": MessageLookupByLibrary.simpleMessage("Siguiente"),
41 | "noItemsFoundText": MessageLookupByLibrary.simpleMessage(
42 | "No se han encontrado elementos"),
43 | "pageIndicatorText": m0,
44 | "previousPageButtonText":
45 | MessageLookupByLibrary.simpleMessage("Anterior"),
46 | "refreshText": MessageLookupByLibrary.simpleMessage("Actualizar"),
47 | "refreshedAtText": m1,
48 | "removeAllFiltersButtonText":
49 | MessageLookupByLibrary.simpleMessage("Borrar"),
50 | "removeFilterButtonText":
51 | MessageLookupByLibrary.simpleMessage("Quitar este filtro"),
52 | "rowsPerPageText":
53 | MessageLookupByLibrary.simpleMessage("Filas por página"),
54 | "showFilterMenuTooltip":
55 | MessageLookupByLibrary.simpleMessage("Filtrar"),
56 | "totalElementsText": m2
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/lib/l10n/generated/intl/messages_it.dart:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
2 | // This is a library that provides messages for a it locale. All the
3 | // messages from the main program should be duplicated here with the same
4 | // function name.
5 |
6 | // Ignore issues from commonly used lints in this file.
7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
12 |
13 | import 'package:intl/intl.dart';
14 | import 'package:intl/message_lookup_by_library.dart';
15 |
16 | final messages = new MessageLookup();
17 |
18 | typedef String MessageIfAbsent(String messageStr, List args);
19 |
20 | class MessageLookup extends MessageLookupByLibrary {
21 | String get localeName => 'it';
22 |
23 | static String m0(currentPage) => "Pagina ${currentPage}";
24 |
25 | static String m1(time) => "Ultimo aggiornamento alle ${time}";
26 |
27 | static String m2(totalElements) =>
28 | "Visualizzazione di ${totalElements} elementi";
29 |
30 | final messages = _notInlinedMessages(_notInlinedMessages);
31 | static Map _notInlinedMessages(_) => {
32 | "applyFilterButtonText":
33 | MessageLookupByLibrary.simpleMessage("Applica"),
34 | "cancelFilteringButtonText":
35 | MessageLookupByLibrary.simpleMessage("Annulla"),
36 | "editableColumnCancelButtonText":
37 | MessageLookupByLibrary.simpleMessage("Annulla"),
38 | "editableColumnSaveChangesButtonText":
39 | MessageLookupByLibrary.simpleMessage("Salva"),
40 | "filterByTitle": MessageLookupByLibrary.simpleMessage("Filtra per"),
41 | "nextPageButtonText":
42 | MessageLookupByLibrary.simpleMessage("Pagina successiva"),
43 | "noItemsFoundText":
44 | MessageLookupByLibrary.simpleMessage("Nessun elemento trovato"),
45 | "pageIndicatorText": m0,
46 | "previousPageButtonText":
47 | MessageLookupByLibrary.simpleMessage("Pagina precedente"),
48 | "refreshText": MessageLookupByLibrary.simpleMessage("Aggiorna"),
49 | "refreshedAtText": m1,
50 | "removeAllFiltersButtonText":
51 | MessageLookupByLibrary.simpleMessage("Rimuovi"),
52 | "removeFilterButtonText":
53 | MessageLookupByLibrary.simpleMessage("Rimuovi filtro"),
54 | "rowsPerPageText":
55 | MessageLookupByLibrary.simpleMessage("Righe per pagina"),
56 | "showFilterMenuTooltip": MessageLookupByLibrary.simpleMessage("Filtro"),
57 | "totalElementsText": m2
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/lib/l10n/generated/intl/messages_de.dart:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
2 | // This is a library that provides messages for a de locale. All the
3 | // messages from the main program should be duplicated here with the same
4 | // function name.
5 |
6 | // Ignore issues from commonly used lints in this file.
7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
12 |
13 | import 'package:intl/intl.dart';
14 | import 'package:intl/message_lookup_by_library.dart';
15 |
16 | final messages = new MessageLookup();
17 |
18 | typedef String MessageIfAbsent(String messageStr, List args);
19 |
20 | class MessageLookup extends MessageLookupByLibrary {
21 | String get localeName => 'de';
22 |
23 | static String m0(currentPage) => "Seite ${currentPage}";
24 |
25 | static String m1(time) => "Zuletzt aktualisiert um ${time}";
26 |
27 | static String m2(totalElements) => "Zeigt ${totalElements} Elemente an";
28 |
29 | final messages = _notInlinedMessages(_notInlinedMessages);
30 | static Map _notInlinedMessages(_) => {
31 | "applyFilterButtonText":
32 | MessageLookupByLibrary.simpleMessage("Anwenden"),
33 | "cancelFilteringButtonText":
34 | MessageLookupByLibrary.simpleMessage("Abbrechen"),
35 | "editableColumnCancelButtonText":
36 | MessageLookupByLibrary.simpleMessage("Abbrechen"),
37 | "editableColumnSaveChangesButtonText":
38 | MessageLookupByLibrary.simpleMessage("Änderungen speichern"),
39 | "filterByTitle": MessageLookupByLibrary.simpleMessage("Filtern nach"),
40 | "nextPageButtonText":
41 | MessageLookupByLibrary.simpleMessage("Nächste Seite"),
42 | "noItemsFoundText":
43 | MessageLookupByLibrary.simpleMessage("Keine Elemente gefunden"),
44 | "pageIndicatorText": m0,
45 | "previousPageButtonText":
46 | MessageLookupByLibrary.simpleMessage("Vorherige Seite"),
47 | "refreshText": MessageLookupByLibrary.simpleMessage("Aktualisieren"),
48 | "refreshedAtText": m1,
49 | "removeAllFiltersButtonText":
50 | MessageLookupByLibrary.simpleMessage("Entfernen"),
51 | "removeFilterButtonText":
52 | MessageLookupByLibrary.simpleMessage("Diesen Filter entfernen"),
53 | "rowsPerPageText":
54 | MessageLookupByLibrary.simpleMessage("Zeilen pro Seite"),
55 | "showFilterMenuTooltip": MessageLookupByLibrary.simpleMessage("Filter"),
56 | "totalElementsText": m2
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/lib/src/column_size.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math' as math;
2 |
3 | /// Indicates the size of a table column
4 | sealed class ColumnSize {
5 | const ColumnSize();
6 |
7 | /// A flag that indicates if the column defines a fixed, constant size.
8 | bool get isFixed => false;
9 |
10 | /// Returns the size of the column as fractional units.
11 | double get fraction => 0.0;
12 |
13 | /// The function used to calculate the constraints for the column given the [availableWidth].
14 | double calculateConstraints(double availableWidth);
15 | }
16 |
17 | /// Indicates a fixed size of a column. If the content of a cell does not fit, it will be wrapped
18 | final class FixedColumnSize extends ColumnSize {
19 | final double size;
20 |
21 | const FixedColumnSize(this.size);
22 |
23 | @override
24 | int get hashCode => size.hashCode;
25 |
26 | @override
27 | bool operator ==(Object other) =>
28 | other is FixedColumnSize && other.size == size;
29 |
30 | @override
31 | bool get isFixed => true;
32 |
33 | @override
34 | double calculateConstraints(double availableWidth) => size;
35 | }
36 |
37 | /// Indicates a fraction size of a column. That is, a column that takes a fraction of the available viewport.
38 | final class FractionalColumnSize extends ColumnSize {
39 | final double _fraction;
40 |
41 | const FractionalColumnSize(double fraction)
42 | : _fraction = fraction,
43 | assert(fraction > 0, "Fraction cannot be less than or equal to zero.");
44 |
45 | @override
46 | int get hashCode => fraction.hashCode;
47 |
48 | @override
49 | bool operator ==(Object other) =>
50 | other is FractionalColumnSize && other.fraction == fraction;
51 |
52 | @override
53 | double get fraction => _fraction;
54 |
55 | @override
56 | double calculateConstraints(double availableWidth) =>
57 | availableWidth * fraction;
58 | }
59 |
60 | /// Indicates that a column will take the remaining space in the viewport.
61 | final class RemainingColumnSize extends ColumnSize {
62 | const RemainingColumnSize();
63 |
64 | @override
65 | double calculateConstraints(double availableWidth) =>
66 | math.max(0.0, availableWidth);
67 | }
68 |
69 | /// A column size that uses the maximum value of two provided constraints.
70 | final class MaxColumnSize extends ColumnSize {
71 | final ColumnSize a, b;
72 |
73 | const MaxColumnSize(this.a, this.b);
74 |
75 | @override
76 | bool get isFixed => a.isFixed && b.isFixed;
77 |
78 | @override
79 | double get fraction => math.max(a.fraction, b.fraction);
80 |
81 | @override
82 | int get hashCode => Object.hash(a.hashCode, b.hashCode);
83 |
84 | @override
85 | bool operator ==(Object other) =>
86 | other is MaxColumnSize && other.a == a && other.b == b;
87 |
88 | @override
89 | double calculateConstraints(double availableWidth) => math.max(
90 | a.calculateConstraints(availableWidth),
91 | b.calculateConstraints(availableWidth));
92 | }
93 |
--------------------------------------------------------------------------------
/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/lib/src/table_view_rows.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | /// A row renderer that uses TableView
4 | /*class _TableViewRows, T> extends StatefulWidget {
5 | final TableController controller;
6 | final ScrollController horizontalController, verticalController;
7 | final int fixedColumnCount;
8 | final List columns;
9 |
10 | const _TableViewRows(
11 | {required this.controller,
12 | required this.horizontalController,
13 | required this.verticalController,
14 | required this.columns,
15 | required this.fixedColumnCount});
16 |
17 | @override
18 | State<_TableViewRows> createState() => _TableViewRowsState();
19 | }
20 |
21 | class _TableViewRowsState, T> extends State<_TableViewRows> {
22 | late PagedDataTableThemeData theme;
23 | late FixedTableSpanExtent rowSpanExtent;
24 |
25 | late _TableState tableState;
26 |
27 | @override
28 | void initState() {
29 | super.initState();
30 |
31 | tableState = widget.controller._state;
32 | widget.controller.addListener(_onControllerChanged);
33 | }
34 |
35 | @override
36 | Widget build(BuildContext context) {
37 | theme = PagedDataTableTheme.of(context);
38 | rowSpanExtent = FixedTableSpanExtent(theme.rowHeight);
39 |
40 | return Opacity(
41 | opacity: tableState == _TableState.idle ? 1 : 0.3,
42 | child: DefaultTextStyle(
43 | style: theme.cellTextStyle,
44 | child: Scrollbar(
45 | controller: widget.verticalController,
46 | thumbVisibility: true,
47 | child: Scrollbar(
48 | controller: widget.horizontalController,
49 | thumbVisibility: true,
50 | child: TableView.builder(
51 | pinnedColumnCount: widget.fixedColumnCount,
52 | verticalDetails: ScrollableDetails.vertical(controller: widget.verticalController),
53 | horizontalDetails: ScrollableDetails.horizontal(controller: widget.horizontalController),
54 | columnCount: widget.columns.length,
55 | rowCount: widget.controller.totalItems,
56 | rowBuilder: _buildRowSpan,
57 | columnBuilder: _buildColumnSpan,
58 | cellBuilder: _buildCell,
59 | ),
60 | ),
61 | ),
62 | ),
63 | );
64 | }
65 |
66 | TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
67 | final itemIndex = vicinity.row;
68 | final column = widget.columns[vicinity.column];
69 | final item = widget.controller._currentDataset[itemIndex];
70 |
71 | return TableViewCell(
72 | child: switch (column) {
73 | TableColumn(:final cellBuilder) => Padding(
74 | padding: theme.padding,
75 | child: column.format.transform(
76 | Padding(
77 | padding: theme.cellPadding,
78 | child: cellBuilder(context, item, itemIndex),
79 | ),
80 | ),
81 | ),
82 | _ => throw UnimplementedError()
83 | },
84 | );
85 | }
86 |
87 | TableSpan _buildColumnSpan(int index) {
88 | final column = widget.columns[index];
89 | return TableSpan(
90 | extent: switch (column.size) {
91 | RemainingColumnSize() => const RemainingTableSpanExtent(),
92 | FixedColumnSize(:final size) => FixedTableSpanExtent(size),
93 | FractionalColumnSize(:final fraction) => FractionalTableSpanExtent(fraction)
94 | },
95 | padding: const TableSpanPadding.all(0),
96 | );
97 | }
98 |
99 | TableSpan _buildRowSpan(int index) {
100 | final TableSpanDecoration decoration = TableSpanDecoration(
101 | color: theme.cellColor?.call(index),
102 | border: const TableSpanBorder(
103 | trailing: BorderSide(width: 1, color: Color(0xFFD6D6D6)),
104 | ),
105 | );
106 |
107 | return TableSpan(
108 | backgroundDecoration: decoration,
109 | cursor: SystemMouseCursors.click,
110 | extent: rowSpanExtent,
111 | recognizerFactories: {
112 | TapGestureRecognizer: GestureRecognizerFactoryWithHandlers(
113 | () => TapGestureRecognizer(),
114 | (TapGestureRecognizer t) => t.onTap = () => debugPrint('Tap row $index'),
115 | ),
116 | },
117 | );
118 | }
119 |
120 | void _onControllerChanged() {
121 | if (tableState != widget.controller._state) {
122 | setState(() {
123 | tableState = widget.controller._state;
124 | });
125 | }
126 | }
127 |
128 | @override
129 | void dispose() {
130 | super.dispose();
131 | widget.controller.removeListener(_onControllerChanged);
132 | }
133 | }
134 | */
135 |
--------------------------------------------------------------------------------
/lib/src/double_list_rows.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | /// A Row renderer that uses two lists for two directional scrolling
4 | class _DoubleListRows, T> extends StatefulWidget {
5 | final List columns;
6 | final ScrollController horizontalController;
7 | final int fixedColumnCount;
8 | final PagedDataTableController controller;
9 | final PagedDataTableConfiguration configuration;
10 | final List sizes;
11 |
12 | const _DoubleListRows({
13 | required this.columns,
14 | required this.fixedColumnCount,
15 | required this.horizontalController,
16 | required this.controller,
17 | required this.configuration,
18 | required this.sizes,
19 | });
20 |
21 | @override
22 | State createState() => _DoubleListRowsState();
23 | }
24 |
25 | class _DoubleListRowsState, T>
26 | extends State<_DoubleListRows> {
27 | final scrollControllerGroup = LinkedScrollControllerGroup();
28 | late final fixedController = scrollControllerGroup.addAndGet();
29 | late final normalController = scrollControllerGroup.addAndGet();
30 |
31 | late _TableState state;
32 |
33 | @override
34 | void initState() {
35 | super.initState();
36 |
37 | state = widget.controller._state;
38 | widget.controller.addListener(() {
39 | setState(() {});
40 | });
41 | }
42 |
43 | @override
44 | Widget build(BuildContext context) {
45 | final theme = PagedDataTableTheme.of(context);
46 |
47 | return DefaultTextStyle(
48 | style: theme.cellTextStyle,
49 | child: ScrollConfiguration(
50 | behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
51 | child: Opacity(
52 | opacity: widget.controller._state == _TableState.idle ? 1 : 0.5,
53 | child: Scrollbar(
54 | thumbVisibility: theme.verticalScrollbarVisibility,
55 | controller: normalController,
56 | child: Row(
57 | children: [
58 | SizedBox(
59 | width: widget.sizes
60 | .take(widget.fixedColumnCount)
61 | .fold(0.0, (a, b) => a! + b),
62 | child: ListView.separated(
63 | primary: false,
64 | controller: fixedController,
65 | itemCount: widget.controller._totalItems,
66 | separatorBuilder: (_, __) =>
67 | const Divider(height: 0, color: Color(0xFFD6D6D6)),
68 | itemBuilder: (context, index) => _FixedPartRow(
69 | index: index,
70 | fixedColumnCount: widget.fixedColumnCount,
71 | sizes: widget.sizes,
72 | columns: widget.columns,
73 | ),
74 | ),
75 | ),
76 | Expanded(
77 | child: Scrollbar(
78 | thumbVisibility: theme.horizontalScrollbarVisibility,
79 | controller: widget.horizontalController,
80 | child: ListView(
81 | controller: widget.horizontalController,
82 | scrollDirection: Axis.horizontal,
83 | children: [
84 | ConstrainedBox(
85 | constraints: BoxConstraints(
86 | maxWidth: widget.sizes
87 | .skip(widget.fixedColumnCount)
88 | .fold(0.0, (a, b) => a + b)),
89 | child: ListView.separated(
90 | controller: normalController,
91 | itemCount: widget.controller._totalItems,
92 | separatorBuilder: (_, __) => const Divider(
93 | height: 0, color: Color(0xFFD6D6D6)),
94 | itemBuilder: (context, index) =>
95 | _VariablePartRow(
96 | sizes: widget.sizes,
97 | index: index,
98 | fixedColumnCount: widget.fixedColumnCount,
99 | columns: widget.columns,
100 | ),
101 | ),
102 | ),
103 | ],
104 | ),
105 | ),
106 | ),
107 | ],
108 | ),
109 | ),
110 | ),
111 | ),
112 | );
113 | }
114 |
115 | @override
116 | void dispose() {
117 | super.dispose();
118 |
119 | normalController.dispose();
120 | fixedController.dispose();
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/src/filter_widgets.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | class _DateTimePicker extends StatefulWidget {
4 | final DateTime firstDate, lastDate;
5 | final DateTime? initialDate;
6 | final DatePickerMode initialDatePickerMode;
7 | final DatePickerEntryMode initialEntryMode;
8 | final bool Function(DateTime)? selectableDayPredicate;
9 | final DateFormat dateFormat;
10 | final DateTime? value;
11 | final void Function(DateTime) onChanged;
12 | final InputDecoration inputDecoration;
13 | final String name;
14 |
15 | const _DateTimePicker({
16 | required this.firstDate,
17 | required this.lastDate,
18 | required this.initialDate,
19 | required this.initialDatePickerMode,
20 | required this.initialEntryMode,
21 | required this.selectableDayPredicate,
22 | required this.dateFormat,
23 | required this.value,
24 | required this.onChanged,
25 | required this.inputDecoration,
26 | required this.name,
27 | });
28 |
29 | @override
30 | State createState() => _DateTimePickerState();
31 | }
32 |
33 | class _DateTimePickerState extends State<_DateTimePicker> {
34 | late final TextEditingController textController;
35 |
36 | @override
37 | void initState() {
38 | super.initState();
39 |
40 | textController = TextEditingController(
41 | text: widget.value == null
42 | ? null
43 | : widget.dateFormat.format(widget.value!));
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return TextFormField(
49 | readOnly: true,
50 | controller: textController,
51 | decoration: widget.inputDecoration.copyWith(
52 | labelText: widget.name,
53 | ),
54 | onTap: () async {
55 | final DateTime? result = await showDatePicker(
56 | context: context,
57 | firstDate: widget.firstDate,
58 | lastDate: widget.lastDate,
59 | initialDate: widget.initialDate,
60 | initialDatePickerMode: widget.initialDatePickerMode,
61 | currentDate: widget.value,
62 | initialEntryMode: widget.initialEntryMode,
63 | selectableDayPredicate: widget.selectableDayPredicate,
64 | );
65 |
66 | if (result != null) {
67 | textController.text = widget.dateFormat.format(result);
68 | widget.onChanged(result);
69 | }
70 | },
71 | );
72 | }
73 |
74 | @override
75 | void dispose() {
76 | super.dispose();
77 | textController.dispose();
78 | }
79 | }
80 |
81 | class _DateRangePicker extends StatefulWidget {
82 | final DateTime firstDate, lastDate;
83 | final DateTimeRange? initialDateRange;
84 | final DatePickerMode initialDatePickerMode;
85 | final DatePickerEntryMode initialEntryMode;
86 | final String Function(DateTimeRange) formatter;
87 | final DateTimeRange? value;
88 | final void Function(DateTimeRange) onChanged;
89 | final String name;
90 | final InputDecoration inputDecoration;
91 | final TransitionBuilder? dialogBuilder;
92 |
93 | const _DateRangePicker({
94 | required this.firstDate,
95 | required this.lastDate,
96 | required this.initialDateRange,
97 | required this.value,
98 | required this.initialDatePickerMode,
99 | required this.initialEntryMode,
100 | required this.formatter,
101 | required this.onChanged,
102 | required this.name,
103 | required this.inputDecoration,
104 | required this.dialogBuilder,
105 | });
106 |
107 | @override
108 | State createState() => _DateRangePickerState();
109 | }
110 |
111 | class _DateRangePickerState extends State<_DateRangePicker> {
112 | late final TextEditingController textController;
113 |
114 | @override
115 | void initState() {
116 | super.initState();
117 |
118 | textController = TextEditingController(
119 | text: widget.value == null ? null : widget.formatter(widget.value!));
120 | }
121 |
122 | @override
123 | Widget build(BuildContext context) {
124 | return TextFormField(
125 | readOnly: true,
126 | controller: textController,
127 | decoration: widget.inputDecoration.copyWith(
128 | labelText: widget.name,
129 | ),
130 | onTap: () async {
131 | final DateTimeRange? result = await showDateRangePicker(
132 | context: context,
133 | firstDate: widget.firstDate,
134 | lastDate: widget.lastDate,
135 | currentDate: widget.value?.start,
136 | initialEntryMode: widget.initialEntryMode,
137 | initialDateRange: widget.initialDateRange,
138 | builder: widget.dialogBuilder,
139 | );
140 |
141 | if (result != null) {
142 | textController.text = widget.formatter(result);
143 | widget.onChanged(result);
144 | }
145 | },
146 | );
147 | }
148 |
149 | @override
150 | void dispose() {
151 | super.dispose();
152 | textController.dispose();
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/lib/src/row.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | abstract class _RowBuilder, T> extends StatefulWidget {
4 | final int index;
5 |
6 | const _RowBuilder({required this.index, super.key});
7 |
8 | @override
9 | State createState() => _RowBuilderState();
10 |
11 | List buildColumns(BuildContext context, int index,
12 | PagedDataTableController controller, PagedDataTableThemeData theme);
13 | }
14 |
15 | class _RowBuilderState, T>
16 | extends State<_RowBuilder> {
17 | late final controller = TableControllerProvider.of(context);
18 | late final theme = PagedDataTableTheme.of(context);
19 | bool selected = false;
20 |
21 | @override
22 | void didChangeDependencies() {
23 | super.didChangeDependencies();
24 |
25 | controller.addRowChangeListener(widget.index, _onRowChanged);
26 | setState(() {
27 | selected = controller._selectedRows.contains(widget.index);
28 | });
29 | }
30 |
31 | @override
32 | Widget build(BuildContext context) {
33 | Widget child = Row(
34 | children:
35 | widget.buildColumns(context, widget.index, controller, theme));
36 | var color = theme.rowColor?.call(widget.index);
37 | if (selected && theme.selectedRow != null) {
38 | color = theme.selectedRow;
39 | }
40 | if (color != null) {
41 | child = DecoratedBox(
42 | decoration: BoxDecoration(color: color),
43 | child: child,
44 | );
45 | }
46 |
47 | return SizedBox(height: theme.rowHeight, child: child);
48 | }
49 |
50 | void _onRowChanged(int index, T value) {
51 | if (mounted) {
52 | setState(() {
53 | selected = controller._selectedRows.contains(index);
54 | });
55 | }
56 | }
57 |
58 | @override
59 | void dispose() {
60 | super.dispose();
61 |
62 | controller.removeRowChangeListener(widget.index, _onRowChanged);
63 | }
64 | }
65 |
66 | class _FixedPartRow, T> extends _RowBuilder {
67 | final int fixedColumnCount;
68 | final List columns;
69 | final List sizes;
70 |
71 | const _FixedPartRow({
72 | required super.index,
73 | required this.fixedColumnCount,
74 | required this.columns,
75 | required this.sizes,
76 | super.key,
77 | });
78 |
79 | @override
80 | List buildColumns(
81 | BuildContext context,
82 | int index,
83 | PagedDataTableController controller,
84 | PagedDataTableThemeData theme) {
85 | final item = controller._currentDataset[index];
86 | final list = [];
87 |
88 | for (int i = 0; i < fixedColumnCount; i++) {
89 | final column = columns[i];
90 | final widget = _buildCell(context, index, item, sizes[i], theme, column);
91 | list.add(widget);
92 | }
93 |
94 | return list;
95 | }
96 | }
97 |
98 | class _VariablePartRow, T> extends _RowBuilder {
99 | final List columns;
100 | final int fixedColumnCount;
101 | final List sizes;
102 |
103 | const _VariablePartRow({
104 | required super.index,
105 | required this.fixedColumnCount,
106 | required this.columns,
107 | required this.sizes,
108 | super.key,
109 | });
110 |
111 | @override
112 | List buildColumns(
113 | BuildContext context,
114 | int index,
115 | PagedDataTableController controller,
116 | PagedDataTableThemeData theme) {
117 | final item = controller._currentDataset[index];
118 | final list = [];
119 |
120 | for (int i = fixedColumnCount; i < columns.length; i++) {
121 | final column = columns[i];
122 | final widget = _buildCell(context, index, item, sizes[i], theme, column);
123 | list.add(widget);
124 | }
125 |
126 | return list;
127 | }
128 | }
129 |
130 | Widget _buildCell(BuildContext context, int index, T value, double width,
131 | PagedDataTableThemeData theme, ReadOnlyTableColumn column) {
132 | Widget child = Container(
133 | padding: theme.cellPadding,
134 | margin: theme.padding,
135 | decoration: BoxDecoration(border: theme.cellBorderSide),
136 | child: column.format.transform(column.build(context, value, index)),
137 | );
138 |
139 | child = SizedBox(width: width, child: child);
140 | // switch (column.size) {
141 | // case FixedColumnSize(:final size):
142 | // child = SizedBox(width: size, child: child);
143 | // availableWidth -= size;
144 | // break;
145 | // case FractionalColumnSize(:final fraction):
146 | // final size = totalWidth * fraction;
147 | // child = SizedBox(width: size, child: child);
148 | // availableWidth -= size;
149 | // break;
150 | // case RemainingColumnSize():
151 | // child = SizedBox(width: availableWidth, child: child);
152 | // availableWidth = 0;
153 | // break;
154 | // }
155 |
156 | return child;
157 | }
158 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2.1.1
2 |
3 | - Fix Date- filters not showing `name` attribute as label in the text field.
4 | - Fix FractionalColumnSize does not calculate correctly sizes.
5 |
6 | ## 2.1.0
7 |
8 | - Add Italian locale
9 |
10 | ## 2.0.1
11 |
12 | - Fix a bug in controller that does not reload dataset when applying a sort filter
13 |
14 | ## 2.0.0
15 |
16 | > First public release of paged-datatable v2
17 |
18 | - `intl` was updated to version 0.19.0
19 |
20 | ## 2.0.0-dev.4
21 |
22 | - Update documentation
23 | - Add more controller examples
24 | - Fix several issues
25 |
26 | > The next version will be a complete release. This version can be used in production. I'm waiting for feedback to publish
27 | > a release.
28 |
29 | ## 2.0.0-dev.3
30 |
31 | - Add localizations again.
32 | - Add TextField-like editable columns.
33 | - Add `ProgrammaticTableFilter`.
34 | - Add `DateTimePickerTableFilter` and `DateRangePickerTableFilter`.
35 |
36 | > Keep in mind this is an uncomplete release. Probably the next version will be a complete release.
37 |
38 | ## 2.0.0-dev.2
39 |
40 | - Add filter bar and filters.
41 | - Add controller methods to manage filters.
42 | - Removed PagedDataTableMenu. Now to display a menu, use the `filterBarChild` and a `PopupMenuButton`.
43 | - Improved the calculation of column widths.
44 |
45 | > Keep in mind this is an uncomplete release. I will continue working on this package actively.
46 |
47 | ## 2.0.0-dev.1
48 |
49 | **First prerelease of PagedDataTable v2!**
50 |
51 | - Adds Horizontal scrolling.
52 | - Fixes row selection.
53 | - Adds a better `TableController`.
54 | - Better column sorting.
55 |
56 | > Keep in mind this is an uncomplete release. Filters, intl and cell edition features are disabled. I will continue working on this package
57 | > actively.
58 |
59 | ## 1.4.4
60 |
61 | - Add support for 'de' locale
62 | - Fix initialPageSize is ignored when setup the widget
63 |
64 | ## 1.4.3
65 |
66 | - Update dependencies
67 | - Format dart code
68 |
69 | ## 1.4.2
70 |
71 | - Packages version upgrade
72 |
73 | ## 1.4.1
74 |
75 | - Improve documentation
76 |
77 | ## 1.4.0
78 |
79 | - Remove internal memory cache.
80 | - Display checkbox as the first column if the table allows row selection
81 | - Bug fixes related to theming
82 | - Add pagination methods to controller
83 | - Improve drawing performance
84 | - Better pub.dev documentation incoming
85 |
86 | ## 1.3.0
87 |
88 | - Add option to completely remove the footer.
89 | - Improve `PagedDataTableTheme` documentation.
90 |
91 | ## 1.2.0
92 |
93 | - Upgrade Flutter version
94 | - Make it compatible with Dart 3
95 |
96 | ## 1.1.4
97 |
98 | - Apply dividerColor on every divider line.
99 | - Add buttons theme
100 |
101 | ## 1.1.3
102 |
103 | - Apply dividerColor on every divider line.
104 | - Add buttons theme
105 |
106 | ## 1.1.2
107 |
108 | - Revert old styling
109 |
110 | ## 1.1.1
111 |
112 | - Fix bugs while customizing text styles
113 |
114 | ## 1.1.0
115 |
116 | - Add better customization by placing a common widget or for a specific one using PagedDataTableTheme
117 |
118 | ## 1.0.17
119 |
120 | - Add method to TableController to allow updating a row based on a predicate.
121 |
122 | ## 1.0.16
123 |
124 | - Fix wrong header alignment
125 |
126 | ## 1.0.15
127 |
128 | - Header child now can take less width. Replaced Expanded with Flexible.
129 |
130 | ## 1.0.14
131 |
132 | - Add method to get applied filters from controller
133 |
134 | ## 1.0.13
135 |
136 | - Fix small bug
137 |
138 | ## 1.0.12
139 |
140 | - Add Flexible to every row
141 |
142 | ## 1.0.11
143 |
144 | - Add option to remove the size of the rows
145 |
146 | ## 1.0.10
147 |
148 | - Fix locales
149 | - Fix refresh current dataset
150 | - Add option to remove a row without reloading the entire dataset
151 |
152 | ## 1.0.9
153 |
154 | - Remove header when no filter is added
155 |
156 | ## 1.0.8
157 |
158 | - Added `ProgrammaticTableFilter`
159 |
160 | ## 1.0.7
161 |
162 | - Fix menu overlay is not opening correctly
163 |
164 | ## 1.0.6
165 |
166 | - Fix call stack exceeded
167 |
168 | ## 1.0.5
169 |
170 | - Small bug fixes
171 |
172 | ## 1.0.4
173 |
174 | - Add refresh listener. Listen to a stream and refresh the dataset when it changes
175 | - Add custom row builder
176 |
177 | ## 1.0.3
178 |
179 | - Add option to set default filters
180 |
181 | ## 1.0.2
182 |
183 | - Fix scores
184 |
185 | ## 1.0.1
186 |
187 | - Fix homepage
188 |
189 | ## 1.0.0
190 |
191 | - Rewrite all the code from scratch
192 |
193 | ## 0.4.9
194 |
195 | - Fixes ScrollController
196 |
197 | ## 0.4.8
198 |
199 | - Fixes ScrollController
200 |
201 | ## 0.4.7
202 |
203 | - When the page changes, the scrollbar is animated to the top of the table
204 |
205 | ## 0.4.6
206 |
207 | - Fixes a bug with filters scrollbar
208 |
209 | ## 0.4.4
210 |
211 | - Fixes a timer bug.
212 |
213 | ## 0.4.0
214 |
215 | - Add a custom row builder
216 |
217 | ## 0.3.1
218 |
219 | - Add option to clear selected rows
220 |
221 | ## 0.3.0
222 |
223 | - Add refresh listener
224 | - Add separated widget to build custom filters.
225 |
226 | ## 0.2.0
227 |
228 | - Add widget to build custom filters.
229 |
230 | ## 0.1.0
231 |
232 | - Initial release.
233 |
--------------------------------------------------------------------------------
/example/lib/post.dart:
--------------------------------------------------------------------------------
1 | import 'dart:collection';
2 |
3 | import 'package:darq/darq.dart';
4 | import 'package:faker/faker.dart';
5 | import 'package:flutter/material.dart';
6 |
7 | class Post {
8 | final int id;
9 | String author;
10 | String content;
11 | DateTime createdAt;
12 | bool isEnabled;
13 | int number;
14 | Gender authorGender;
15 |
16 | Post({
17 | required this.id,
18 | required this.author,
19 | required this.content,
20 | required this.createdAt,
21 | required this.isEnabled,
22 | required this.number,
23 | required this.authorGender,
24 | });
25 |
26 | static final Faker _faker = Faker();
27 | factory Post.random({required int id}) {
28 | return Post(
29 | id: id,
30 | author: _faker.person.name(),
31 | content: _faker.lorem.sentences(10).join(". "),
32 | createdAt: _faker.date.dateTime(minYear: 2022, maxYear: 2023),
33 | isEnabled: _faker.randomGenerator.boolean(),
34 | number: faker.randomGenerator.integer(9999),
35 | authorGender: Gender.values[_faker.randomGenerator.integer(3)]);
36 | }
37 |
38 | @override
39 | int get hashCode => id.hashCode;
40 |
41 | @override
42 | bool operator ==(Object other) => other is Post ? other.id == id : false;
43 |
44 | @override
45 | String toString() =>
46 | "Post(id: $id, author: $author, content: ${content.length > 50 ? content.substring(0, 50) + '...' : content}, createdAt: $createdAt, isEnabled: $isEnabled, number: $number, authorGender: $authorGender)";
47 | }
48 |
49 | enum Gender {
50 | male("Male"),
51 | female("Female"),
52 | unespecified("Unspecified");
53 |
54 | const Gender(this.name);
55 |
56 | final String name;
57 | }
58 |
59 | class PostsRepository {
60 | PostsRepository._();
61 |
62 | static final List _backend = [];
63 |
64 | static void generate(int count) {
65 | _backend.clear();
66 | _backend.addAll(List.generate(count, (index) => Post.random(id: index)));
67 | }
68 |
69 | static Future> getPosts(
70 | {required int pageSize,
71 | required String? pageToken,
72 | bool? status,
73 | Gender? gender,
74 | DateTimeRange? between,
75 | String? authorName,
76 | String? searchQuery,
77 | String? sortBy,
78 | bool sortDescending = false}) async {
79 | await Future.delayed(const Duration(seconds: 1));
80 |
81 | // Decode page token
82 | int nextId = pageToken == null ? 0 : int.tryParse(pageToken) ?? 1;
83 |
84 | Iterable query = _backend;
85 |
86 | if (sortBy == null) {
87 | query = query.orderBy((element) => element.id);
88 | } else {
89 | switch (sortBy) {
90 | case "createdAt":
91 | query = sortDescending
92 | ? query.orderByDescending(
93 | (element) => element.createdAt.millisecondsSinceEpoch)
94 | : query.orderBy(
95 | (element) => element.createdAt.millisecondsSinceEpoch);
96 | break;
97 |
98 | case "number":
99 | query = sortDescending
100 | ? query.orderByDescending((element) => element.number)
101 | : query.orderBy((element) => element.number);
102 | break;
103 |
104 | case "author":
105 | query = sortDescending
106 | ? query.orderByDescending((element) => element.author)
107 | : query.orderBy((element) => element.author);
108 | break;
109 |
110 | case "authorGender":
111 | query = sortDescending
112 | ? query.orderByDescending((element) => element.authorGender.name)
113 | : query.orderBy((element) => element.authorGender.name);
114 | break;
115 | }
116 | }
117 |
118 | query = query.where((element) => element.id >= nextId);
119 | if (status != null) {
120 | query = query.where((element) => element.isEnabled == status);
121 | }
122 |
123 | if (gender != null) {
124 | query = query.where((element) => element.authorGender == gender);
125 | }
126 |
127 | if (between != null) {
128 | query = query.where((element) =>
129 | between.start.isBefore(element.createdAt) &&
130 | between.end.isAfter(element.createdAt));
131 | }
132 |
133 | if (authorName != null) {
134 | query = query.where((element) =>
135 | element.author.toLowerCase().contains(authorName.toLowerCase()));
136 | }
137 |
138 | if (searchQuery != null) {
139 | searchQuery = searchQuery.toLowerCase();
140 | query = query.where((element) =>
141 | element.author.toLowerCase().startsWith(searchQuery!) ||
142 | element.content.toLowerCase().contains(searchQuery));
143 | }
144 |
145 | var resultSet = query.take(pageSize + 1).toList();
146 | String? nextPageToken;
147 | if (resultSet.length == pageSize + 1) {
148 | Post lastPost = resultSet.removeLast();
149 | nextPageToken = lastPost.id.toString();
150 | }
151 |
152 | return PaginatedList(items: resultSet, nextPageToken: nextPageToken);
153 | }
154 | }
155 |
156 | class PaginatedList {
157 | final Iterable _items;
158 | final String? _nextPageToken;
159 |
160 | List get items => UnmodifiableListView(_items);
161 | String? get nextPageToken => _nextPageToken;
162 |
163 | PaginatedList({required Iterable items, String? nextPageToken})
164 | : _items = items,
165 | _nextPageToken = nextPageToken;
166 | }
167 |
--------------------------------------------------------------------------------
/lib/src/theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | final class PagedDataTableThemeData {
4 | /// The padding of the cell.
5 | final EdgeInsetsGeometry cellPadding;
6 |
7 | /// The padding of a entire column, from other columns.
8 | final EdgeInsetsGeometry padding;
9 |
10 | /// The [BorderRadius] of the table.
11 | final BorderRadius borderRadius;
12 |
13 | /// The elevation of the table.
14 | final double elevation;
15 |
16 | /// The table's background color.
17 | final Color backgroundColor;
18 |
19 | /// The header bar's height
20 | final double headerHeight;
21 |
22 | /// The footer bar's height
23 | final double footerHeight;
24 |
25 | /// The height of each row.
26 | final double rowHeight;
27 |
28 | /// The filter bar's height.
29 | final double filterBarHeight;
30 |
31 | /// The [BoxBorder] to render for each cell.
32 | final BoxBorder cellBorderSide;
33 |
34 | /// The [TextStyle] for [Text]-like elements of a cell.
35 | final TextStyle cellTextStyle;
36 |
37 | /// The [TextStyle] for [Text]-like elements in the header.
38 | final TextStyle headerTextStyle;
39 |
40 | /// The [TextStyle] for [Text]-like elements in the footer.
41 | final TextStyle footerTextStyle;
42 |
43 | /// A function that calculates the [Color] of a row.
44 | final Color? Function(int index)? rowColor;
45 |
46 | /// The [Color] of a selected row.
47 | final Color? selectedRow;
48 |
49 | /// A flag that indicates if the vertical scrollbar should be visible.
50 | final bool verticalScrollbarVisibility;
51 |
52 | /// A flag that indicates if the horizontal scrollbar should be visible.
53 | final bool horizontalScrollbarVisibility;
54 |
55 | /// The width breakpoint that [PagedDataTable] uses to decide if will render a popup or a bottom sheet when the filter dialog is requested.
56 | final double filterDialogBreakpoint;
57 |
58 | /// The [ChipThemeData] to apply to filter chips.
59 | final ChipThemeData? chipTheme;
60 |
61 | const PagedDataTableThemeData({
62 | this.cellPadding =
63 | const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
64 | this.padding = const EdgeInsets.symmetric(horizontal: 16.0),
65 | this.borderRadius = const BorderRadius.all(Radius.circular(4.0)),
66 | this.elevation = 0.0,
67 | this.cellBorderSide = const Border(),
68 | this.headerHeight = 56.0,
69 | this.footerHeight = 56.0,
70 | this.filterBarHeight = 50.0,
71 | this.rowHeight = 52.0,
72 | this.selectedRow,
73 | this.cellTextStyle =
74 | const TextStyle(color: Colors.black, overflow: TextOverflow.ellipsis),
75 | this.headerTextStyle = const TextStyle(
76 | color: Colors.black,
77 | fontWeight: FontWeight.bold,
78 | overflow: TextOverflow.ellipsis),
79 | this.footerTextStyle = const TextStyle(fontSize: 14, color: Colors.black),
80 | this.rowColor,
81 | this.verticalScrollbarVisibility = true,
82 | this.horizontalScrollbarVisibility = true,
83 | this.filterDialogBreakpoint = 1000.0,
84 | this.chipTheme,
85 | this.backgroundColor = Colors.white,
86 | });
87 |
88 | @override
89 | int get hashCode => Object.hash(
90 | cellPadding,
91 | padding,
92 | borderRadius,
93 | elevation,
94 | headerHeight,
95 | footerHeight,
96 | rowHeight,
97 | cellBorderSide,
98 | cellTextStyle,
99 | headerTextStyle,
100 | rowColor,
101 | verticalScrollbarVisibility,
102 | horizontalScrollbarVisibility,
103 | chipTheme,
104 | backgroundColor);
105 |
106 | @override
107 | bool operator ==(Object other) =>
108 | identical(other, this) ||
109 | (other is PagedDataTableThemeData &&
110 | other.cellPadding == cellPadding &&
111 | other.padding == padding &&
112 | other.borderRadius == borderRadius &&
113 | other.elevation == elevation &&
114 | other.headerHeight == headerHeight &&
115 | other.footerHeight == footerHeight &&
116 | other.rowHeight == rowHeight &&
117 | other.cellTextStyle == cellTextStyle &&
118 | other.headerTextStyle == headerTextStyle &&
119 | other.rowColor == rowColor &&
120 | other.cellBorderSide == cellBorderSide &&
121 | other.selectedRow == selectedRow &&
122 | other.verticalScrollbarVisibility == verticalScrollbarVisibility &&
123 | other.horizontalScrollbarVisibility ==
124 | horizontalScrollbarVisibility &&
125 | other.chipTheme == chipTheme &&
126 | other.backgroundColor == backgroundColor);
127 | }
128 |
129 | final class PagedDataTableTheme extends InheritedWidget {
130 | final PagedDataTableThemeData data;
131 |
132 | const PagedDataTableTheme(
133 | {required this.data, required super.child, super.key});
134 |
135 | @override
136 | bool updateShouldNotify(covariant InheritedWidget oldWidget) =>
137 | data != (oldWidget as PagedDataTableTheme).data;
138 |
139 | /// Lookups for a [PagedDataTableTheme] widget in the widget tree, if not found, returns null.
140 | static PagedDataTableThemeData? maybeOf(BuildContext context) =>
141 | context.dependOnInheritedWidgetOfExactType()?.data;
142 |
143 | /// Lookups for a [PagedDataTableTheme] widget in the widget tree, if not found, then default [PagedDataTableThemeData] is returned.
144 | static PagedDataTableThemeData of(BuildContext context) =>
145 | maybeOf(context) ?? const PagedDataTableThemeData();
146 | }
147 |
--------------------------------------------------------------------------------
/lib/src/header.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | final class _Header, T> extends StatefulWidget {
4 | final PagedDataTableController controller;
5 | final int fixedColumnCount;
6 | final List columns;
7 | final ScrollController horizontalController;
8 | final PagedDataTableConfiguration configuration;
9 | final List sizes;
10 |
11 | const _Header({
12 | required this.sizes,
13 | required this.controller,
14 | required this.columns,
15 | required this.fixedColumnCount,
16 | required this.horizontalController,
17 | required this.configuration,
18 | });
19 |
20 | @override
21 | State createState() => _HeaderState();
22 | }
23 |
24 | final class _HeaderState, T>
25 | extends State<_Header> {
26 | late _TableState tableState;
27 | SortModel? sortModel;
28 |
29 | @override
30 | void initState() {
31 | super.initState();
32 |
33 | sortModel = widget.controller.sortModel;
34 | tableState = widget.controller._state;
35 | widget.controller.addListener(_onControllerChanged);
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | final theme = PagedDataTableTheme.of(context);
41 | final fixedColumns = _buildFixedColumns(context, theme);
42 | final columns = _buildColumns(context, theme);
43 |
44 | return SizedBox(
45 | height: theme.headerHeight,
46 | child: DefaultTextStyle(
47 | style: theme.headerTextStyle,
48 | child: Stack(
49 | fit: StackFit.expand,
50 | children: [
51 | Row(
52 | children: [
53 | if (widget.fixedColumnCount > 0) ...fixedColumns,
54 | Expanded(
55 | child: _ScrollableColumns(
56 | controller: widget.horizontalController,
57 | children: columns
58 | .map((e) => SliverToBoxAdapter(child: e))
59 | .toList(growable: false)),
60 | ),
61 | ],
62 | ),
63 | Align(
64 | alignment: Alignment.bottomCenter,
65 | child: tableState == _TableState.fetching
66 | ? const LinearProgressIndicator()
67 | : const SizedBox.shrink(),
68 | )
69 | ],
70 | ),
71 | ),
72 | );
73 | }
74 |
75 | List _buildFixedColumns(
76 | BuildContext context, PagedDataTableThemeData theme) {
77 | final list = [];
78 |
79 | for (int i = 0; i < widget.fixedColumnCount; i++) {
80 | final column = widget.columns[i];
81 | list.add(_buildColumn(context, theme, widget.sizes[i], column));
82 | }
83 |
84 | return list;
85 | }
86 |
87 | List _buildColumns(
88 | BuildContext context, PagedDataTableThemeData theme) {
89 | final list = [];
90 | for (int i = widget.fixedColumnCount; i < widget.columns.length; i++) {
91 | final column = widget.columns[i];
92 | list.add(_buildColumn(context, theme, widget.sizes[i], column));
93 | }
94 |
95 | return list;
96 | }
97 |
98 | Widget _buildColumn(BuildContext context, PagedDataTableThemeData theme,
99 | double width, ReadOnlyTableColumn column) {
100 | Widget child = Container(
101 | padding: theme.cellPadding,
102 | margin: theme.padding,
103 | child: column.format.transform(
104 | Tooltip(
105 | textAlign: TextAlign.left,
106 | message: column.title is Text
107 | ? (column.title as Text).data!
108 | : column.title is RichText
109 | ? (column.title as RichText).text.toPlainText()
110 | : column.tooltip ?? "",
111 | child: column.title),
112 | ),
113 | );
114 |
115 | if (column.sortable) {
116 | child = MouseRegion(
117 | cursor: SystemMouseCursors.click,
118 | child: GestureDetector(
119 | onTap: () {
120 | widget.controller.swipeSortModel(column.id);
121 | },
122 | child: child,
123 | ),
124 | );
125 |
126 | if (sortModel?.fieldName == column.id) {
127 | child = Row(
128 | children: [
129 | Flexible(child: child),
130 | IconButton(
131 | icon: sortModel!.descending
132 | ? const Icon(Icons.arrow_downward)
133 | : const Icon(Icons.arrow_upward),
134 | onPressed: () {
135 | widget.controller.swipeSortModel(column.id);
136 | },
137 | )
138 | ],
139 | );
140 | }
141 | }
142 |
143 | child = SizedBox(width: width, child: child);
144 |
145 | return child;
146 | }
147 |
148 | void _onControllerChanged() {
149 | if (widget.controller.sortModel != sortModel ||
150 | widget.controller._state != tableState) {
151 | setState(() {
152 | sortModel = widget.controller.sortModel;
153 | tableState = widget.controller._state;
154 | });
155 | }
156 | }
157 |
158 | @override
159 | void dispose() {
160 | super.dispose();
161 | widget.controller.removeListener(_onControllerChanged);
162 | }
163 | }
164 |
165 | class _ScrollableColumns extends ScrollView {
166 | final List children;
167 |
168 | const _ScrollableColumns(
169 | {required this.children, required ScrollController controller})
170 | : super(scrollDirection: Axis.horizontal, controller: controller);
171 |
172 | @override
173 | List buildSlivers(BuildContext context) => children;
174 | }
175 |
--------------------------------------------------------------------------------
/lib/l10n/generated/l10n.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 | import 'package:flutter/material.dart';
3 | import 'package:intl/intl.dart';
4 | import 'intl/messages_all.dart';
5 |
6 | // **************************************************************************
7 | // Generator: Flutter Intl IDE plugin
8 | // Made by Localizely
9 | // **************************************************************************
10 |
11 | // ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
12 | // ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
13 | // ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
14 |
15 | class PagedDataTableLocalization {
16 | PagedDataTableLocalization();
17 |
18 | static PagedDataTableLocalization? _current;
19 |
20 | static PagedDataTableLocalization get current {
21 | assert(_current != null,
22 | 'No instance of PagedDataTableLocalization was loaded. Try to initialize the PagedDataTableLocalization delegate before accessing PagedDataTableLocalization.current.');
23 | return _current!;
24 | }
25 |
26 | static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
27 |
28 | static Future load(Locale locale) {
29 | final name = (locale.countryCode?.isEmpty ?? false)
30 | ? locale.languageCode
31 | : locale.toString();
32 | final localeName = Intl.canonicalizedLocale(name);
33 | return initializeMessages(localeName).then((_) {
34 | Intl.defaultLocale = localeName;
35 | final instance = PagedDataTableLocalization();
36 | PagedDataTableLocalization._current = instance;
37 |
38 | return instance;
39 | });
40 | }
41 |
42 | static PagedDataTableLocalization of(BuildContext context) {
43 | final instance = PagedDataTableLocalization.maybeOf(context);
44 | assert(instance != null,
45 | 'No instance of PagedDataTableLocalization present in the widget tree. Did you add PagedDataTableLocalization.delegate in localizationsDelegates?');
46 | return instance!;
47 | }
48 |
49 | static PagedDataTableLocalization? maybeOf(BuildContext context) {
50 | return Localizations.of(
51 | context, PagedDataTableLocalization);
52 | }
53 |
54 | /// `Filter`
55 | String get showFilterMenuTooltip {
56 | return Intl.message(
57 | 'Filter',
58 | name: 'showFilterMenuTooltip',
59 | desc: '',
60 | args: [],
61 | );
62 | }
63 |
64 | /// `Filter by`
65 | String get filterByTitle {
66 | return Intl.message(
67 | 'Filter by',
68 | name: 'filterByTitle',
69 | desc: '',
70 | args: [],
71 | );
72 | }
73 |
74 | /// `Apply`
75 | String get applyFilterButtonText {
76 | return Intl.message(
77 | 'Apply',
78 | name: 'applyFilterButtonText',
79 | desc: '',
80 | args: [],
81 | );
82 | }
83 |
84 | /// `Cancel`
85 | String get cancelFilteringButtonText {
86 | return Intl.message(
87 | 'Cancel',
88 | name: 'cancelFilteringButtonText',
89 | desc: '',
90 | args: [],
91 | );
92 | }
93 |
94 | /// `Remove`
95 | String get removeAllFiltersButtonText {
96 | return Intl.message(
97 | 'Remove',
98 | name: 'removeAllFiltersButtonText',
99 | desc: '',
100 | args: [],
101 | );
102 | }
103 |
104 | /// `Save changes`
105 | String get editableColumnSaveChangesButtonText {
106 | return Intl.message(
107 | 'Save changes',
108 | name: 'editableColumnSaveChangesButtonText',
109 | desc: '',
110 | args: [],
111 | );
112 | }
113 |
114 | /// `Cancel`
115 | String get editableColumnCancelButtonText {
116 | return Intl.message(
117 | 'Cancel',
118 | name: 'editableColumnCancelButtonText',
119 | desc: '',
120 | args: [],
121 | );
122 | }
123 |
124 | /// `Remove this filter`
125 | String get removeFilterButtonText {
126 | return Intl.message(
127 | 'Remove this filter',
128 | name: 'removeFilterButtonText',
129 | desc: '',
130 | args: [],
131 | );
132 | }
133 |
134 | /// `Refresh`
135 | String get refreshText {
136 | return Intl.message(
137 | 'Refresh',
138 | name: 'refreshText',
139 | desc: '',
140 | args: [],
141 | );
142 | }
143 |
144 | /// `Last refreshed at {time}`
145 | String refreshedAtText(Object time) {
146 | return Intl.message(
147 | 'Last refreshed at $time',
148 | name: 'refreshedAtText',
149 | desc: '',
150 | args: [time],
151 | );
152 | }
153 |
154 | /// `Rows per page`
155 | String get rowsPerPageText {
156 | return Intl.message(
157 | 'Rows per page',
158 | name: 'rowsPerPageText',
159 | desc: '',
160 | args: [],
161 | );
162 | }
163 |
164 | /// `Page {currentPage}`
165 | String pageIndicatorText(Object currentPage) {
166 | return Intl.message(
167 | 'Page $currentPage',
168 | name: 'pageIndicatorText',
169 | desc: '',
170 | args: [currentPage],
171 | );
172 | }
173 |
174 | /// `Showing {totalElements} elements`
175 | String totalElementsText(Object totalElements) {
176 | return Intl.message(
177 | 'Showing $totalElements elements',
178 | name: 'totalElementsText',
179 | desc: '',
180 | args: [totalElements],
181 | );
182 | }
183 |
184 | /// `Next page`
185 | String get nextPageButtonText {
186 | return Intl.message(
187 | 'Next page',
188 | name: 'nextPageButtonText',
189 | desc: '',
190 | args: [],
191 | );
192 | }
193 |
194 | /// `Previous page`
195 | String get previousPageButtonText {
196 | return Intl.message(
197 | 'Previous page',
198 | name: 'previousPageButtonText',
199 | desc: '',
200 | args: [],
201 | );
202 | }
203 |
204 | /// `No items found`
205 | String get noItemsFoundText {
206 | return Intl.message(
207 | 'No items found',
208 | name: 'noItemsFoundText',
209 | desc: '',
210 | args: [],
211 | );
212 | }
213 | }
214 |
215 | class AppLocalizationDelegate
216 | extends LocalizationsDelegate {
217 | const AppLocalizationDelegate();
218 |
219 | List get supportedLocales {
220 | return const [
221 | Locale.fromSubtags(languageCode: 'en'),
222 | Locale.fromSubtags(languageCode: 'de'),
223 | Locale.fromSubtags(languageCode: 'es'),
224 | Locale.fromSubtags(languageCode: 'it'),
225 | ];
226 | }
227 |
228 | @override
229 | bool isSupported(Locale locale) => _isSupported(locale);
230 | @override
231 | Future load(Locale locale) =>
232 | PagedDataTableLocalization.load(locale);
233 | @override
234 | bool shouldReload(AppLocalizationDelegate old) => false;
235 |
236 | bool _isSupported(Locale locale) {
237 | for (var supportedLocale in supportedLocales) {
238 | if (supportedLocale.languageCode == locale.languageCode) {
239 | return true;
240 | }
241 | }
242 | return false;
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/lib/src/filter.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | /// Represents a value selector that filters the dataset returned and displayed in the table.
4 | abstract class TableFilter {
5 | /// The name of the filter.
6 | ///
7 | /// It will appear in the filter dialog.
8 | final String name;
9 |
10 | /// The id of the filter, used to identify it when fetching data.
11 | final String id;
12 |
13 | /// Formats [T] to be displayed in the selected filter chip.
14 | final String Function(T value) chipFormatter;
15 |
16 | /// A flag that indicates if the filter is enabled or not
17 | final bool enabled;
18 |
19 | /// A flag that indicates if the filter is visible or not
20 | final bool visible;
21 |
22 | /// The initial value for the filter
23 | final T? initialValue;
24 |
25 | const TableFilter({
26 | required this.id,
27 | required this.name,
28 | required this.chipFormatter,
29 | required this.enabled,
30 | required this.initialValue,
31 | this.visible = true,
32 | });
33 |
34 | /// Renders the picker for the filter.
35 | Widget buildPicker(BuildContext context, FilterState state);
36 |
37 | /// Creates the state of the filter
38 | FilterState createState() => FilterState._(this);
39 |
40 | @override
41 | int get hashCode => Object.hash(name, id, enabled, initialValue);
42 |
43 | @override
44 | bool operator ==(Object other) =>
45 | identical(other, this) ||
46 | (other is TableFilter &&
47 | other.id == id &&
48 | other.name == name &&
49 | other.enabled == enabled &&
50 | other.initialValue == initialValue);
51 | }
52 |
53 | /// A [TableFilter] that renders a [TextFormField].
54 | final class TextTableFilter extends TableFilter {
55 | final InputDecoration? decoration;
56 |
57 | const TextTableFilter({
58 | this.decoration,
59 | required super.chipFormatter,
60 | required super.id,
61 | required super.name,
62 | super.initialValue,
63 | super.enabled = true,
64 | });
65 |
66 | @override
67 | Widget buildPicker(BuildContext context, FilterState state) {
68 | return TextFormField(
69 | decoration: decoration ?? InputDecoration(labelText: name),
70 | initialValue: state.value,
71 | onSaved: (newValue) {
72 | if (newValue != null && newValue.isNotEmpty) {
73 | state.value = newValue;
74 | }
75 | },
76 | );
77 | }
78 | }
79 |
80 | /// A [TableFilter] that is not visible in the filter selection but can be set using the controller.
81 | final class ProgrammingTextFilter extends TableFilter {
82 | ProgrammingTextFilter({
83 | required super.id,
84 | required super.chipFormatter,
85 | required super.initialValue,
86 | }) : super(enabled: true, visible: false, name: "");
87 |
88 | @override
89 | Widget buildPicker(BuildContext context, FilterState state) =>
90 | const SizedBox.shrink();
91 | }
92 |
93 | /// A [TableFilter] that renders a [DropdownButtonFormField].
94 | final class DropdownTableFilter extends TableFilter {
95 | final InputDecoration? decoration;
96 | final List> items;
97 |
98 | const DropdownTableFilter({
99 | this.decoration,
100 | required this.items,
101 | required super.chipFormatter,
102 | required super.id,
103 | required super.name,
104 | super.initialValue,
105 | super.enabled = true,
106 | });
107 |
108 | @override
109 | Widget buildPicker(BuildContext context, FilterState state) {
110 | return DropdownButtonFormField(
111 | items: items,
112 | value: state.value,
113 | onChanged: (newValue) {},
114 | onSaved: (newValue) {
115 | state.value = newValue;
116 | },
117 | decoration: decoration ?? InputDecoration(labelText: name),
118 | );
119 | }
120 | }
121 |
122 | /// A [TableFilter] that renders a [TextField] that, when selected, opens a [DateTime] picker.
123 | final class DateTimePickerTableFilter extends TableFilter {
124 | final DateTime firstDate;
125 | final DateTime lastDate;
126 | final DateTime? initialDate;
127 | final DatePickerMode initialDatePickerMode;
128 | final DatePickerEntryMode initialEntryMode;
129 | final bool Function(DateTime)? selectableDayPredicate;
130 | final DateFormat dateFormat;
131 | final InputDecoration inputDecoration;
132 |
133 | DateTimePickerTableFilter({
134 | required super.id,
135 | required super.name,
136 | required super.chipFormatter,
137 | required super.initialValue,
138 | required this.firstDate,
139 | required this.lastDate,
140 | required this.dateFormat,
141 | super.enabled = true,
142 | this.initialDate,
143 | this.initialDatePickerMode = DatePickerMode.day,
144 | this.initialEntryMode = DatePickerEntryMode.calendar,
145 | this.selectableDayPredicate,
146 | this.inputDecoration = const InputDecoration(),
147 | });
148 |
149 | @override
150 | Widget buildPicker(BuildContext context, FilterState state) =>
151 | _DateTimePicker(
152 | firstDate: firstDate,
153 | initialDate: initialDate,
154 | initialDatePickerMode: initialDatePickerMode,
155 | initialEntryMode: initialEntryMode,
156 | lastDate: lastDate,
157 | selectableDayPredicate: selectableDayPredicate,
158 | dateFormat: dateFormat,
159 | value: state.value,
160 | inputDecoration: inputDecoration,
161 | name: name,
162 | onChanged: (newValue) {
163 | state.value = newValue;
164 | },
165 | );
166 | }
167 |
168 | /// A [TableFilter] that renders a [TextField] that, when selected, opens a [DateTimeRange] picker.
169 |
170 | final class DateRangePickerTableFilter extends TableFilter {
171 | final DateTime firstDate;
172 | final DateTime lastDate;
173 | final DateTimeRange? initialDateRange;
174 | final DatePickerMode initialDatePickerMode;
175 | final DatePickerEntryMode initialEntryMode;
176 | final String Function(DateTimeRange) formatter;
177 | final InputDecoration inputDecoration;
178 | final TransitionBuilder? dialogBuilder;
179 |
180 | DateRangePickerTableFilter({
181 | required super.id,
182 | required super.name,
183 | required super.chipFormatter,
184 | required super.initialValue,
185 | required this.firstDate,
186 | required this.lastDate,
187 | required this.formatter,
188 | super.enabled = true,
189 | this.initialDateRange,
190 | this.initialDatePickerMode = DatePickerMode.day,
191 | this.initialEntryMode = DatePickerEntryMode.calendar,
192 | this.inputDecoration = const InputDecoration(),
193 | this.dialogBuilder,
194 | });
195 |
196 | @override
197 | Widget buildPicker(BuildContext context, FilterState state) =>
198 | _DateRangePicker(
199 | firstDate: firstDate,
200 | formatter: formatter,
201 | initialDateRange: initialDateRange,
202 | initialDatePickerMode: initialDatePickerMode,
203 | initialEntryMode: initialEntryMode,
204 | lastDate: lastDate,
205 | value: state.value,
206 | inputDecoration: inputDecoration,
207 | name: name,
208 | dialogBuilder: dialogBuilder,
209 | onChanged: (newValue) {
210 | state.value = newValue;
211 | },
212 | );
213 |
214 | /// A convenient method to create a [TransitionBuilder] that builds the DateRangePicker dialog with a given size.
215 | ///
216 | /// This is useful on desktop platforms.
217 | static TransitionBuilder sizedDialog(double height, double width) {
218 | return (context, child) => Center(
219 | child: ConstrainedBox(
220 | constraints: const BoxConstraints(
221 | maxWidth: 400.0,
222 | maxHeight: 600.0,
223 | ),
224 | child: child,
225 | ),
226 | );
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/lib/src/footer_widgets.dart:
--------------------------------------------------------------------------------
1 | part of "paged_datatable.dart";
2 |
3 | /// A [PagedDataTable] footer widget that renders a refresh button.
4 | class RefreshTable, T> extends StatefulWidget {
5 | const RefreshTable({super.key});
6 |
7 | @override
8 | State createState() => _RefreshTableState();
9 | }
10 |
11 | class _RefreshTableState, T>
12 | extends State> {
13 | late final theme = PagedDataTableTheme.of(context);
14 | late final controller = TableControllerProvider.of(context);
15 |
16 | @override
17 | void didChangeDependencies() {
18 | super.didChangeDependencies();
19 |
20 | controller.addListener(_onChanged);
21 | }
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | final localizations = PagedDataTableLocalization.of(context);
26 | return Row(
27 | children: [
28 | const SizedBox(width: 10),
29 | IconButton(
30 | splashRadius: 20,
31 | tooltip: localizations.refreshText,
32 | onPressed: () => controller.refresh(fromStart: false),
33 | icon: const Icon(Icons.refresh_outlined),
34 | ),
35 | const SizedBox(width: 10),
36 | ],
37 | );
38 | }
39 |
40 | void _onChanged() {
41 | if (mounted) {
42 | setState(() {});
43 | }
44 | }
45 |
46 | @override
47 | void dispose() {
48 | super.dispose();
49 | controller.removeListener(_onChanged);
50 | }
51 | }
52 |
53 | /// A [PagedDataTable] footer widget that renders a dropdown used to select a page size.
54 | class PageSizeSelector, T> extends StatefulWidget {
55 | const PageSizeSelector({super.key});
56 |
57 | @override
58 | State createState() => _PageSizeSelectorState();
59 | }
60 |
61 | class _PageSizeSelectorState, T>
62 | extends State> {
63 | late final theme = PagedDataTableTheme.of(context);
64 | late final controller = TableControllerProvider.of(context);
65 |
66 | @override
67 | void didChangeDependencies() {
68 | super.didChangeDependencies();
69 |
70 | controller.addListener(_onChanged);
71 | }
72 |
73 | @override
74 | Widget build(BuildContext context) {
75 | final localizations = PagedDataTableLocalization.of(context);
76 | assert(controller._pageSizes != null,
77 | "PageSizeSelector widget can be used only if the pageSizes property is set.");
78 |
79 | return Row(
80 | children: [
81 | const SizedBox(width: 10),
82 | Text(localizations.rowsPerPageText),
83 | const SizedBox(width: 10),
84 | SizedBox(
85 | width: 100,
86 | child: DropdownButtonFormField(
87 | value: controller.pageSize,
88 | items: controller._pageSizes!
89 | .map((pageSize) => DropdownMenuItem(
90 | value: pageSize, child: Text(pageSize.toString())))
91 | .toList(growable: false),
92 | onChanged: controller._state == _TableState.fetching
93 | ? null
94 | : (newPageSize) {
95 | if (newPageSize != null) {
96 | controller.pageSize = newPageSize;
97 | }
98 | },
99 | style: theme.footerTextStyle.copyWith(fontSize: 14),
100 | decoration: const InputDecoration(
101 | border: OutlineInputBorder(
102 | borderSide: BorderSide(color: Color(0xFFD6D6D6))),
103 | isCollapsed: true,
104 | contentPadding: EdgeInsets.symmetric(horizontal: 6, vertical: 8),
105 | ),
106 | ),
107 | ),
108 | const SizedBox(width: 10),
109 | ],
110 | );
111 | }
112 |
113 | void _onChanged() {
114 | if (mounted) {
115 | setState(() {});
116 | }
117 | }
118 |
119 | @override
120 | void dispose() {
121 | super.dispose();
122 | controller.removeListener(_onChanged);
123 | }
124 | }
125 |
126 | /// A [PagedDataTable] footer widget that renders the total items in the current resultset.
127 | class TotalItems, T> extends StatefulWidget {
128 | const TotalItems({super.key});
129 |
130 | @override
131 | State createState() => _TotalItemsState();
132 | }
133 |
134 | class _TotalItemsState, T>
135 | extends State> {
136 | late final theme = PagedDataTableTheme.of(context);
137 | late final controller = TableControllerProvider.of(context);
138 |
139 | @override
140 | void didChangeDependencies() {
141 | super.didChangeDependencies();
142 |
143 | controller.addListener(_onChanged);
144 | }
145 |
146 | @override
147 | Widget build(BuildContext context) {
148 | final localizations = PagedDataTableLocalization.of(context);
149 |
150 | return Row(
151 | children: [
152 | const SizedBox(width: 10),
153 | Text(localizations.totalElementsText(controller._totalItems)),
154 | const SizedBox(width: 10),
155 | ],
156 | );
157 | }
158 |
159 | void _onChanged() {
160 | if (mounted) {
161 | setState(() {});
162 | }
163 | }
164 |
165 | @override
166 | void dispose() {
167 | super.dispose();
168 | controller.removeListener(_onChanged);
169 | }
170 | }
171 |
172 | /// A [PagedDataTable] footer widget that renders the current page.
173 | class CurrentPage, T> extends StatefulWidget {
174 | const CurrentPage({super.key});
175 |
176 | @override
177 | State createState() => _CurrentPageState();
178 | }
179 |
180 | class _CurrentPageState, T>
181 | extends State> {
182 | late final theme = PagedDataTableTheme.of(context);
183 | late final controller = TableControllerProvider.of(context);
184 |
185 | @override
186 | void didChangeDependencies() {
187 | super.didChangeDependencies();
188 |
189 | controller.addListener(_onChanged);
190 | }
191 |
192 | @override
193 | Widget build(BuildContext context) {
194 | final localizations = PagedDataTableLocalization.of(context);
195 | return Row(
196 | children: [
197 | const SizedBox(width: 10),
198 | Text(localizations.pageIndicatorText(controller._currentPageIndex + 1)),
199 | const SizedBox(width: 10),
200 | ],
201 | );
202 | }
203 |
204 | void _onChanged() {
205 | if (mounted) {
206 | setState(() {});
207 | }
208 | }
209 |
210 | @override
211 | void dispose() {
212 | super.dispose();
213 | controller.removeListener(_onChanged);
214 | }
215 | }
216 |
217 | /// A [PagedDataTable] footer widget that renders navigation buttons.
218 | class NavigationButtons, T> extends StatefulWidget {
219 | const NavigationButtons({super.key});
220 |
221 | @override
222 | State createState() => _NavigationButtonsState();
223 | }
224 |
225 | class _NavigationButtonsState, T>
226 | extends State> {
227 | late final theme = PagedDataTableTheme.of(context);
228 | late final controller = TableControllerProvider.of(context);
229 |
230 | @override
231 | void didChangeDependencies() {
232 | super.didChangeDependencies();
233 |
234 | controller.addListener(_onChanged);
235 | }
236 |
237 | @override
238 | Widget build(BuildContext context) {
239 | final localizations = PagedDataTableLocalization.of(context);
240 | return Row(
241 | children: [
242 | const SizedBox(width: 10),
243 | IconButton(
244 | tooltip: localizations.previousPageButtonText,
245 | splashRadius: 20,
246 | icon: const Icon(Icons.keyboard_arrow_left_rounded),
247 | onPressed: (controller.hasPreviousPage &&
248 | controller._state != _TableState.fetching)
249 | ? controller.previousPage
250 | : null,
251 | ),
252 | const SizedBox(width: 12),
253 | IconButton(
254 | tooltip: localizations.nextPageButtonText,
255 | splashRadius: 20,
256 | icon: const Icon(Icons.keyboard_arrow_right_rounded),
257 | onPressed: (controller.hasNextPage &&
258 | controller._state != _TableState.fetching)
259 | ? controller.nextPage
260 | : null,
261 | ),
262 | const SizedBox(width: 10),
263 | ],
264 | );
265 | }
266 |
267 | void _onChanged() {
268 | if (mounted) {
269 | setState(() {});
270 | }
271 | }
272 |
273 | @override
274 | void dispose() {
275 | super.dispose();
276 | controller.removeListener(_onChanged);
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/lib/src/column.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | typedef Setter = FutureOr Function(T item, V value, int rowIndex);
4 | typedef Getter = V? Function(T item, int rowIndex);
5 | typedef CellBuilder = Widget Function(
6 | BuildContext context, T item, int rowIndex);
7 |
8 | /// [ReadOnlyTableColumn] represents a basic table column for [T] that displays read-only content.
9 | abstract class ReadOnlyTableColumn, T> {
10 | /// The title of the column.
11 | final Widget title;
12 |
13 | /// The tooltip to show in the column's header. If null, it will try to take the content of [title], if it's a Text, otherwise, it won't show a tooltip.
14 | final String? tooltip;
15 |
16 | /// The id of the column. Useful if this column can be used to sort data.
17 | final String? id;
18 |
19 | /// The size of the column
20 | final ColumnSize size;
21 |
22 | /// The column's format
23 | final ColumnFormat format;
24 |
25 | /// A flag that indicates if the column can be used as sort model. [id] must not be null.
26 | final bool sortable;
27 |
28 | const ReadOnlyTableColumn({
29 | required this.id,
30 | required this.title,
31 | required this.size,
32 | required this.format,
33 | required this.tooltip,
34 | required this.sortable,
35 | }) : assert(sortable ? id != null : true,
36 | "When column is sortable, id must be set.");
37 |
38 | /// Builds the cell for [item] at [index].
39 | Widget build(BuildContext context, T item, int index);
40 |
41 | @override
42 | int get hashCode => Object.hash(id, size, title, format);
43 |
44 | @override
45 | bool operator ==(Object other) =>
46 | other is ReadOnlyTableColumn &&
47 | other.title == title &&
48 | other.id == id &&
49 | other.size == size &&
50 | other.format == format;
51 | }
52 |
53 | /// [EditableTableColumn] represents a basic table column for [T] that display editable content of type [V].
54 | abstract class EditableTableColumn, T, V>
55 | extends ReadOnlyTableColumn {
56 | /// The function that is going to be called when the field is saved. It must return a boolean indicating
57 | /// if the update operation succeeded or not, being [true] for a successful operation or false otherwise.
58 | final Setter setter;
59 |
60 | /// The function that is going to retrieve the current value of [T] for this column.
61 | final Getter getter;
62 |
63 | const EditableTableColumn({
64 | required super.id,
65 | required super.title,
66 | required super.size,
67 | required super.format,
68 | required super.tooltip,
69 | required super.sortable,
70 | required this.setter,
71 | required this.getter,
72 | });
73 | }
74 |
75 | /// [TableColumn] is a implementation of [ReadOnlyTableColumn] that renders a cell based on [cellBuilder].
76 | final class TableColumn, T>
77 | extends ReadOnlyTableColumn {
78 | final CellBuilder cellBuilder;
79 |
80 | const TableColumn({
81 | required this.cellBuilder,
82 | required super.title,
83 | super.id,
84 | super.size = const FractionalColumnSize(.1),
85 | super.format = const AlignColumnFormat(alignment: Alignment.centerLeft),
86 | super.tooltip,
87 | super.sortable = false,
88 | });
89 |
90 | @override
91 | Widget build(BuildContext context, T item, int index) =>
92 | cellBuilder(context, item, index);
93 |
94 | @override
95 | int get hashCode => Object.hash(id, size, title, cellBuilder, format);
96 |
97 | @override
98 | bool operator ==(Object other) =>
99 | other is TableColumn &&
100 | other.cellBuilder ==
101 | cellBuilder && // todo: this will always return false, fix a better way to compare those
102 | other.title == title &&
103 | other.id == id &&
104 | other.size == size &&
105 | other.format == format;
106 | }
107 |
108 | /// [DropdownTableColumn] renders a compact [DropdownButton] that allows to modify the cell's value in place.
109 | ///
110 | /// The [DropdownButton]'s type is [V].
111 | final class DropdownTableColumn, T, V>
112 | extends EditableTableColumn {
113 | final InputDecoration inputDecoration;
114 | final List> items;
115 |
116 | const DropdownTableColumn({
117 | required super.title,
118 | super.id,
119 | super.size = const FractionalColumnSize(.1),
120 | super.format = const AlignColumnFormat(alignment: Alignment.centerLeft),
121 | super.tooltip,
122 | super.sortable = false,
123 | required super.setter,
124 | required super.getter,
125 | required this.items,
126 | this.inputDecoration = const InputDecoration(isDense: true),
127 | });
128 |
129 | @override
130 | Widget build(BuildContext context, T item, int index) => _DropdownCell(
131 | getter: getter,
132 | setter: setter,
133 | index: index,
134 | item: item,
135 | items: items,
136 | inputDecoration: inputDecoration,
137 | key: ValueKey(item),
138 | );
139 | }
140 |
141 | /// [TextTableColumn] renders a compact [TextField] that allows to modify the cell's value in place when double-clicked.
142 | final class TextTableColumn, T>
143 | extends EditableTableColumn {
144 | final InputDecoration inputDecoration;
145 | final List? inputFormatters;
146 |
147 | const TextTableColumn({
148 | required super.title,
149 | super.id,
150 | super.size = const FractionalColumnSize(.1),
151 | super.format = const AlignColumnFormat(alignment: Alignment.centerLeft),
152 | super.tooltip,
153 | super.sortable = false,
154 | required super.setter,
155 | required super.getter,
156 | this.inputDecoration = const InputDecoration(isDense: true),
157 | this.inputFormatters,
158 | });
159 |
160 | @override
161 | Widget build(BuildContext context, T item, int index) => _TextFieldCell(
162 | getter: getter,
163 | setter: setter,
164 | index: index,
165 | item: item,
166 | key: ValueKey(item),
167 | isDialog: false,
168 | inputDecoration: inputDecoration,
169 | inputFormatters: inputFormatters,
170 | );
171 | }
172 |
173 | /// [LargeTextTableColumn] renders an overlay dialog with a [TextField] that allows to modify the cell's value when double-clicked.
174 | ///
175 | /// This works better for cells with large content.
176 | final class LargeTextTableColumn, T>
177 | extends EditableTableColumn {
178 | final InputDecoration inputDecoration;
179 | final List? inputFormatters;
180 |
181 | /// A flag that indicates if the cell should display a tooltip of the whole cell's content.
182 | final bool showTooltip;
183 |
184 | /// The text's validator.
185 | final FormFieldValidator? validator;
186 |
187 | /// The overlay field's label.
188 | final String fieldLabel;
189 |
190 | /// The tooltip's text style.
191 | final TextStyle tooltipStyle;
192 |
193 | /// The [BoxConstraints] where to render the tooltip.
194 | final BoxConstraints? tooltipConstraints;
195 |
196 | /// The width breakpoint that [PagedDataTable] uses to decide if will render an overlay or a bottom sheet when the field is edited.
197 | final double bottomSheetBreakpoint;
198 |
199 | const LargeTextTableColumn({
200 | required super.title,
201 | super.id,
202 | super.size = const FractionalColumnSize(.1),
203 | super.format = const AlignColumnFormat(alignment: Alignment.centerLeft),
204 | super.tooltip,
205 | super.sortable = false,
206 | required super.setter,
207 | required super.getter,
208 | required this.fieldLabel,
209 | this.inputDecoration =
210 | const InputDecoration(isDense: true, border: OutlineInputBorder()),
211 | this.inputFormatters,
212 | this.validator,
213 | this.showTooltip = true,
214 | this.tooltipStyle = const TextStyle(color: Colors.white),
215 | this.tooltipConstraints,
216 | this.bottomSheetBreakpoint = 1000,
217 | });
218 |
219 | @override
220 | Widget build(BuildContext context, T item, int index) =>
221 | _LargeTextFieldCell(
222 | getter: getter,
223 | setter: setter,
224 | index: index,
225 | item: item,
226 | key: ValueKey(item),
227 | isDialog: false,
228 | inputDecoration: inputDecoration,
229 | inputFormatters: inputFormatters,
230 | label: fieldLabel,
231 | tooltipText: showTooltip,
232 | validator: validator,
233 | tooltipStyle: tooltipStyle,
234 | tooltipConstraints: tooltipConstraints,
235 | bottomSheetBreakpoint: bottomSheetBreakpoint,
236 | );
237 | }
238 |
239 | /// A special [ReadOnlyTableColumn] that renders a checkbox used to select rows.
240 | final class RowSelectorColumn, T>
241 | extends ReadOnlyTableColumn {
242 | /// Creates a new [RowSelectorColumn].
243 | RowSelectorColumn()
244 | : super(
245 | format: const AlignColumnFormat(alignment: Alignment.center),
246 | id: null,
247 | size: const FixedColumnSize(80),
248 | sortable: false,
249 | tooltip: "Select rows",
250 | title: _SelectAllRowsCheckbox(),
251 | );
252 |
253 | @override
254 | Widget build(BuildContext context, T item, int index) {
255 | return _SelectRowCheckbox(index: index);
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/lib/src/paged_datatable.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:collection';
3 |
4 | import 'package:flutter/foundation.dart' show kDebugMode;
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter/services.dart';
7 | import 'package:intl/intl.dart';
8 | import 'package:paged_datatable/paged_datatable.dart';
9 | import 'package:paged_datatable/src/linked_scroll_controller.dart';
10 | import 'package:paged_datatable/src/table_controller_notifier.dart';
11 |
12 | part 'column.dart';
13 | part 'column_widgets.dart';
14 | part 'controller.dart';
15 | part 'double_list_rows.dart';
16 | part 'filter.dart';
17 | part 'filter_bar.dart';
18 | part 'filter_model.dart';
19 | part 'filter_state.dart';
20 | part 'filter_widgets.dart';
21 | part 'footer_widgets.dart';
22 | part 'header.dart';
23 | part 'row.dart';
24 | part 'sort_model.dart';
25 | part 'table_view_rows.dart';
26 |
27 | /// [PagedDataTable] renders a table of items that is paginable.
28 | ///
29 | /// The type of element to be displayed in the table is [T] and [K] is the type of key
30 | /// used to paginate the table.
31 | final class PagedDataTable, T> extends StatefulWidget {
32 | /// An specific [PagedDataTableController] to be use in this [PagedDataTable].
33 | final PagedDataTableController? controller;
34 |
35 | /// The list of columns to draw in the table.
36 | final List> columns;
37 |
38 | /// The initial page size of the table.
39 | ///
40 | /// If [pageSizes] is not null, this value must match any of the its values.
41 | final int initialPageSize;
42 |
43 | /// The initial page query.
44 | final K? initialPage;
45 |
46 | /// The list of page sizes availables to be selected in the footer.
47 | final List? pageSizes;
48 |
49 | /// The callback used to fetch new items.
50 | final Fetcher fetcher;
51 |
52 | /// The amount of columns to fix, starting from the left.
53 | final int fixedColumnCount;
54 |
55 | /// The configuration of this [PagedDataTable].
56 | final PagedDataTableConfiguration configuration;
57 |
58 | /// The widget to display at the footer of the table.
59 | ///
60 | /// If null, the default footer will be displayed.
61 | final Widget? footer;
62 |
63 | /// Additional widget to add at the right of the filter bar.
64 | final Widget? filterBarChild;
65 |
66 | /// The list of filters to use.
67 | final List filters;
68 |
69 | const PagedDataTable({
70 | required this.columns,
71 | required this.fetcher,
72 | this.initialPage,
73 | this.initialPageSize = 50,
74 | this.pageSizes = const [10, 50, 100],
75 | this.controller,
76 | this.fixedColumnCount = 0,
77 | this.configuration = const PagedDataTableConfiguration(),
78 | this.footer,
79 | this.filterBarChild,
80 | this.filters = const [],
81 | super.key,
82 | });
83 |
84 | @override
85 | State createState() => _PagedDataTableState();
86 | }
87 |
88 | final class _PagedDataTableState, T>
89 | extends State> {
90 | final verticalController = ScrollController();
91 | final linkedControllers = LinkedScrollControllerGroup();
92 | late final headerHorizontalController = linkedControllers.addAndGet();
93 | late final horizontalController = linkedControllers.addAndGet();
94 | late final PagedDataTableController tableController;
95 | // late FixedTableSpanExtent rowSpanExtent, headerRowSpanExtent;
96 | late PagedDataTableThemeData theme;
97 | bool selfConstructedController = false;
98 |
99 | @override
100 | void initState() {
101 | super.initState();
102 | assert(
103 | widget.pageSizes != null
104 | ? widget.pageSizes!.contains(widget.initialPageSize)
105 | : true,
106 | "initialPageSize must be inside pageSizes. To disable this restriction, set pageSizes to null.");
107 |
108 | if (widget.controller == null) {
109 | selfConstructedController = true;
110 | tableController = PagedDataTableController();
111 | } else {
112 | tableController = widget.controller!;
113 | }
114 | tableController._init(
115 | columns: widget.columns,
116 | pageSizes: widget.pageSizes,
117 | initialPageSize: widget.initialPageSize,
118 | fetcher: widget.fetcher,
119 | config: widget.configuration,
120 | filters: widget.filters,
121 | );
122 | }
123 |
124 | @override
125 | void didUpdateWidget(covariant PagedDataTable oldWidget) {
126 | super.didUpdateWidget(oldWidget);
127 |
128 | if (oldWidget.columns.length !=
129 | widget.columns
130 | .length /*!listEquals(oldWidget.columns, widget.columns)*/) {
131 | tableController._reset(columns: widget.columns);
132 | debugPrint("PagedDataTable<$T> changed and rebuilt.");
133 | }
134 | }
135 |
136 | @override
137 | Widget build(BuildContext context) {
138 | theme = PagedDataTableTheme.of(context);
139 |
140 | return Card(
141 | color: theme.backgroundColor,
142 | elevation: theme.elevation,
143 | shape: RoundedRectangleBorder(borderRadius: theme.borderRadius),
144 | margin: EdgeInsets.zero,
145 | child: TableControllerProvider(
146 | controller: tableController,
147 | child: LayoutBuilder(
148 | builder: (context, constraints) {
149 | final sizes = _calculateColumnWidth(constraints.maxWidth);
150 |
151 | return Column(
152 | children: [
153 | _FilterBar(child: widget.filterBarChild),
154 |
155 | _Header(
156 | controller: tableController,
157 | configuration: widget.configuration,
158 | columns: widget.columns,
159 | sizes: sizes,
160 | fixedColumnCount: widget.fixedColumnCount,
161 | horizontalController: headerHorizontalController,
162 | ),
163 | const Divider(height: 0, color: Color(0xFFD6D6D6)),
164 |
165 | Expanded(
166 | child: _DoubleListRows(
167 | fixedColumnCount: widget.fixedColumnCount,
168 | columns: widget.columns,
169 | horizontalController: horizontalController,
170 | controller: tableController,
171 | configuration: widget.configuration,
172 | sizes: sizes,
173 | ),
174 | ),
175 |
176 | // Expanded(
177 | // child: _TableViewRows(
178 | // columns: widget.columns,
179 | // controller: tableController,
180 | // fixedColumnCount: widget.fixedColumnCount,
181 | // horizontalController: horizontalController,
182 | // verticalController: verticalController,
183 | // ),
184 | // ),
185 | const Divider(height: 0, color: Color(0xFFD6D6D6)),
186 | SizedBox(
187 | height: theme.footerHeight,
188 | child: widget.footer ?? DefaultFooter(),
189 | ),
190 | ],
191 | );
192 | },
193 | ),
194 | ),
195 | );
196 | }
197 |
198 | @override
199 | void dispose() {
200 | super.dispose();
201 | verticalController.dispose();
202 | horizontalController.dispose();
203 | headerHorizontalController.dispose();
204 |
205 | if (selfConstructedController) {
206 | tableController.dispose();
207 | }
208 | }
209 |
210 | List _calculateColumnWidth(double maxWidth) {
211 | final sizes =
212 | List.filled(widget.columns.length, 0.0, growable: false);
213 |
214 | double totalFixedWidth = 0.0;
215 | double totalFraction = 0.0;
216 | int remainingColumnCount = 0;
217 | double totalFractionalWidth = 0.0;
218 |
219 | // First pass to determine widths and types of columns
220 | for (int i = 0; i < widget.columns.length; i++) {
221 | final column = widget.columns[i];
222 | if (column.size.isFixed) {
223 | final columnSize = column.size.calculateConstraints(maxWidth);
224 | totalFixedWidth += columnSize;
225 | } else {
226 | totalFraction += column.size.fraction;
227 |
228 | // Handle this special case
229 | if (column.size is RemainingColumnSize) {
230 | remainingColumnCount++;
231 | }
232 | }
233 | }
234 |
235 | // Ensure totalFraction is within a valid range to prevent overflow
236 | assert(totalFraction <= 1.0,
237 | "Total fraction exceeds 1.0, which means the columns will overflow.");
238 |
239 | double remainingWidth = maxWidth -
240 | totalFixedWidth; // Calculate remaining width after fixed sizes are allocated
241 | totalFractionalWidth =
242 | remainingWidth * totalFraction; // Re-calculate total fractional width
243 | remainingWidth -=
244 | totalFractionalWidth; // Adjust remaining width to exclude fractional columns' widths for RemainingColumnSize
245 | final remainingColumnWidth = remainingColumnCount > 0.0
246 | ? remainingWidth / remainingColumnCount
247 | : 0.0;
248 |
249 | // Now calculate and assign column sizes
250 | for (int i = 0; i < widget.columns.length; i++) {
251 | final column = widget.columns[i];
252 | if (column.size.isFixed) {
253 | // Pass totalFixedWidth but the ColumnSize should don't care about it because its a fixed size.
254 | sizes[i] = column.size.calculateConstraints(totalFixedWidth);
255 | } else if (column.size is RemainingColumnSize) {
256 | sizes[i] = remainingColumnWidth;
257 | } else {
258 | sizes[i] = totalFractionalWidth * column.size.fraction / totalFraction;
259 | }
260 | }
261 |
262 | return sizes;
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/lib/src/filter_bar.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | /// The filter bar is displayed before the table header
4 | class _FilterBar, T> extends StatefulWidget {
5 | final Widget? child;
6 |
7 | const _FilterBar({required this.child});
8 |
9 | @override
10 | State createState() => _FilterBarState();
11 | }
12 |
13 | class _FilterBarState, T>
14 | extends State<_FilterBar> {
15 | late final theme = PagedDataTableTheme.of(context);
16 | late final controller = TableControllerProvider.of(context);
17 |
18 | final chipsListController = ScrollController();
19 |
20 | @override
21 | void didChangeDependencies() {
22 | super.didChangeDependencies();
23 |
24 | controller.addListener(_onChanged);
25 | }
26 |
27 | @override
28 | Widget build(BuildContext context) {
29 | var localizations = PagedDataTableLocalization.of(context);
30 |
31 | Widget child = SizedBox(
32 | height: theme.filterBarHeight,
33 | child: Row(
34 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
35 | children: [
36 | Flexible(
37 | child: Row(
38 | children: [
39 | /* FILTER BUTTON */
40 | if (controller._filtersState.isNotEmpty)
41 | Container(
42 | padding: theme.cellPadding,
43 | margin: theme.padding,
44 | child: Ink(
45 | child: InkWell(
46 | radius: 20,
47 | child: Tooltip(
48 | message: localizations.showFilterMenuTooltip,
49 | child: MouseRegion(
50 | cursor: controller._state == _TableState.fetching
51 | ? SystemMouseCursors.basic
52 | : SystemMouseCursors.click,
53 | child: GestureDetector(
54 | onTapDown:
55 | controller._state == _TableState.fetching
56 | ? null
57 | : (details) =>
58 | _showFilterOverlay(details, context),
59 | child: const Icon(Icons.filter_list_rounded),
60 | ),
61 | ),
62 | ),
63 | ),
64 | ),
65 | ),
66 |
67 | /* SELECTED FILTERS */
68 | Expanded(
69 | child: Scrollbar(
70 | controller: chipsListController,
71 | trackVisibility: true,
72 | child: SingleChildScrollView(
73 | controller: chipsListController,
74 | scrollDirection: Axis.horizontal,
75 | child: Row(
76 | children: controller._filtersState.values
77 | .where((element) => element.value != null)
78 | .map(
79 | (e) => Padding(
80 | padding:
81 | const EdgeInsets.symmetric(horizontal: 4.0),
82 | child: Chip(
83 | deleteIcon: const Icon(
84 | Icons.close,
85 | size: 20,
86 | ),
87 | deleteButtonTooltipMessage:
88 | "Remove filter", //localizations.removeFilterButtonText,
89 | onDeleted: () {
90 | controller.removeFilter(e._filter.id);
91 | },
92 | label: Text((e._filter as dynamic)
93 | .chipFormatter(e.value)),
94 | ),
95 | ),
96 | )
97 | .toList(),
98 | ),
99 | ),
100 | ),
101 | ),
102 | ],
103 | ),
104 | ),
105 | if (widget.child != null) widget.child!,
106 | ],
107 | ),
108 | );
109 |
110 | if (theme.chipTheme != null) {
111 | child = ChipTheme(
112 | data: theme.chipTheme!,
113 | child: child,
114 | );
115 | }
116 |
117 | return child;
118 | }
119 |
120 | Future _showFilterOverlay(
121 | TapDownDetails details, BuildContext context) {
122 | final mediaWidth = MediaQuery.of(context).size.width;
123 | final bool isBottomSheet = mediaWidth < theme.filterDialogBreakpoint;
124 |
125 | if (isBottomSheet) {
126 | return showModalBottomSheet(
127 | barrierLabel:
128 | MaterialLocalizations.of(context).modalBarrierDismissLabel,
129 | context: context,
130 | builder: (context) => _FiltersDialog(
131 | availableWidth: mediaWidth,
132 | rect: null,
133 | tableController: controller,
134 | ),
135 | );
136 | }
137 |
138 | final RenderBox renderBox = context.findRenderObject() as RenderBox;
139 | final offset = renderBox.localToGlobal(Offset.zero);
140 | final rect = RelativeRect.fromLTRB(
141 | offset.dx + 10, offset.dy + renderBox.size.height - 10, 0, 0);
142 |
143 | return showDialog(
144 | context: context,
145 | barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
146 | barrierDismissible: true,
147 | barrierColor: Colors.transparent,
148 | builder: (context) => _FiltersDialog(
149 | availableWidth: mediaWidth,
150 | rect: rect,
151 | tableController: controller,
152 | ),
153 | );
154 | }
155 |
156 | void _onChanged() {
157 | setState(() {});
158 | }
159 |
160 | @override
161 | void dispose() {
162 | super.dispose();
163 | chipsListController.dispose();
164 | controller.removeListener(_onChanged);
165 | }
166 | }
167 |
168 | class _FiltersDialog, T> extends StatelessWidget {
169 | final RelativeRect? rect;
170 | final PagedDataTableController tableController;
171 | final double availableWidth;
172 |
173 | const _FiltersDialog(
174 | {required this.rect,
175 | required this.availableWidth,
176 | required this.tableController});
177 |
178 | @override
179 | Widget build(BuildContext context) {
180 | final localizations = PagedDataTableLocalization.of(context);
181 |
182 | Widget filtersList = Padding(
183 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8.0),
184 | child: Form(
185 | key: tableController._filtersFormKey,
186 | child: Column(
187 | crossAxisAlignment: CrossAxisAlignment.stretch,
188 | children: [
189 | Text(localizations.filterByTitle,
190 | style: const TextStyle(fontWeight: FontWeight.bold)),
191 | const SizedBox(height: 8),
192 | ...tableController._filtersState.entries
193 | .where((element) => element.value._filter.visible)
194 | .map(
195 | (entry) => Padding(
196 | padding: const EdgeInsets.symmetric(vertical: 6),
197 | child:
198 | entry.value._filter.buildPicker(context, entry.value),
199 | ),
200 | )
201 | ],
202 | ),
203 | ),
204 | );
205 |
206 | final buttons = Padding(
207 | padding: const EdgeInsets.all(8),
208 | child: Row(
209 | children: [
210 | TextButton(
211 | style: TextButton.styleFrom(
212 | foregroundColor: Theme.of(context).colorScheme.secondary,
213 | padding:
214 | const EdgeInsets.symmetric(horizontal: 30, vertical: 20)),
215 | onPressed: () {
216 | Navigator.pop(context);
217 | tableController.removeFilters();
218 | },
219 | child: Text(localizations.removeAllFiltersButtonText),
220 | ),
221 | const Spacer(),
222 | TextButton(
223 | style: TextButton.styleFrom(
224 | padding:
225 | const EdgeInsets.symmetric(horizontal: 30, vertical: 20)),
226 | onPressed: () {
227 | Navigator.pop(context);
228 | },
229 | child: Text(localizations.cancelFilteringButtonText),
230 | ),
231 | const SizedBox(width: 10),
232 | FilledButton(
233 | style: FilledButton.styleFrom(
234 | padding:
235 | const EdgeInsets.symmetric(horizontal: 30, vertical: 20)),
236 | onPressed: () {
237 | // to ensure onSaved is called on filters
238 | tableController._filtersFormKey.currentState!.save();
239 | Navigator.pop(context);
240 | tableController.applyFilters();
241 | },
242 | child: Text(localizations.applyFilterButtonText),
243 | ),
244 | ],
245 | ),
246 | );
247 |
248 | if (rect == null) {
249 | filtersList = Expanded(child: filtersList);
250 | }
251 |
252 | Widget child = Material(
253 | shape: const RoundedRectangleBorder(
254 | borderRadius: BorderRadius.all(Radius.circular(28))),
255 | elevation: 0,
256 | child: Column(
257 | crossAxisAlignment: CrossAxisAlignment.stretch,
258 | children: [
259 | filtersList,
260 | const Divider(height: 0, color: Color(0xFFD6D6D6)),
261 | buttons,
262 | ],
263 | ),
264 | );
265 |
266 | if (rect != null) {
267 | return Stack(
268 | fit: StackFit.loose,
269 | children: [
270 | Positioned(
271 | top: rect!.top,
272 | left: rect!.left,
273 | child: Container(
274 | width: availableWidth / 3,
275 | decoration: const BoxDecoration(
276 | color: Colors.white,
277 | boxShadow: [BoxShadow(blurRadius: 3, color: Colors.black54)],
278 | borderRadius: BorderRadius.all(Radius.circular(28)),
279 | ),
280 | child: child,
281 | ),
282 | ),
283 | ],
284 | );
285 | }
286 |
287 | return child;
288 | }
289 | }
290 |
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter/services.dart';
6 | import 'package:flutter_localizations/flutter_localizations.dart';
7 | import 'package:intl/intl.dart';
8 | import 'package:paged_datatable/paged_datatable.dart';
9 | import 'package:paged_datatable_example/post.dart';
10 | import 'package:google_fonts/google_fonts.dart';
11 |
12 | Future main() async {
13 | WidgetsFlutterBinding.ensureInitialized();
14 | // await initializeDateFormatting("en");
15 |
16 | PostsRepository.generate(500);
17 |
18 | runApp(const MyApp());
19 | }
20 |
21 | class MyApp extends StatelessWidget {
22 | const MyApp({Key? key}) : super(key: key);
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return MaterialApp(
27 | localizationsDelegates: const [
28 | GlobalMaterialLocalizations.delegate,
29 | GlobalCupertinoLocalizations.delegate,
30 | GlobalWidgetsLocalizations.delegate,
31 | PagedDataTableLocalization.delegate
32 | ],
33 | supportedLocales: const [
34 | Locale("es"),
35 | Locale("en"),
36 | Locale("de"),
37 | Locale("it"),
38 | ],
39 | locale: const Locale("en"),
40 | title: 'Flutter Demo',
41 | debugShowCheckedModeBanner: false,
42 | theme: ThemeData(
43 | useMaterial3: true,
44 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
45 | textTheme: kIsWeb ? GoogleFonts.robotoTextTheme() : null,
46 | ),
47 | home: const MainView(),
48 | );
49 | }
50 | }
51 |
52 | class MainView extends StatefulWidget {
53 | const MainView({Key? key}) : super(key: key);
54 |
55 | @override
56 | State createState() => _MainViewState();
57 | }
58 |
59 | class _MainViewState extends State {
60 | final tableController = PagedDataTableController();
61 |
62 | @override
63 | Widget build(BuildContext context) {
64 | return Scaffold(
65 | body: Container(
66 | color: const Color.fromARGB(255, 208, 208, 208),
67 | padding: const EdgeInsets.all(20.0),
68 | child: Column(
69 | children: [
70 | TextButton(
71 | child: const Text("Print debug"),
72 | onPressed: () {
73 | tableController.printDebugString();
74 | },
75 | ),
76 | Expanded(
77 | child: PagedDataTableTheme(
78 | data: PagedDataTableThemeData(
79 | selectedRow: const Color(0xFFCE93D8),
80 | rowColor: (index) => index.isEven ? Colors.purple[50] : null,
81 | ),
82 | child: PagedDataTable(
83 | controller: tableController,
84 | initialPageSize: 100,
85 | configuration: const PagedDataTableConfiguration(),
86 | pageSizes: const [10, 20, 50, 100],
87 | fetcher: (pageSize, sortModel, filterModel, pageToken) async {
88 | final data = await PostsRepository.getPosts(
89 | pageSize: pageSize,
90 | pageToken: pageToken,
91 | sortBy: sortModel?.fieldName,
92 | sortDescending: sortModel?.descending ?? false,
93 | gender: filterModel["authorGender"],
94 | searchQuery: filterModel["content"],
95 | );
96 | return (data.items, data.nextPageToken);
97 | },
98 | filters: [
99 | TextTableFilter(
100 | id: "content",
101 | chipFormatter: (value) => 'Content has "$value"',
102 | name: "Content",
103 | ),
104 | DropdownTableFilter(
105 | items: Gender.values
106 | .map((e) =>
107 | DropdownMenuItem(value: e, child: Text(e.name)))
108 | .toList(growable: false),
109 | chipFormatter: (value) =>
110 | 'Author is ${value.name.toLowerCase()}',
111 | id: "authorGender",
112 | name: "Author's Gender",
113 | ),
114 | DateTimePickerTableFilter(
115 | id: "1",
116 | name: "Date picker",
117 | chipFormatter: (date) => "Date is $date",
118 | initialValue: DateTime.now(),
119 | firstDate:
120 | DateTime.now().subtract(const Duration(days: 30)),
121 | lastDate: DateTime.now(),
122 | dateFormat: DateFormat.yMd(),
123 | ),
124 | DateRangePickerTableFilter(
125 | id: "2",
126 | name: "DateRange picker",
127 | chipFormatter: (date) => "Date is $date",
128 | initialValue: null,
129 | firstDate:
130 | DateTime.now().subtract(const Duration(days: 30)),
131 | lastDate: DateTime.now(),
132 | formatter: (range) => "${range.start} - ${range.end}",
133 | ),
134 | ],
135 | filterBarChild: PopupMenuButton(
136 | icon: const Icon(Icons.more_vert_outlined),
137 | itemBuilder: (context) => [
138 | PopupMenuItem(
139 | child: const Text("Print selected rows"),
140 | onTap: () {
141 | debugPrint(tableController.selectedRows.toString());
142 | debugPrint(tableController.selectedItems.toString());
143 | },
144 | ),
145 | PopupMenuItem(
146 | child: const Text("Select random row"),
147 | onTap: () {
148 | final index =
149 | Random().nextInt(tableController.totalItems);
150 | tableController.selectRow(index);
151 | },
152 | ),
153 | PopupMenuItem(
154 | child: const Text("Select all rows"),
155 | onTap: () {
156 | tableController.selectAllRows();
157 | },
158 | ),
159 | PopupMenuItem(
160 | child: const Text("Unselect all rows"),
161 | onTap: () {
162 | tableController.unselectAllRows();
163 | },
164 | ),
165 | const PopupMenuDivider(),
166 | PopupMenuItem(
167 | child: const Text("Remove first row"),
168 | onTap: () {
169 | tableController.removeRowAt(0);
170 | },
171 | ),
172 | PopupMenuItem(
173 | child: const Text("Remove last row"),
174 | onTap: () {
175 | tableController
176 | .removeRowAt(tableController.totalItems - 1);
177 | },
178 | ),
179 | PopupMenuItem(
180 | child: const Text("Remove random row"),
181 | onTap: () {
182 | final index =
183 | Random().nextInt(tableController.totalItems);
184 | tableController.removeRowAt(index);
185 | },
186 | ),
187 | ],
188 | ),
189 | fixedColumnCount: 2,
190 | columns: [
191 | RowSelectorColumn(),
192 | TableColumn(
193 | title: const Text("Id"),
194 | cellBuilder: (context, item, index) =>
195 | Text(item.id.toString()),
196 | size: const FixedColumnSize(100),
197 | ),
198 | TableColumn(
199 | title: const Text("Author"),
200 | cellBuilder: (context, item, index) => Text(item.author),
201 | sortable: true,
202 | id: "author",
203 | size: const FractionalColumnSize(.15),
204 | ),
205 | DropdownTableColumn(
206 | title: const Text("Enabled"),
207 | // cellBuilder: (context, item, index) => Text(item.isEnabled ? "Yes" : "No"),
208 | items: const >[
209 | DropdownMenuItem(value: true, child: Text("Yes")),
210 | DropdownMenuItem(value: false, child: Text("No")),
211 | ],
212 | size: const FixedColumnSize(100),
213 | getter: (item, index) => item.isEnabled,
214 | setter: (item, newValue, index) async {
215 | await Future.delayed(const Duration(seconds: 2));
216 | item.isEnabled = newValue;
217 | return true;
218 | },
219 | ),
220 | TableColumn(
221 | title: const Text("Author Gender"),
222 | cellBuilder: (context, item, index) =>
223 | Text(item.authorGender.name),
224 | sortable: true,
225 | id: "authorGender",
226 | size: const MaxColumnSize(
227 | FractionalColumnSize(.2), FixedColumnSize(100)),
228 | ),
229 | LargeTextTableColumn(
230 | title: const Text("Content"),
231 | size: const RemainingColumnSize(),
232 | getter: (item, index) => item.content,
233 | fieldLabel: "Content",
234 | setter: (item, newValue, index) async {
235 | await Future.delayed(const Duration(seconds: 2));
236 | item.content = newValue;
237 | return true;
238 | },
239 | ),
240 | TextTableColumn(
241 | title: const Text("Number"),
242 | format: const NumericColumnFormat(),
243 | // cellBuilder: (context, item, index) => Text(item.number.toString()),
244 | size: const MaxColumnSize(
245 | FixedColumnSize(100), FractionalColumnSize(.1)),
246 | getter: (item, index) => item.number.toString(),
247 | inputFormatters: [FilteringTextInputFormatter.digitsOnly],
248 | setter: (item, newValue, index) async {
249 | await Future.delayed(const Duration(seconds: 2));
250 | item.number = int.parse(newValue);
251 | return true;
252 | },
253 | ),
254 | ],
255 | ),
256 | ),
257 | ),
258 | ],
259 | ),
260 | ),
261 | );
262 | }
263 |
264 | @override
265 | void dispose() {
266 | super.dispose();
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/lib/src/linked_scroll_controller.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2018 the Dart project authors.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd
6 |
7 | /*
8 | TAKEN FROM https://github.com/google/flutter.widgets/tree/master/packages/linked_scroll_controller
9 | */
10 |
11 | import 'package:flutter/material.dart';
12 | import 'package:flutter/rendering.dart';
13 |
14 | /// Sets up a collection of scroll controllers that mirror their movements to
15 | /// each other.
16 | ///
17 | /// Controllers are added and returned via [addAndGet]. The initial offset
18 | /// of the newly created controller is synced to the current offset.
19 | /// Controllers must be `dispose`d when no longer in use to prevent memory
20 | /// leaks and performance degradation.
21 | ///
22 | /// If controllers are disposed over the course of the lifetime of this
23 | /// object the corresponding scrollables should be given unique keys.
24 | /// Without the keys, Flutter may reuse a controller after it has been disposed,
25 | /// which can cause the controller offsets to fall out of sync.
26 | class LinkedScrollControllerGroup {
27 | LinkedScrollControllerGroup() {
28 | _offsetNotifier = _LinkedScrollControllerGroupOffsetNotifier(this);
29 | }
30 |
31 | final _allControllers = <_LinkedScrollController>[];
32 |
33 | late _LinkedScrollControllerGroupOffsetNotifier _offsetNotifier;
34 |
35 | /// The current scroll offset of the group.
36 | double get offset {
37 | assert(
38 | _attachedControllers.isNotEmpty,
39 | 'LinkedScrollControllerGroup does not have any scroll controllers '
40 | 'attached.',
41 | );
42 | return _attachedControllers.first.offset;
43 | }
44 |
45 | /// Creates a new controller that is linked to any existing ones.
46 | ScrollController addAndGet() {
47 | final initialScrollOffset = _attachedControllers.isEmpty
48 | ? 0.0
49 | : _attachedControllers.first.position.pixels;
50 | final controller =
51 | _LinkedScrollController(this, initialScrollOffset: initialScrollOffset);
52 | _allControllers.add(controller);
53 | controller.addListener(_offsetNotifier.notifyListeners);
54 | return controller;
55 | }
56 |
57 | /// Adds a callback that will be called when the value of [offset] changes.
58 | void addOffsetChangedListener(VoidCallback onChanged) {
59 | _offsetNotifier.addListener(onChanged);
60 | }
61 |
62 | /// Removes the specified offset changed listener.
63 | void removeOffsetChangedListener(VoidCallback listener) {
64 | _offsetNotifier.removeListener(listener);
65 | }
66 |
67 | Iterable<_LinkedScrollController> get _attachedControllers =>
68 | _allControllers.where((controller) => controller.hasClients);
69 |
70 | /// Animates the scroll position of all linked controllers to [offset].
71 | Future animateTo(
72 | double offset, {
73 | required Curve curve,
74 | required Duration duration,
75 | }) async {
76 | final animations = >[];
77 | for (final controller in _attachedControllers) {
78 | animations
79 | .add(controller.animateTo(offset, duration: duration, curve: curve));
80 | }
81 | return Future.wait(animations).then((List _) => null);
82 | }
83 |
84 | /// Jumps the scroll position of all linked controllers to [value].
85 | void jumpTo(double value) {
86 | for (final controller in _attachedControllers) {
87 | controller.jumpTo(value);
88 | }
89 | }
90 |
91 | /// Resets the scroll position of all linked controllers to 0.
92 | void resetScroll() {
93 | jumpTo(0.0);
94 | }
95 | }
96 |
97 | /// This class provides change notification for [LinkedScrollControllerGroup]'s
98 | /// scroll offset.
99 | ///
100 | /// This change notifier de-duplicates change events by only firing listeners
101 | /// when the scroll offset of the group has changed.
102 | class _LinkedScrollControllerGroupOffsetNotifier extends ChangeNotifier {
103 | _LinkedScrollControllerGroupOffsetNotifier(this.controllerGroup);
104 |
105 | final LinkedScrollControllerGroup controllerGroup;
106 |
107 | /// The cached offset for the group.
108 | ///
109 | /// This value will be used in determining whether to notify listeners.
110 | double? _cachedOffset;
111 |
112 | @override
113 | void notifyListeners() {
114 | final currentOffset = controllerGroup.offset;
115 | if (currentOffset != _cachedOffset) {
116 | _cachedOffset = currentOffset;
117 | super.notifyListeners();
118 | }
119 | }
120 | }
121 |
122 | /// A scroll controller that mirrors its movements to a peer, which must also
123 | /// be a [_LinkedScrollController].
124 | class _LinkedScrollController extends ScrollController {
125 | final LinkedScrollControllerGroup _controllers;
126 |
127 | _LinkedScrollController(this._controllers,
128 | {required super.initialScrollOffset})
129 | : super(keepScrollOffset: false);
130 |
131 | @override
132 | void dispose() {
133 | _controllers._allControllers.remove(this);
134 | super.dispose();
135 | }
136 |
137 | @override
138 | void attach(ScrollPosition position) {
139 | assert(
140 | position is _LinkedScrollPosition,
141 | '_LinkedScrollControllers can only be used with'
142 | ' _LinkedScrollPositions.');
143 | final _LinkedScrollPosition linkedPosition =
144 | position as _LinkedScrollPosition;
145 | assert(linkedPosition.owner == this,
146 | '_LinkedScrollPosition cannot change controllers once created.');
147 | super.attach(position);
148 | }
149 |
150 | @override
151 | _LinkedScrollPosition createScrollPosition(ScrollPhysics physics,
152 | ScrollContext context, ScrollPosition? oldPosition) {
153 | return _LinkedScrollPosition(
154 | this,
155 | physics: physics,
156 | context: context,
157 | initialPixels: initialScrollOffset,
158 | oldPosition: oldPosition,
159 | );
160 | }
161 |
162 | @override
163 | double get initialScrollOffset => _controllers._attachedControllers.isEmpty
164 | ? super.initialScrollOffset
165 | : _controllers.offset;
166 |
167 | @override
168 | _LinkedScrollPosition get position => super.position as _LinkedScrollPosition;
169 |
170 | Iterable<_LinkedScrollController> get _allPeersWithClients =>
171 | _controllers._attachedControllers.where((peer) => peer != this);
172 |
173 | bool get canLinkWithPeers => _allPeersWithClients.isNotEmpty;
174 |
175 | Iterable<_LinkedScrollActivity> linkWithPeers(_LinkedScrollPosition driver) {
176 | assert(canLinkWithPeers);
177 | return _allPeersWithClients
178 | .map((peer) => peer.link(driver))
179 | .expand((e) => e);
180 | }
181 |
182 | Iterable<_LinkedScrollActivity> link(_LinkedScrollPosition driver) {
183 | assert(hasClients);
184 | final activities = <_LinkedScrollActivity>[];
185 | for (final position in positions) {
186 | final linkedPosition = position as _LinkedScrollPosition;
187 | activities.add(linkedPosition.link(driver));
188 | }
189 | return activities;
190 | }
191 | }
192 |
193 | // Implementation details: Whenever position.setPixels or position.forcePixels
194 | // is called on a _LinkedScrollPosition (which may happen programmatically, or
195 | // as a result of a user action), the _LinkedScrollPosition creates a
196 | // _LinkedScrollActivity for each linked position and uses it to move to or jump
197 | // to the appropriate offset.
198 | //
199 | // When a new activity begins, the set of peer activities is cleared.
200 | class _LinkedScrollPosition extends ScrollPositionWithSingleContext {
201 | _LinkedScrollPosition(
202 | this.owner, {
203 | required super.physics,
204 | required super.context,
205 | super.initialPixels = null,
206 | super.oldPosition,
207 | });
208 |
209 | final _LinkedScrollController owner;
210 |
211 | final Set<_LinkedScrollActivity> _peerActivities = <_LinkedScrollActivity>{};
212 |
213 | // We override hold to propagate it to all peer controllers.
214 | @override
215 | ScrollHoldController hold(VoidCallback holdCancelCallback) {
216 | for (final controller in owner._allPeersWithClients) {
217 | controller.position._holdInternal();
218 | }
219 | return super.hold(holdCancelCallback);
220 | }
221 |
222 | // Calls hold without propagating to peers.
223 | void _holdInternal() {
224 | super.hold(() {});
225 | }
226 |
227 | @override
228 | void beginActivity(ScrollActivity? newActivity) {
229 | if (newActivity == null) {
230 | return;
231 | }
232 | for (var activity in _peerActivities) {
233 | activity.unlink(this);
234 | }
235 |
236 | _peerActivities.clear();
237 |
238 | super.beginActivity(newActivity);
239 | }
240 |
241 | @override
242 | double setPixels(double newPixels) {
243 | if (newPixels == pixels) {
244 | return 0.0;
245 | }
246 | updateUserScrollDirection(newPixels - pixels > 0.0
247 | ? ScrollDirection.forward
248 | : ScrollDirection.reverse);
249 |
250 | if (owner.canLinkWithPeers) {
251 | _peerActivities.addAll(owner.linkWithPeers(this));
252 | for (var activity in _peerActivities) {
253 | activity.moveTo(newPixels);
254 | }
255 | }
256 |
257 | return setPixelsInternal(newPixels);
258 | }
259 |
260 | double setPixelsInternal(double newPixels) {
261 | return super.setPixels(newPixels);
262 | }
263 |
264 | @override
265 | void forcePixels(double value) {
266 | if (value == pixels) {
267 | return;
268 | }
269 | updateUserScrollDirection(value - pixels > 0.0
270 | ? ScrollDirection.forward
271 | : ScrollDirection.reverse);
272 |
273 | if (owner.canLinkWithPeers) {
274 | _peerActivities.addAll(owner.linkWithPeers(this));
275 | for (var activity in _peerActivities) {
276 | activity.jumpTo(value);
277 | }
278 | }
279 |
280 | forcePixelsInternal(value);
281 | }
282 |
283 | void forcePixelsInternal(double value) {
284 | super.forcePixels(value);
285 | }
286 |
287 | _LinkedScrollActivity link(_LinkedScrollPosition driver) {
288 | if (this.activity is! _LinkedScrollActivity) {
289 | beginActivity(_LinkedScrollActivity(this));
290 | }
291 | final _LinkedScrollActivity activity =
292 | this.activity as _LinkedScrollActivity;
293 | activity.link(driver);
294 | return activity;
295 | }
296 |
297 | void unlink(_LinkedScrollActivity activity) {
298 | _peerActivities.remove(activity);
299 | }
300 |
301 | // We override this method to make it public (overridden method is protected)
302 | @override
303 | void updateUserScrollDirection(ScrollDirection value) {
304 | super.updateUserScrollDirection(value);
305 | }
306 |
307 | @override
308 | void debugFillDescription(List description) {
309 | super.debugFillDescription(description);
310 | description.add('owner: $owner');
311 | }
312 | }
313 |
314 | class _LinkedScrollActivity extends ScrollActivity {
315 | _LinkedScrollActivity(_LinkedScrollPosition super.delegate);
316 |
317 | @override
318 | _LinkedScrollPosition get delegate => super.delegate as _LinkedScrollPosition;
319 |
320 | final Set<_LinkedScrollPosition> drivers = <_LinkedScrollPosition>{};
321 |
322 | void link(_LinkedScrollPosition driver) {
323 | drivers.add(driver);
324 | }
325 |
326 | void unlink(_LinkedScrollPosition driver) {
327 | drivers.remove(driver);
328 | if (drivers.isEmpty) {
329 | delegate.goIdle();
330 | }
331 | }
332 |
333 | @override
334 | bool get shouldIgnorePointer => true;
335 |
336 | @override
337 | bool get isScrolling => true;
338 |
339 | // _LinkedScrollActivity is not self-driven but moved by calls to the [moveTo]
340 | // method.
341 | @override
342 | double get velocity => 0.0;
343 |
344 | void moveTo(double newPixels) {
345 | _updateUserScrollDirection();
346 | delegate.setPixelsInternal(newPixels);
347 | }
348 |
349 | void jumpTo(double newPixels) {
350 | _updateUserScrollDirection();
351 | delegate.forcePixelsInternal(newPixels);
352 | }
353 |
354 | void _updateUserScrollDirection() {
355 | assert(drivers.isNotEmpty);
356 | ScrollDirection commonDirection = drivers.first.userScrollDirection;
357 | for (var driver in drivers) {
358 | if (driver.userScrollDirection != commonDirection) {
359 | commonDirection = ScrollDirection.idle;
360 | }
361 | }
362 | delegate.updateUserScrollDirection(commonDirection);
363 | }
364 |
365 | @override
366 | void dispose() {
367 | for (var driver in drivers) {
368 | driver.unlink(this);
369 | }
370 | super.dispose();
371 | }
372 | }
373 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PagedDataTable
2 |
3 | [](https://pub.dev/packages/paged_datatable)
4 |
5 | Completely customisable data table which supports cursor and offset pagination, filters and horizontal scrolling out-of-the-box. It's written from scratch, no dependency from Flutter's `DataTable` nor `Table`.
6 | Designed to follow Google's Material You style.
7 |
8 | ## Online demo
9 |
10 | Check it out here
11 |
12 | ## Features
13 |
14 | - **Horizontal scrolling**, allowing you to define columns wider than the viewport width.
15 | - **Fixed columns**, to scroll horizontally only a set of columns.
16 | - **Row updating on demand**, preventing you to create other views for updating fields of a class. Now you can update an object from the table directly.
17 | - **Cursor and offset pagination**, you decide how to paginate your data.
18 | - **Filtering** by date, text, number, whatever you want!
19 | - **Sorting** by predefined columns
20 | - **Page modification** using controller, to add or remove items to the current page without reloading the entire table.
21 | - **Themeable**, allowing you to change colors, fonts, text styles, and more!
22 |
23 | ## Table Of Contents
24 |
25 | - [Setup](#setup)
26 | - [Fetcher](#fetcher)
27 | - [Header](#header)
28 | - [Footer](#footer)
29 | - [Custom footer](#custom-footer)
30 | - [Columns](#columns)
31 | - [TableColumn](#tablecolumnk-t)
32 | - [EditableTableColumn](#editabletablecolumnk-t-v)
33 | - [Custom columns](#custom-columns)
34 | - [Filters](#filters)
35 | - [Custom filters](#custom-filters)
36 | - [Controller](#controller)
37 | - [Internationalization](#internationalization)
38 | - [Screenshots](#screenshots)
39 | - [Contributing](#contribute)
40 |
41 | ## Setup
42 |
43 | Everything you need is a **PagedDataTable** widget, which accepts two generic arguments, **K** and **T**, the type of object you will use as paging key and the type of object you will be showing in the table.
44 |
45 | > Keep in mind that `K` must extends `Comparable`.
46 |
47 | There are only two required parameters: `columns` and `fetcher`.
48 |
49 | ```dart
50 | PagedDataTable(
51 | fetcher: (int pageSize, SortModel? sortModel, FilterModel filterModel, String? pageToken) => ...,
52 | columns: [...],
53 | )
54 | ```
55 |
56 | ### Fetcher
57 |
58 | The fetcher is a function that gets called every time a page is requested. It gives you the current page size, sort and filter models and the page token that is requested. It must return
59 | a `FutureOr<(List, K?)>`, so you can convert it to a Future and do `async`-like requests or simply return the data.
60 |
61 | It expects a tuple, where the first value is the list of items and the second the next page token:
62 |
63 | ```dart
64 | PagedDataTable(
65 | fetcher: (int pageSize, SortModel? sortModel, FilterModel filterModel, String? pageToken) {
66 | final result = await FetchService.listPosts();
67 | return (result.data, result.nextPageToken);
68 | },
69 | columns: [...],
70 | )
71 | ```
72 |
73 | By default, `PagedDataTable` does not copy the returned list, so, if it is a shared list, you or the table may modify the items. If you want, `PagedDataTable` can copy the list
74 | if you specify it in the `configuration` property:
75 |
76 | > Note that `PagedDataTable` **DOES NOT** cache pages.
77 |
78 | ```dart
79 | PagedDataTable(
80 | configuration: PagedDataTableConfiguration(
81 | copyItems: true,
82 | ),
83 | )
84 | ```
85 |
86 | ### Header
87 |
88 | The header renders the column names, but also the FilterBar exists, which is an additional header that renders the filter picker and, optionally, you can display additional widgets aligned at the right of the bar.
89 |
90 | Just pass your widget to the `filterBarChild` property. Naturally you would want to display a `PopupMenuButton` that will act as a menu.
91 |
92 | ### Footer
93 |
94 | Using the `footer` property you can render anything. If you don't pass it, it will render the `DefaultFooter` widget,
95 | which again, if not specified, will display, aligned to the right, the following widgets:
96 |
97 | - **Refresh button**: A button that can be used to refresh the current dataset.
98 | - **Page size selector**: A dropdown that can be used to select the current page size to use, based on the `pageSizes` property.
99 | - **Current page display**: Displays the current page number.
100 | - **Navigation buttons**: will display the previous and next buttons as `IconButton`s.
101 |
102 | #### Custom footer
103 |
104 | If you want your own footer widget but reuse some of the already existing widgets, they are named: `RefreshButton`, `PageSizeSelector`, `CurrentPage` and `NavigationButtons`.
105 |
106 | ### Columns
107 |
108 | There are two types of columns in `PagedDataTable`:
109 |
110 | - **ReadOnlyTableColumn**: renders a simple widget that does not allow edition.
111 | - **EditableTableColumn**: renders a simple widget too, but this can be modified in place and modify the dataset.
112 |
113 | > `K` and `T` are the same parameters defined in the `PagedDataTable` widget.
114 |
115 | Every column type has:
116 |
117 | - **title**: the column's title. It is a widget but commonly it's a `Text` widget displaying the name.
118 | - **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 use `FixedColumnSize`, `FractionalColumnSize`, `RemainingColumnSize` and `MaxColumnSize`.
119 | - **format**: applies a transformation to the cell's widget. You have `NumericColumnFormat`, which aligns content to the right and `AlignColumnFormat` which aligns cell's content to the `alignment` property and you can implement your own implemeting the `ColumnFormat` interface.
120 | - **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's `SortModel`. To sort, you click the column's header.
121 | - There are other properties that you can use to play around and modify your columns. Check out the `ReadOnlyTableColumn`'s documentation.
122 |
123 | > If you want to fix columns at the left, you can specify the amount of columns to fix using the `fixedColumnCount` property.
124 |
125 | #### TableColumn
126 |
127 | Is the default `ReadOnlyTableColumn` that renders a cell using the `cellBuilder` property.
128 |
129 | ```dart
130 | PagedDataTable(
131 | columns: [
132 | TableColumn(
133 | title: const Text("Author"),
134 | cellBuilder: (context, item, index) => Text(item.author),
135 | ),
136 | ],
137 | )
138 | ```
139 |
140 | #### EditableTableColumn
141 |
142 | This abstract class provides two more properties, `getter` and `setter`. The first one is used to provide the value **V** to render and the second one
143 | is the function used to set the new value. It must return a boolean indicating if the operation succeeded or not. If is true, the cell will update its
144 | value, otherwise will keep the old one.
145 |
146 | There are three built in editable columns, which are `DropdownTableColumn` which renders a dropdown; `TextTableColumn` which renders a `Text` until double-clicked, then it renders a `TextField` used to edit the cell's content; `LargeTextTableColumn`, which is the same as `TextTableColumn` but when
147 | double-clicked, it opens an overlay, designed to edit large text cells.
148 |
149 | You can [create your own column](#custom-columns).
150 |
151 | #### Custom columns
152 |
153 | To create your own columns, simply extend `ReadOnlyTableColumn` or `EditableTableColumn` depending on your needs.
154 |
155 | For example:
156 |
157 | ```dart
158 | class MyColumnType extends ReadOnlyTableColumn {
159 | @override
160 | Widget build(BuildContext context, T item, int index) {
161 | return MyCellWidget();
162 | }
163 | }
164 | ```
165 |
166 | > If you want more examples, check out the implementation of the already existing column types.
167 |
168 | ## Filters
169 |
170 | `PagedDataTable` allows you to define a set of filters that you can use to interactively select them using a
171 | popup overlay or a bottom sheet if you are in a small device.
172 |
173 | To define filters, use the `filters` property:
174 |
175 | ```dart
176 | PagedDataTable(
177 | ...,
178 | filters: [
179 | TextTableFilter(
180 | id: "content",
181 | chipFormatter: (value) => 'Content has "$value"',
182 | name: "Content",
183 | ),
184 | DropdownTableFilter(
185 | items: Gender.values
186 | .map((e) =>
187 | DropdownMenuItem(value: e, child: Text(e.name)))
188 | .toList(growable: false),
189 | chipFormatter: (value) =>
190 | 'Author is ${value.name.toLowerCase()}',
191 | id: "authorGender",
192 | name: "Author's Gender",
193 | ),
194 | ],
195 | )
196 | ```
197 |
198 | There are five built-in filter types:
199 |
200 | - `TextTableFilter`: renders a `TextField` to filter by raw text.
201 | - `DropdownTableFilter`: renders a `DropdownButton` with a set of options.
202 | - `DateTimePickerTableFilter`: renders a `TextField` that, when tapped, opens the `DateTime` picker dialog.
203 | - `DateRangePickerTableFilter`: the same as `DateTimePickerTableFilter` but selects a `DateTimeRange`.
204 | - `ProgrammingTextFilter`: a filter that does not render nothing in the filter dialog but can be set using the controller.
205 |
206 | Every filter type must define, at least, the `id`, the `name` and the `chipFormatter` properties. The first one is the identifier of the filter, used in the fetcher's `FilterMode` property. The name is the label displayed in the filter picker and the `chipFormatter` is a function that maps the actual selected value to a more user-friendly string that is displayed in the selected filter's chip.
207 |
208 | ### Custom filters
209 |
210 | To create your own filter, extend the `TableFilter` abstract class, where `T` is the type of value the filter will handle. Then, implement the `buildPicker` function, which renders the actual filter picker.
211 |
212 | The implementation of the `TextTableFilter` as an example:
213 |
214 | ```dart
215 | final class TextTableFilter extends TableFilter {
216 | final InputDecoration? decoration;
217 |
218 | const TextTableFilter({
219 | this.decoration,
220 | required super.chipFormatter,
221 | required super.id,
222 | required super.name,
223 | super.initialValue,
224 | super.enabled = true,
225 | });
226 |
227 | @override
228 | Widget buildPicker(BuildContext context, FilterState state) {
229 | return TextFormField(
230 | decoration: decoration ?? InputDecoration(labelText: name),
231 | initialValue: state.value,
232 | onSaved: (newValue) {
233 | if (newValue != null && newValue.isNotEmpty) {
234 | state.value = newValue;
235 | }
236 | },
237 | );
238 | }
239 | }
240 | ```
241 |
242 | ## Controller
243 |
244 | If you want to control the table programatically, provide your own `PagedDataTableController` instance passing it to the `controller` property. It provides methods to interact with rows (selecting, unselecting, removing, inserting, updating), filters, sorting, pagination, and more.
245 |
246 | ## Internationalization
247 |
248 | Update your `MaterialApp` or `CupertinoApp` widget with the following:
249 |
250 | ```dart
251 | localizationsDelegates: const [
252 | PagedDataTableLocalization.delegate
253 | ],
254 |
255 | ```
256 |
257 | And you're done.
258 |
259 | At the moment of writing this, the supported locales are:
260 |
261 | - **es**: Spanish
262 | - **en**: English
263 | - **de**: Deutsch
264 |
265 | If you want more languages, you can [contribute](#contribute).
266 |
267 | ## Screenshots
268 |
269 | 
270 | 
271 | 
272 | 
273 | 
274 |
275 | ## Contribute
276 |
277 | Any suggestion to improve/add is welcome, if you want to make a PR, you are welcome :)
278 |
--------------------------------------------------------------------------------
/example/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | async:
5 | dependency: transitive
6 | description:
7 | name: async
8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
9 | url: "https://pub.dev"
10 | source: hosted
11 | version: "2.11.0"
12 | boolean_selector:
13 | dependency: transitive
14 | description:
15 | name: boolean_selector
16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
17 | url: "https://pub.dev"
18 | source: hosted
19 | version: "2.1.1"
20 | characters:
21 | dependency: transitive
22 | description:
23 | name: characters
24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
25 | url: "https://pub.dev"
26 | source: hosted
27 | version: "1.3.0"
28 | clock:
29 | dependency: transitive
30 | description:
31 | name: clock
32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
33 | url: "https://pub.dev"
34 | source: hosted
35 | version: "1.1.1"
36 | collection:
37 | dependency: transitive
38 | description:
39 | name: collection
40 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
41 | url: "https://pub.dev"
42 | source: hosted
43 | version: "1.18.0"
44 | crypto:
45 | dependency: transitive
46 | description:
47 | name: crypto
48 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
49 | url: "https://pub.dev"
50 | source: hosted
51 | version: "3.0.3"
52 | darq:
53 | dependency: "direct main"
54 | description:
55 | name: darq
56 | sha256: eb58203f550d113df1984cf6550b94b6951e515bed98eb49d098fb35719c78b0
57 | url: "https://pub.dev"
58 | source: hosted
59 | version: "2.0.0"
60 | equatable:
61 | dependency: "direct main"
62 | description:
63 | name: equatable
64 | sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
65 | url: "https://pub.dev"
66 | source: hosted
67 | version: "2.0.5"
68 | fake_async:
69 | dependency: transitive
70 | description:
71 | name: fake_async
72 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
73 | url: "https://pub.dev"
74 | source: hosted
75 | version: "1.3.1"
76 | faker:
77 | dependency: "direct main"
78 | description:
79 | name: faker
80 | sha256: "746e59f91d8b06a389e74cf76e909a05ed69c12691768e2f93557fdf29200fd0"
81 | url: "https://pub.dev"
82 | source: hosted
83 | version: "2.1.0"
84 | ffi:
85 | dependency: transitive
86 | description:
87 | name: ffi
88 | sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
89 | url: "https://pub.dev"
90 | source: hosted
91 | version: "2.1.2"
92 | flutter:
93 | dependency: "direct main"
94 | description: flutter
95 | source: sdk
96 | version: "0.0.0"
97 | flutter_lints:
98 | dependency: "direct dev"
99 | description:
100 | name: flutter_lints
101 | sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493
102 | url: "https://pub.dev"
103 | source: hosted
104 | version: "1.0.4"
105 | flutter_localizations:
106 | dependency: "direct main"
107 | description: flutter
108 | source: sdk
109 | version: "0.0.0"
110 | flutter_test:
111 | dependency: "direct dev"
112 | description: flutter
113 | source: sdk
114 | version: "0.0.0"
115 | google_fonts:
116 | dependency: "direct main"
117 | description:
118 | name: google_fonts
119 | sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
120 | url: "https://pub.dev"
121 | source: hosted
122 | version: "6.2.1"
123 | http:
124 | dependency: transitive
125 | description:
126 | name: http
127 | sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
128 | url: "https://pub.dev"
129 | source: hosted
130 | version: "1.2.1"
131 | http_parser:
132 | dependency: transitive
133 | description:
134 | name: http_parser
135 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
136 | url: "https://pub.dev"
137 | source: hosted
138 | version: "4.0.2"
139 | intl:
140 | dependency: "direct main"
141 | description:
142 | name: intl
143 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
144 | url: "https://pub.dev"
145 | source: hosted
146 | version: "0.19.0"
147 | leak_tracker:
148 | dependency: transitive
149 | description:
150 | name: leak_tracker
151 | sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
152 | url: "https://pub.dev"
153 | source: hosted
154 | version: "10.0.4"
155 | leak_tracker_flutter_testing:
156 | dependency: transitive
157 | description:
158 | name: leak_tracker_flutter_testing
159 | sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
160 | url: "https://pub.dev"
161 | source: hosted
162 | version: "3.0.3"
163 | leak_tracker_testing:
164 | dependency: transitive
165 | description:
166 | name: leak_tracker_testing
167 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
168 | url: "https://pub.dev"
169 | source: hosted
170 | version: "3.0.1"
171 | lints:
172 | dependency: transitive
173 | description:
174 | name: lints
175 | sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c
176 | url: "https://pub.dev"
177 | source: hosted
178 | version: "1.0.1"
179 | matcher:
180 | dependency: transitive
181 | description:
182 | name: matcher
183 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
184 | url: "https://pub.dev"
185 | source: hosted
186 | version: "0.12.16+1"
187 | material_color_utilities:
188 | dependency: transitive
189 | description:
190 | name: material_color_utilities
191 | sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
192 | url: "https://pub.dev"
193 | source: hosted
194 | version: "0.8.0"
195 | meta:
196 | dependency: transitive
197 | description:
198 | name: meta
199 | sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
200 | url: "https://pub.dev"
201 | source: hosted
202 | version: "1.12.0"
203 | paged_datatable:
204 | dependency: "direct main"
205 | description:
206 | path: ".."
207 | relative: true
208 | source: path
209 | version: "2.1.0"
210 | path:
211 | dependency: transitive
212 | description:
213 | name: path
214 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
215 | url: "https://pub.dev"
216 | source: hosted
217 | version: "1.9.0"
218 | path_provider:
219 | dependency: transitive
220 | description:
221 | name: path_provider
222 | sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
223 | url: "https://pub.dev"
224 | source: hosted
225 | version: "2.1.3"
226 | path_provider_android:
227 | dependency: transitive
228 | description:
229 | name: path_provider_android
230 | sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
231 | url: "https://pub.dev"
232 | source: hosted
233 | version: "2.2.5"
234 | path_provider_foundation:
235 | dependency: transitive
236 | description:
237 | name: path_provider_foundation
238 | sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
239 | url: "https://pub.dev"
240 | source: hosted
241 | version: "2.4.0"
242 | path_provider_linux:
243 | dependency: transitive
244 | description:
245 | name: path_provider_linux
246 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
247 | url: "https://pub.dev"
248 | source: hosted
249 | version: "2.2.1"
250 | path_provider_platform_interface:
251 | dependency: transitive
252 | description:
253 | name: path_provider_platform_interface
254 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
255 | url: "https://pub.dev"
256 | source: hosted
257 | version: "2.1.2"
258 | path_provider_windows:
259 | dependency: transitive
260 | description:
261 | name: path_provider_windows
262 | sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
263 | url: "https://pub.dev"
264 | source: hosted
265 | version: "2.2.1"
266 | platform:
267 | dependency: transitive
268 | description:
269 | name: platform
270 | sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
271 | url: "https://pub.dev"
272 | source: hosted
273 | version: "3.1.4"
274 | plugin_platform_interface:
275 | dependency: transitive
276 | description:
277 | name: plugin_platform_interface
278 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
279 | url: "https://pub.dev"
280 | source: hosted
281 | version: "2.1.8"
282 | sky_engine:
283 | dependency: transitive
284 | description: flutter
285 | source: sdk
286 | version: "0.0.99"
287 | source_span:
288 | dependency: transitive
289 | description:
290 | name: source_span
291 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
292 | url: "https://pub.dev"
293 | source: hosted
294 | version: "1.10.0"
295 | stack_trace:
296 | dependency: transitive
297 | description:
298 | name: stack_trace
299 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
300 | url: "https://pub.dev"
301 | source: hosted
302 | version: "1.11.1"
303 | stream_channel:
304 | dependency: transitive
305 | description:
306 | name: stream_channel
307 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
308 | url: "https://pub.dev"
309 | source: hosted
310 | version: "2.1.2"
311 | string_scanner:
312 | dependency: transitive
313 | description:
314 | name: string_scanner
315 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
316 | url: "https://pub.dev"
317 | source: hosted
318 | version: "1.2.0"
319 | term_glyph:
320 | dependency: transitive
321 | description:
322 | name: term_glyph
323 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
324 | url: "https://pub.dev"
325 | source: hosted
326 | version: "1.2.1"
327 | test_api:
328 | dependency: transitive
329 | description:
330 | name: test_api
331 | sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
332 | url: "https://pub.dev"
333 | source: hosted
334 | version: "0.7.0"
335 | typed_data:
336 | dependency: transitive
337 | description:
338 | name: typed_data
339 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
340 | url: "https://pub.dev"
341 | source: hosted
342 | version: "1.3.2"
343 | vector_math:
344 | dependency: transitive
345 | description:
346 | name: vector_math
347 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
348 | url: "https://pub.dev"
349 | source: hosted
350 | version: "2.1.4"
351 | vm_service:
352 | dependency: transitive
353 | description:
354 | name: vm_service
355 | sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
356 | url: "https://pub.dev"
357 | source: hosted
358 | version: "14.2.1"
359 | web:
360 | dependency: transitive
361 | description:
362 | name: web
363 | sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
364 | url: "https://pub.dev"
365 | source: hosted
366 | version: "0.5.1"
367 | win32:
368 | dependency: transitive
369 | description:
370 | name: win32
371 | sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
372 | url: "https://pub.dev"
373 | source: hosted
374 | version: "5.5.1"
375 | xdg_directories:
376 | dependency: transitive
377 | description:
378 | name: xdg_directories
379 | sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
380 | url: "https://pub.dev"
381 | source: hosted
382 | version: "1.0.4"
383 | sdks:
384 | dart: ">=3.4.0 <4.0.0"
385 | flutter: ">=3.22.0"
386 |
--------------------------------------------------------------------------------
/lib/src/controller.dart:
--------------------------------------------------------------------------------
1 | part of 'paged_datatable.dart';
2 |
3 | typedef Fetcher, T>
4 | = FutureOr<(List resultset, K? nextPageToken)> Function(int pageSize,
5 | SortModel? sortModel, FilterModel filterModel, K? pageToken);
6 |
7 | typedef RowChangeListener, T> = void Function(
8 | int index, T item);
9 |
10 | /// [PagedDataTableController] represents the state of a [PagedDataTable] of type [T], using pagination keys of type [K].
11 | ///
12 | /// Is recommended that [T] specifies a custom hashCode and equals method for comparison reasons.
13 | final class PagedDataTableController, T>
14 | extends ChangeNotifier {
15 | final List _currentDataset =
16 | []; // the current dataset that is being displayed
17 | final Map _filtersState =
18 | {}; // The list of filters' states
19 | final Map _paginationKeys =
20 | {}; // it's a map because on not found map will return null, list will throw
21 | final Set