官网:
https://pub.dev/packages/data_table_2
https://github.com/maxim-saplin/data_table_2
官方 flutter:samples (*****) https://blog.csdn.net/ken2232/article/details/143101285
到 2024 为止,flutter 官方维护的包,不多。不如 Qt 样例那么丰富,因此,采用第三方包,还是需要的。
-------------------------------------------------------------
用法:
- VSCode 打开项目文件夹;
- flutter pub get
- run /debug << OK
-------------------------------------------------------------
关联参考:
摘录:flutter third 数据表增强库 `data_table_2` 2.5.15 源码 (****) https://blog.csdn.net/ken2232/article/details/143167410
flutter third 数据表增强库 `data_table_2` 教程 (****) https://blog.csdn.net/ken2232/article/details/142760014
===================================
摘录:flutter third 数据表增强库 `data_table_2` 2.5.15 源码
目录结构
`data_table_2` :
- 摘录:`data_table_2` : DataCell 的数据结构
注:这个包的每一个数据都是人工给出;市场上有些包的数据源,采用随机数自动生成。
/// Domain model entity
class Dessert {
Dessert(
this.name,
this.calories,
this.fat,
this.carbs,
this.protein,
this.sodium,
this.calcium,
this.iron,
);
/// +++++++++++++++
List<Dessert> _desserts = <Dessert>[
Dessert(
'Frozen Yogurt',
159,
6.0,
24,
4.0,
87,
14,
1,
),
Dessert(
'Ice Cream Sandwich',
237,
9.0,
37,
4.3,
129,
8,
1,
),
对比:数据行的类
https://blog.csdn.net/ken2232/article/details/143157089
文件:data_item.dart
class DataItem {
final String text;
final bool selected;
const DataItem({this.text = '', this.selected = false});
}
- data_sources.dart
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:data_table_2/data_table_2.dart';
// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The file was extracted from GitHub: https://github.com/flutter/gallery
// Changes and modifications by Maxim Saplin, 2021
/// Keeps track of selected rows, feed the data into DesertsDataSource
class RestorableDessertSelections extends RestorableProperty<Set<int>> {
Set<int> _dessertSelections = {};
/// Returns whether or not a dessert row is selected by index.
bool isSelected(int index) => _dessertSelections.contains(index);
/// Takes a list of [Dessert]s and saves the row indices of selected rows
/// into a [Set].
void setDessertSelections(List<Dessert> desserts) {
final updatedSet = <int>{};
for (var i = 0; i < desserts.length; i += 1) {
var dessert = desserts[i];
if (dessert.selected) {
updatedSet.add(i);
}
}
_dessertSelections = updatedSet;
notifyListeners();
}
@override
Set<int> createDefaultValue() => _dessertSelections;
@override
Set<int> fromPrimitives(Object? data) {
final selectedItemIndices = data as List<dynamic>;
_dessertSelections = {
...selectedItemIndices.map<int>((dynamic id) => id as int),
};
return _dessertSelections;
}
@override
void initWithValue(Set<int> value) {
_dessertSelections = value;
}
@override
Object toPrimitives() => _dessertSelections.toList();
}
int _idCounter = 0;
/// Domain model entity
class Dessert {
Dessert(
this.name,
this.calories,
this.fat,
this.carbs,
this.protein,
this.sodium,
this.calcium,
this.iron,
);
final int id = _idCounter++;
final String name;
final int calories;
final double fat;
final int carbs;
final double protein;
final int sodium;
final int calcium;
final int iron;
bool selected = false;
}
/// Data source implementing standard Flutter's DataTableSource abstract class
/// which is part of DataTable and PaginatedDataTable synchronous data fecthin API.
/// This class uses static collection of deserts as a data store, projects it into
/// DataRows, keeps track of selected items, provides sprting capability
class DessertDataSource extends DataTableSource {
DessertDataSource.empty(this.context) {
desserts = [];
}
DessertDataSource(this.context,
[sortedByCalories = false,
this.hasRowTaps = false,
this.hasRowHeightOverrides = false,
this.hasZebraStripes = false]) {
desserts = _desserts;
if (sortedByCalories) {
sort((d) => d.calories, true);
}
}
final BuildContext context;
late List<Dessert> desserts;
// Add row tap handlers and show snackbar
bool hasRowTaps = false;
// Override height values for certain rows
bool hasRowHeightOverrides = false;
// Color each Row by index's parity
bool hasZebraStripes = false;
void sort<T>(Comparable<T> Function(Dessert d) getField, bool ascending) {
desserts.sort((a, b) {
final aValue = getField(a);
final bValue = getField(b);
return ascending
? Comparable.compare(aValue, bValue)
: Comparable.compare(bValue, aValue);
});
notifyListeners();
}
void updateSelectedDesserts(RestorableDessertSelections selectedRows) {
_selectedCount = 0;
for (var i = 0; i < desserts.length; i += 1) {
var dessert = desserts[i];
if (selectedRows.isSelected(i)) {
dessert.selected = true;
_selectedCount += 1;
} else {
dessert.selected = false;
}
}
notifyListeners();
}
@override
DataRow2 getRow(int index, [Color? color]) {
final format = NumberFormat.decimalPercentPattern(
locale: 'en',
decimalDigits: 0,
);
assert(index >= 0);
if (index >= desserts.length) throw 'index > _desserts.length';
final dessert = desserts[index];
return DataRow2.byIndex(
index: index,
selected: dessert.selected,
color: color != null
? WidgetStateProperty.all(color)
: (hasZebraStripes && index.isEven
? WidgetStateProperty.all(Theme.of(context).highlightColor)
: null),
onSelectChanged: (value) {
if (dessert.selected != value) {
_selectedCount += value! ? 1 : -1;
assert(_selectedCount >= 0);
dessert.selected = value;
notifyListeners();
}
},
onTap: hasRowTaps
? () => _showSnackbar(context, 'Tapped on row ${dessert.name}')
: null,
onDoubleTap: hasRowTaps
? () => _showSnackbar(context, 'Double Tapped on row ${dessert.name}')
: null,
onLongPress: hasRowTaps
? () => _showSnackbar(context, 'Long pressed on row ${dessert.name}')
: null,
onSecondaryTap: hasRowTaps
? () => _showSnackbar(context, 'Right clicked on row ${dessert.name}')
: null,
onSecondaryTapDown: hasRowTaps
? (d) =>
_showSnackbar(context, 'Right button down on row ${dessert.name}')
: null,
specificRowHeight:
hasRowHeightOverrides && dessert.fat >= 25 ? 100 : null,
cells: [
DataCell(Text(dessert.name)),
DataCell(Text('${dessert.calories}'),
onTap: () => _showSnackbar(context,
'Tapped on a cell with "${dessert.calories}"', Colors.red)),
DataCell(Text(dessert.fat.toStringAsFixed(1))),
DataCell(Text('${dessert.carbs}')),
DataCell(Text(dessert.protein.toStringAsFixed(1))),
DataCell(Text('${dessert.sodium}')),
DataCell(Text(format.format(dessert.calcium / 100))),
DataCell(Text(format.format(dessert.iron / 100))),
],
);
}
@override
int get rowCount => desserts.length;
@override
bool get isRowCountApproximate => false;
@override
int get selectedRowCount => _selectedCount;
void selectAll(bool? checked) {
for (final dessert in desserts) {
dessert.selected = checked ?? false;
}
_selectedCount = (checked ?? false) ? desserts.length : 0;
notifyListeners();
}
}
/// Async datasource for AsynPaginatedDataTabke2 example. Based on AsyncDataTableSource which
/// is an extension to Flutter's DataTableSource and aimed at solving
/// saync data fetching scenarious by paginated table (such as using Web API)
class DessertDataSourceAsync extends AsyncDataTableSource {
DessertDataSourceAsync() {
print('DessertDataSourceAsync created');
}
DessertDataSourceAsync.empty() {
_empty = true;
print('DessertDataSourceAsync.empty created');
}
DessertDataSourceAsync.error() {
_errorCounter = 0;
print('DessertDataSourceAsync.error created');
}
bool _empty = false;
int? _errorCounter;
RangeValues? _caloriesFilter;
RangeValues? get caloriesFilter => _caloriesFilter;
set caloriesFilter(RangeValues? calories) {
_caloriesFilter = calories;
refreshDatasource();
}
final DesertsFakeWebService _repo = DesertsFakeWebService();
String _sortColumn = "name";
bool _sortAscending = true;
void sort(String columnName, bool ascending) {
_sortColumn = columnName;
_sortAscending = ascending;
refreshDatasource();
}
Future<int> getTotalRecords() {
return Future<int>.delayed(
const Duration(milliseconds: 0), () => _empty ? 0 : _dessertsX3.length);
}
@override
Future<AsyncRowsResponse> getRows(int startIndex, int count) async {
print('getRows($startIndex, $count)');
if (_errorCounter != null) {
_errorCounter = _errorCounter! + 1;
if (_errorCounter! % 2 == 1) {
await Future.delayed(const Duration(milliseconds: 1000));
throw 'Error #${((_errorCounter! - 1) / 2).round() + 1} has occured';
}
}
final format = NumberFormat.decimalPercentPattern(
locale: 'en',
decimalDigits: 0,
);
assert(startIndex >= 0);
// List returned will be empty is there're fewer items than startingAt
var x = _empty
? await Future.delayed(const Duration(milliseconds: 2000),
() => DesertsFakeWebServiceResponse(0, []))
: await _repo.getData(
startIndex, count, _caloriesFilter, _sortColumn, _sortAscending);
var r = AsyncRowsResponse(
x.totalRecords,
x.data.map((dessert) {
return DataRow(
key: ValueKey<int>(dessert.id),
//selected: dessert.selected,
onSelectChanged: (value) {
if (value != null) {
setRowSelection(ValueKey<int>(dessert.id), value);
}
},
cells: [
DataCell(Text(dessert.name)),
DataCell(Text('${dessert.calories}')),
DataCell(Text(dessert.fat.toStringAsFixed(1))),
DataCell(Text('${dessert.carbs}')),
DataCell(Text(dessert.protein.toStringAsFixed(1))),
DataCell(Text('${dessert.sodium}')),
DataCell(Text(format.format(dessert.calcium / 100))),
DataCell(Text(format.format(dessert.iron / 100))),
],
);
}).toList());
return r;
}
}
class DesertsFakeWebServiceResponse {
DesertsFakeWebServiceResponse(this.totalRecords, this.data);
/// THe total ammount of records on the server, e.g. 100
final int totalRecords;
/// One page, e.g. 10 reocrds
final List<Dessert> data;
}
class DesertsFakeWebService {
int Function(Dessert, Dessert)? _getComparisonFunction(
String column, bool ascending) {
var coef = ascending ? 1 : -1;
switch (column) {
case 'name':
return (Dessert d1, Dessert d2) => coef * d1.name.compareTo(d2.name);
case 'calories':
return (Dessert d1, Dessert d2) => coef * (d1.calories - d2.calories);
case 'fat':
return (Dessert d1, Dessert d2) => coef * (d1.fat - d2.fat).round();
case 'carbs':
return (Dessert d1, Dessert d2) => coef * (d1.carbs - d2.carbs);
case 'protein':
return (Dessert d1, Dessert d2) =>
coef * (d1.protein - d2.protein).round();
case 'sodium':
return (Dessert d1, Dessert d2) => coef * (d1.sodium - d2.sodium);
case 'calcium':
return (Dessert d1, Dessert d2) => coef * (d1.calcium - d2.calcium);
case 'iron':
return (Dessert d1, Dessert d2) => coef * (d1.iron - d2.iron);
}
return null;
}
Future<DesertsFakeWebServiceResponse> getData(int startingAt, int count,
RangeValues? caloriesFilter, String sortedBy, bool sortedAsc) async {
return Future.delayed(
Duration(
milliseconds: startingAt == 0
? 2650
: startingAt < 20
? 2000
: 400), () {
var result = _dessertsX3;
if (caloriesFilter != null) {
result = result
.where((e) =>
e.calories >= caloriesFilter.start &&
e.calories <= caloriesFilter.end)
.toList();
}
result.sort(_getComparisonFunction(sortedBy, sortedAsc));
return DesertsFakeWebServiceResponse(
result.length, result.skip(startingAt).take(count).toList());
});
}
}
int _selectedCount = 0;
List<Dessert> _desserts = <Dessert>[
Dessert(
'Frozen Yogurt',
159,
6.0,
24,
4.0,
87,
14,
1,
),
Dessert(
'Ice Cream Sandwich',
237,
9.0,
37,
4.3,
129,
8,
1,
),
Dessert(
'Eclair',
262,
16.0,
24,
6.0,
337,
6,
7,
),
Dessert(
'Cupcake',
305,
3.7,
67,
4.3,
413,
3,
8,
),
Dessert(
'Gingerbread',
356,
16.0,
49,
3.9,
327,
7,
16,
),
Dessert(
'Jelly Bean',
375,
0.0,
94,
0.0,
50,
0,
0,
),
Dessert(
'Lollipop',
392,
0.2,
98,
0.0,
38,
0,
2,
),
Dessert(
'Honeycomb',
408,
3.2,
87,
6.5,
562,
0,
45,
),
Dessert(
'Donut',
452,
25.0,
51,
4.9,
326,
2,
22,
),
Dessert(
'Apple Pie',
518,
26.0,
65,
7.0,
54,
12,
6,
),
Dessert(
'Frozen Yougurt with sugar',
168,
6.0,
26,
4.0,
87,
14,
1,
),
Dessert(
'Ice Cream Sandwich with sugar',
246,
9.0,
39,
4.3,
129,
8,
1,
),
Dessert(
'Eclair with sugar',
271,
16.0,
26,
6.0,
337,
6,
7,
),
Dessert(
'Cupcake with sugar',
314,
3.7,
69,
4.3,
413,
3,
8,
),
Dessert(
'Gingerbread with sugar',
345,
16.0,
51,
3.9,
327,
7,
16,
),
Dessert(
'Jelly Bean with sugar',
364,
0.0,
96,
0.0,
50,
0,
0,
),
Dessert(
'Lollipop with sugar',
401,
0.2,
100,
0.0,
38,
0,
2,
),
Dessert(
'Honeycomd with sugar',
417,
3.2,
89,
6.5,
562,
0,
45,
),
Dessert(
'Donut with sugar',
461,
25.0,
53,
4.9,
326,
2,
22,
),
Dessert(
'Apple pie with sugar',
527,
26.0,
67,
7.0,
54,
12,
6,
),
Dessert(
'Forzen yougurt with honey',
223,
6.0,
36,
4.0,
87,
14,
1,
),
Dessert(
'Ice Cream Sandwich with honey',
301,
9.0,
49,
4.3,
129,
8,
1,
),
Dessert(
'Eclair with honey',
326,
16.0,
36,
6.0,
337,
6,
7,
),
Dessert(
'Cupcake with honey',
369,
3.7,
79,
4.3,
413,
3,
8,
),
Dessert(
'Gignerbread with hone',
420,
16.0,
61,
3.9,
327,
7,
16,
),
Dessert(
'Jelly Bean with honey',
439,
0.0,
106,
0.0,
50,
0,
0,
),
Dessert(
'Lollipop with honey',
456,
0.2,
110,
0.0,
38,
0,
2,
),
Dessert(
'Honeycomd with honey',
472,
3.2,
99,
6.5,
562,
0,
45,
),
Dessert(
'Donut with honey',
516,
25.0,
63,
4.9,
326,
2,
22,
),
Dessert(
'Apple pie with honey',
582,
26.0,
77,
7.0,
54,
12,
6,
),
];
List<Dessert> _dessertsX3 = _desserts.toList()
..addAll(_desserts.map((i) => Dessert('${i.name} x2', i.calories, i.fat,
i.carbs, i.protein, i.sodium, i.calcium, i.iron)))
..addAll(_desserts.map((i) => Dessert('${i.name} x3', i.calories, i.fat,
i.carbs, i.protein, i.sodium, i.calcium, i.iron)));
_showSnackbar(BuildContext context, String text, [Color? color]) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
backgroundColor: color,
duration: const Duration(seconds: 1),
content: Text(text),
));
}
`data_table_2` :
- main.dart
import 'package:example/screens/data_table2_fixed_nm.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'nav_helper.dart';
import 'screens/async_paginated_data_table2.dart';
import 'screens/data_table.dart';
import 'screens/data_table2.dart';
import 'screens/data_table2_rounded.dart';
import 'screens/data_table2_scrollup.dart';
import 'screens/data_table2_simple.dart';
import 'screens/data_table2_tests.dart';
import 'screens/paginated_data_table.dart';
import 'screens/paginated_data_table2.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
void main() {
runApp(MyApp());
// Add import
// import 'package:data_table_2/data_table_2.dart';
// and uncomment below line to remove widgets' logs
//dataTableShowLogs = false;
}
const String initialRoute = '/datatable2';
Scaffold _getScaffold(BuildContext context, Widget body,
[List<String>? options]) {
var defaultOption = getCurrentRouteOption(context);
if (defaultOption.isEmpty && options != null && options.isNotEmpty) {
defaultOption = options[0];
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.grey[200],
shadowColor: Colors.transparent,
automaticallyImplyLeading: false,
title: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Container(
padding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
color: Colors.grey[850],
//screen selection
child: DropdownButton<String>(
icon: const Icon(Icons.arrow_forward),
dropdownColor: Colors.grey[800],
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(color: Colors.white),
value: _getCurrentRoute(context),
onChanged: (v) {
Navigator.of(context).pushNamed(v!);
},
items: const [
DropdownMenuItem(
value: '/datatable2',
child: Text('DataTable2'),
),
DropdownMenuItem(
value: '/datatable2simple',
child: Text('Simple'),
),
DropdownMenuItem(
value: '/datatable2scrollup',
child: Text('Scroll-up/Scroll-left'),
),
DropdownMenuItem(
value: '/datatable2fixedmn',
child: Text('Fixed Rows/Cols'),
),
DropdownMenuItem(
value: '/paginated2',
child: Text('PaginatedDataTable2'),
),
DropdownMenuItem(
value: '/asyncpaginated2',
child: Text('AsyncPaginatedDataTable2'),
),
DropdownMenuItem(
value: '/datatable',
child: Text('DataTable'),
),
DropdownMenuItem(
value: '/paginated',
child: Text('PaginatedDataTable'),
),
if (kDebugMode)
DropdownMenuItem(
value: '/datatable2tests',
child: Text('Unit Tests Preview'),
),
],
)),
options != null && options.isNotEmpty
? Flexible(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 4, 0, 4),
child: DropdownButton<String>(
icon: const SizedBox(),
dropdownColor: Colors.grey[300],
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(color: Colors.black),
value: defaultOption,
onChanged: (v) {
var r = _getCurrentRoute(context);
Navigator.of(context).pushNamed(r, arguments: v);
},
items: options
.map<DropdownMenuItem<String>>(
(v) => DropdownMenuItem<String>(
value: v,
child: Text(v),
))
.toList()))))
: const SizedBox()
]),
),
body: body,
);
}
String _getCurrentRoute(BuildContext context) {
return ModalRoute.of(context) != null &&
ModalRoute.of(context)!.settings.name != null
? ModalRoute.of(context)!.settings.name!
: initialRoute;
}
// ignore: use_key_in_widget_constructors
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
restorationScopeId: 'main',
title: 'Flutter Demo',
theme: ThemeData(
primaryColor: Colors.grey[300],
),
initialRoute: initialRoute,
routes: {
'/datatable2': (context) {
final currentRouteOption = getCurrentRouteOption(context);
return _getScaffold(
context,
currentRouteOption == rounded
? const DataTable2RoundedDemo()
: const DataTable2Demo(),
getOptionsForRoute('/datatable2'));
},
'/datatable2simple': (context) =>
_getScaffold(context, const DataTable2SimpleDemo()),
'/datatable2scrollup': (context) =>
_getScaffold(context, const DataTable2ScrollupDemo()),
'/datatable2fixedmn': (context) => _getScaffold(
context,
const DataTable2FixedNMDemo(),
getOptionsForRoute('/datatable2fixedmn')),
'/paginated2': (context) => _getScaffold(context,
const PaginatedDataTable2Demo(), getOptionsForRoute('/paginated2')),
'/asyncpaginated2': (context) => _getScaffold(
context,
const AsyncPaginatedDataTable2Demo(),
getOptionsForRoute('/asyncpaginated2')),
'/datatable': (context) => _getScaffold(context, const DataTableDemo()),
'/paginated': (context) =>
_getScaffold(context, const PaginatedDataTableDemo()),
'/datatable2tests': (context) =>
_getScaffold(context, const DataTable2Tests()),
},
localizationsDelegates: const [GlobalMaterialLocalizations.delegate],
supportedLocales: const [
Locale('en', ''),
Locale('be', ''),
Locale('ru', ''),
Locale('fr', ''),
],
// change to see how PaginatedDataTable2 controls (e.g. Rows per page) get translated
locale: const Locale('en', ''),
);
}
}
- custom_pager.dart
import 'package:data_table_2/data_table_2.dart';
import 'package:flutter/material.dart';
class PageNumber extends StatefulWidget {
const PageNumber({
super.key,
required PaginatorController controller,
}) : _controller = controller;
final PaginatorController _controller;
@override
PageNumberState createState() => PageNumberState();
}
class PageNumberState extends State<PageNumber> {
void update() {
setState(() {});
}
@override
void initState() {
super.initState();
widget._controller.addListener(update);
}
@override
void dispose() {
widget._controller.removeListener(update);
super.dispose();
}
@override
Widget build(BuildContext context) {
// Checking instance id to see if build is called
// on different ones
// Due to some reasons when using this widget
// with AsyncPaginatedDatatable2 the widget is instatiotaed once
// though it's state is created 3 times upon first loading
// of the Custom pager example
// print(identityHashCode(this));
return Text(widget._controller.isAttached
? 'Page: ${1 + ((widget._controller.currentRowIndex + 1) / widget._controller.rowsPerPage).floor()} of '
'${(widget._controller.rowCount / widget._controller.rowsPerPage).ceil()}'
: 'Page: x of y');
}
}
class CustomPager extends StatefulWidget {
const CustomPager(this.controller, {super.key});
final PaginatorController controller;
@override
CustomPagerState createState() => CustomPagerState();
}
class CustomPagerState extends State<CustomPager> {
static const List<int> _availableSizes = [3, 5, 10, 20];
@override
void initState() {
super.initState();
widget.controller.addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
// skip this build pass
if (!widget.controller.isAttached) return const SizedBox();
return Container(
width: 220,
height: 40,
decoration: BoxDecoration(
color: Colors.grey[850],
borderRadius: BorderRadius.circular(3),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(100),
blurRadius: 4,
offset: const Offset(4, 8), // Shadow position
),
],
),
child: Theme(
data: Theme.of(context).copyWith(
iconTheme: const IconThemeData(color: Colors.white),
textTheme:
const TextTheme(titleMedium: TextStyle(color: Colors.white))),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () => widget.controller.goToFirstPage(),
icon: const Icon(Icons.skip_previous)),
IconButton(
onPressed: () => widget.controller.goToPreviousPage(),
icon: const Icon(Icons.chevron_left_sharp)),
DropdownButton<int>(
onChanged: (v) => widget.controller.setRowsPerPage(v!),
value: _availableSizes.contains(widget.controller.rowsPerPage)
? widget.controller.rowsPerPage
: _availableSizes[0],
dropdownColor: Colors.grey[800],
items: _availableSizes
.map((s) => DropdownMenuItem<int>(
value: s,
child: Text(s.toString()),
))
.toList()),
IconButton(
onPressed: () => widget.controller.goToNextPage(),
icon: const Icon(Icons.chevron_right_sharp)),
IconButton(
onPressed: () => widget.controller.goToLastPage(),
icon: const Icon(Icons.skip_next))
],
)),
);
}
}
- nav_helper.dart
import 'package:flutter/material.dart';
/// Route options are used to configure certain features of
/// the given example
String getCurrentRouteOption(BuildContext context) {
var isEmpty = ModalRoute.of(context) != null &&
ModalRoute.of(context)!.settings.arguments != null &&
ModalRoute.of(context)!.settings.arguments is String
? ModalRoute.of(context)!.settings.arguments as String
: '';
return isEmpty;
}
// Route options
const dflt = 'Default';
const noData = 'No data';
const autoRows = 'Auto rows';
const showBordersWithZebraStripes = 'Borders with Zebra';
const custPager = 'Custom pager';
const defaultSorting = 'Default sorting';
const selectAllPage = 'Select all at page';
const rowTaps = 'Row Taps';
const rowHeightOverrides = 'Row height overrides';
const fixedColumnWidth = 'Fixed column width';
const dataTable2 = 'DataTable2';
const paginatedFixedRowsCols = 'PaginatedDataTable2';
const asyncPaginatedFixedRowsCols = 'AsyncPaginatedDataTable2';
const custArrows = 'Custom sort arrows';
const asyncErrors =
"Errors/Retries"; // Async sample that emulates network error and allow retrying load operation
const goToLast =
"Start at last page"; // Used by async example, navigates to the very last page upon opening the screen
const rounded = 'Rounded style';
/// Configurations available to given example routes
const Map<String, List<String>> routeOptions = {
'/datatable2': [
dflt,
noData,
showBordersWithZebraStripes,
fixedColumnWidth,
rowTaps,
rowHeightOverrides,
custArrows,
rounded
],
'/paginated2': [dflt, noData, autoRows, custPager, defaultSorting],
'/datatable2fixedmn': [
dataTable2,
paginatedFixedRowsCols,
asyncPaginatedFixedRowsCols
],
'/asyncpaginated2': [
dflt,
noData,
selectAllPage,
autoRows,
asyncErrors,
goToLast,
custPager
],
};
List<String>? getOptionsForRoute(String route) {
if (!routeOptions.containsKey(route)) {
return null;
}
return routeOptions[route];
}
- helper.dart
import 'package:flutter/material.dart';
import 'dart:math';
ThemeData blackSlider(BuildContext context) {
return Theme.of(context).copyWith(
sliderTheme: SliderThemeData(
rangeThumbShape:
const RectRangeSliderThumbShape(enabledThumbRadius: 8),
thumbShape: const RectSliderThumbShape(enabledThumbRadius: 8),
thumbColor: Colors.grey[800],
activeTrackColor: Colors.grey[700],
inactiveTrackColor: Colors.grey[400],
activeTickMarkColor: Colors.white,
inactiveTickMarkColor: Colors.white));
}
class RectRangeSliderThumbShape extends RangeSliderThumbShape {
const RectRangeSliderThumbShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
});
final double enabledThumbRadius;
final double? disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
final double elevation;
final double pressedElevation;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(
isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
bool isDiscrete = false,
bool isEnabled = false,
bool? isOnTop,
required SliderThemeData sliderTheme,
TextDirection? textDirection,
Thumb? thumb,
bool? isPressed,
}) {
assert(sliderTheme.showValueIndicator != null);
assert(sliderTheme.overlappingShapeStrokeColor != null);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: enabledThumbRadius,
);
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final double radius = radiusTween.evaluate(enableAnimation);
final Tween<double> elevationTween = Tween<double>(
begin: elevation,
end: pressedElevation,
);
if (isOnTop ?? false) {
final Paint strokePaint = Paint()
..color = sliderTheme.overlappingShapeStrokeColor!
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
canvas.drawRect(
Rect.fromCenter(
center: center, width: 2 * radius, height: 2 * radius),
strokePaint);
}
final Color color = colorTween.evaluate(enableAnimation)!;
final double evaluatedElevation =
isPressed! ? elevationTween.evaluate(activationAnimation) : elevation;
final Path shadowPath = Path()
..addArc(
Rect.fromCenter(
center: center, width: 2 * radius, height: 2 * radius),
0,
pi * 2);
canvas.drawShadow(shadowPath, Colors.black, evaluatedElevation, true);
canvas.drawRect(
Rect.fromCenter(center: center, width: 2 * radius, height: 2 * radius),
Paint()..color = color,
);
}
}
class RectSliderThumbShape extends SliderComponentShape {
const RectSliderThumbShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
});
final double enabledThumbRadius;
final double? disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
final double elevation;
final double pressedElevation;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(
isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: enabledThumbRadius,
);
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final Color color = colorTween.evaluate(enableAnimation)!;
final double radius = radiusTween.evaluate(enableAnimation);
final Tween<double> elevationTween = Tween<double>(
begin: elevation,
end: pressedElevation,
);
final double evaluatedElevation =
elevationTween.evaluate(activationAnimation);
final Path path = Path()
..addArc(
Rect.fromCenter(
center: center, width: 2 * radius, height: 2 * radius),
0,
pi * 2);
canvas.drawShadow(path, Colors.black, evaluatedElevation, true);
canvas.drawRect(
Rect.fromCenter(center: center, width: 2 * radius, height: 2 * radius),
Paint()..color = color,
);
}
}
- screens/paginated_data_table2.dart
// ignore_for_file: avoid_print
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:data_table_2/data_table_2.dart';
import '../data_sources.dart';
import '../nav_helper.dart';
import '../custom_pager.dart';
// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The file was extracted from GitHub: https://github.com/flutter/gallery
// Changes and modifications by Maxim Saplin, 2021
class PaginatedDataTable2Demo extends StatefulWidget {
const PaginatedDataTable2Demo({super.key});
@override
PaginatedDataTable2DemoState createState() => PaginatedDataTable2DemoState();
}
class PaginatedDataTable2DemoState extends State<PaginatedDataTable2Demo> {
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
bool _sortAscending = true;
int? _sortColumnIndex;
late DessertDataSource _dessertsDataSource;
bool _initialized = false;
PaginatorController? _controller;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initialized) {
_dessertsDataSource = DessertDataSource(
context, getCurrentRouteOption(context) == defaultSorting);
_controller = PaginatorController();
if (getCurrentRouteOption(context) == defaultSorting) {
_sortColumnIndex = 1;
}
_initialized = true;
}
}
void sort<T>(
Comparable<T> Function(Dessert d) getField,
int columnIndex,
bool ascending,
) {
_dessertsDataSource.sort<T>(getField, ascending);
setState(() {
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
});
}
@override
void dispose() {
_dessertsDataSource.dispose();
super.dispose();
}
List<DataColumn> get _columns {
return [
DataColumn(
label: const Text('Desert'),
onSort: (columnIndex, ascending) =>
sort<String>((d) => d.name, columnIndex, ascending),
),
DataColumn(
label: const Text('Calories'),
numeric: true,
onSort: (columnIndex, ascending) =>
sort<num>((d) => d.calories, columnIndex, ascending),
),
DataColumn(
label: const Text('Fat (gm)'),
numeric: true,
onSort: (columnIndex, ascending) =>
sort<num>((d) => d.fat, columnIndex, ascending),
),
DataColumn(
label: const Text('Carbs (gm)'),
numeric: true,
onSort: (columnIndex, ascending) =>
sort<num>((d) => d.carbs, columnIndex, ascending),
),
DataColumn(
label: const Text('Protein (gm)'),
numeric: true,
onSort: (columnIndex, ascending) =>
sort<num>((d) => d.protein, columnIndex, ascending),
),
DataColumn(
label: const Text('Sodium (mg)'),
numeric: true,
onSort: (columnIndex, ascending) =>
sort<num>((d) => d.sodium, columnIndex, ascending),
),
DataColumn(
label: const Text('Calcium (%)'),
numeric: true,
onSort: (columnIndex, ascending) =>
sort<num>((d) => d.calcium, columnIndex, ascending),
),
DataColumn(
label: const Text('Iron (%)'),
numeric: true,
onSort: (columnIndex, ascending) =>
sort<num>((d) => d.iron, columnIndex, ascending),
),
];
}
@override
Widget build(BuildContext context) {
return Stack(alignment: Alignment.bottomCenter, children: [
PaginatedDataTable2(
// 100 Won't be shown since it is smaller than total records
availableRowsPerPage: const [2, 5, 10, 30, 100],
horizontalMargin: 20,
checkboxHorizontalMargin: 12,
columnSpacing: 0,
wrapInCard: false,
renderEmptyRowsInTheEnd: false,
headingRowColor:
WidgetStateColor.resolveWith((states) => Colors.grey[200]!),
header:
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
const Text('PaginatedDataTable2'),
if (kDebugMode && getCurrentRouteOption(context) == custPager)
Row(children: [
OutlinedButton(
onPressed: () => _controller!.goToPageWithRow(25),
child: const Text('Go to row 25')),
OutlinedButton(
onPressed: () => _controller!.goToRow(5),
child: const Text('Go to row 5'))
]),
if (getCurrentRouteOption(context) == custPager &&
_controller != null)
PageNumber(controller: _controller!)
]),
rowsPerPage: _rowsPerPage,
autoRowsToHeight: getCurrentRouteOption(context) == autoRows,
minWidth: 800,
fit: FlexFit.tight,
border: TableBorder(
top: const BorderSide(color: Colors.black),
bottom: BorderSide(color: Colors.grey[300]!),
left: BorderSide(color: Colors.grey[300]!),
right: BorderSide(color: Colors.grey[300]!),
verticalInside: BorderSide(color: Colors.grey[300]!),
horizontalInside: const BorderSide(color: Colors.grey, width: 1)),
onRowsPerPageChanged: (value) {
// No need to wrap into setState, it will be called inside the widget
// and trigger rebuild
//setState(() {
_rowsPerPage = value!;
print(_rowsPerPage);
//});
},
initialFirstRowIndex: 0,
onPageChanged: (rowIndex) {
print(rowIndex / _rowsPerPage);
},
sortColumnIndex: _sortColumnIndex,
sortAscending: _sortAscending,
sortArrowIcon: Icons.keyboard_arrow_up, // custom arrow
sortArrowAnimationDuration:
const Duration(milliseconds: 0), // custom animation duration
onSelectAll: _dessertsDataSource.selectAll,
controller:
getCurrentRouteOption(context) == custPager ? _controller : null,
hidePaginator: getCurrentRouteOption(context) == custPager,
columns: _columns,
empty: Center(
child: Container(
padding: const EdgeInsets.all(20),
color: Colors.grey[200],
child: const Text('No data'))),
source: getCurrentRouteOption(context) == noData
? DessertDataSource.empty(context)
: _dessertsDataSource,
),
if (getCurrentRouteOption(context) == custPager)
Positioned(bottom: 16, child: CustomPager(_controller!))
]);
}
}