mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
PaginatedDataTable improvements (#131374)
- slightly improved assert message when row cell counts don't match column count. - more breadcrumbs in API documentation. more documentation in general. - added more documentation for the direction of the "ascending" arrow. - two samples for PaginatedDataTable. - make PaginatedDataTable support hot reloading across changes to the number of columns. - introduce matrix3MoreOrLessEquals. An earlier version of this PR used it in tests, but eventually it was not needed. The function seems useful to keep though.
This commit is contained in:
parent
668a002280
commit
ccdf826466
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Flutter code sample for [PaginatedDataTable].
|
||||||
|
|
||||||
|
class MyDataSource extends DataTableSource {
|
||||||
|
@override
|
||||||
|
int get rowCount => 3;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DataRow? getRow(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0: return const DataRow(
|
||||||
|
cells: <DataCell>[
|
||||||
|
DataCell(Text('Sarah')),
|
||||||
|
DataCell(Text('19')),
|
||||||
|
DataCell(Text('Student')),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
case 1: return const DataRow(
|
||||||
|
cells: <DataCell>[
|
||||||
|
DataCell(Text('Janine')),
|
||||||
|
DataCell(Text('43')),
|
||||||
|
DataCell(Text('Professor')),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
case 2: return const DataRow(
|
||||||
|
cells: <DataCell>[
|
||||||
|
DataCell(Text('William')),
|
||||||
|
DataCell(Text('27')),
|
||||||
|
DataCell(Text('Associate Professor')),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isRowCountApproximate => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get selectedRowCount => 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DataTableSource dataSource = MyDataSource();
|
||||||
|
|
||||||
|
void main() => runApp(const DataTableExampleApp());
|
||||||
|
|
||||||
|
class DataTableExampleApp extends StatelessWidget {
|
||||||
|
const DataTableExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const MaterialApp(
|
||||||
|
home: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(12.0),
|
||||||
|
child: DataTableExample(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataTableExample extends StatelessWidget {
|
||||||
|
const DataTableExample({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PaginatedDataTable(
|
||||||
|
columns: const <DataColumn>[
|
||||||
|
DataColumn(
|
||||||
|
label: Text('Name'),
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: Text('Age'),
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: Text('Role'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
source: dataSource,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,307 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Flutter code sample for [PaginatedDataTable].
|
||||||
|
|
||||||
|
class MyDataSource extends DataTableSource {
|
||||||
|
static const List<int> _displayIndexToRawIndex = <int>[ 0, 3, 4, 5, 6 ];
|
||||||
|
|
||||||
|
late List<List<Comparable<Object>>> sortedData;
|
||||||
|
void setData(List<List<Comparable<Object>>> rawData, int sortColumn, bool sortAscending) {
|
||||||
|
sortedData = rawData.toList()..sort((List<Comparable<Object>> a, List<Comparable<Object>> b) {
|
||||||
|
final Comparable<Object> cellA = a[_displayIndexToRawIndex[sortColumn]];
|
||||||
|
final Comparable<Object> cellB = b[_displayIndexToRawIndex[sortColumn]];
|
||||||
|
return cellA.compareTo(cellB) * (sortAscending ? 1 : -1);
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get rowCount => sortedData.length;
|
||||||
|
|
||||||
|
static DataCell cellFor(Object data) {
|
||||||
|
String value;
|
||||||
|
if (data is DateTime) {
|
||||||
|
value = '${data.year}-${data.month.toString().padLeft(2, '0')}-${data.day.toString().padLeft(2, '0')}';
|
||||||
|
} else {
|
||||||
|
value = data.toString();
|
||||||
|
}
|
||||||
|
return DataCell(Text(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
DataRow? getRow(int index) {
|
||||||
|
return DataRow.byIndex(
|
||||||
|
index: sortedData[index][0] as int,
|
||||||
|
cells: <DataCell>[
|
||||||
|
cellFor('S${sortedData[index][1]}E${sortedData[index][2].toString().padLeft(2, '0')}'),
|
||||||
|
cellFor(sortedData[index][3]),
|
||||||
|
cellFor(sortedData[index][4]),
|
||||||
|
cellFor(sortedData[index][5]),
|
||||||
|
cellFor(sortedData[index][6]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isRowCountApproximate => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get selectedRowCount => 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() => runApp(const DataTableExampleApp());
|
||||||
|
|
||||||
|
class DataTableExampleApp extends StatelessWidget {
|
||||||
|
const DataTableExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const MaterialApp(
|
||||||
|
home: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(12.0),
|
||||||
|
child: DataTableExample(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataTableExample extends StatefulWidget {
|
||||||
|
const DataTableExample({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DataTableExample> createState() => _DataTableExampleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DataTableExampleState extends State<DataTableExample> {
|
||||||
|
final MyDataSource dataSource = MyDataSource()
|
||||||
|
..setData(episodes, 0, true);
|
||||||
|
|
||||||
|
int _columnIndex = 0;
|
||||||
|
bool _columnAscending = true;
|
||||||
|
|
||||||
|
void _sort(int columnIndex, bool ascending) {
|
||||||
|
setState(() {
|
||||||
|
_columnIndex = columnIndex;
|
||||||
|
_columnAscending = ascending;
|
||||||
|
dataSource.setData(episodes, _columnIndex, _columnAscending);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PaginatedDataTable(
|
||||||
|
sortColumnIndex: _columnIndex,
|
||||||
|
sortAscending: _columnAscending,
|
||||||
|
columns: <DataColumn>[
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Episode'),
|
||||||
|
onSort: _sort,
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Title'),
|
||||||
|
onSort: _sort,
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Director'),
|
||||||
|
onSort: _sort,
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Writer(s)'),
|
||||||
|
onSort: _sort,
|
||||||
|
),
|
||||||
|
DataColumn(
|
||||||
|
label: const Text('Air Date'),
|
||||||
|
onSort: _sort,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
source: dataSource,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<List<Comparable<Object>>> episodes = <List<Comparable<Object>>>[
|
||||||
|
<Comparable<Object>>[
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'Strange New Worlds',
|
||||||
|
'Akiva Goldsman',
|
||||||
|
'Akiva Goldsman, Alex Kurtzman, Jenny Lumet',
|
||||||
|
DateTime(2022, 5, 5),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'Children of the Comet',
|
||||||
|
'Maja Vrvilo',
|
||||||
|
'Henry Alonso Myers, Sarah Tarkoff',
|
||||||
|
DateTime(2022, 5, 12),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
'Ghosts of Illyria',
|
||||||
|
'Leslie Hope',
|
||||||
|
'Akela Cooper, Bill Wolkoff',
|
||||||
|
DateTime(2022, 5, 19),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
'Memento Mori',
|
||||||
|
'Dan Liu',
|
||||||
|
'Davy Perez, Beau DeMayo',
|
||||||
|
DateTime(2022, 5, 26),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
5,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
'Spock Amok',
|
||||||
|
'Rachel Leiterman',
|
||||||
|
'Henry Alonso Myers, Robin Wasserman',
|
||||||
|
DateTime(2022, 6, 2),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
6,
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
'Lift Us Where Suffering Cannot Reach',
|
||||||
|
'Andi Armaganian',
|
||||||
|
'Robin Wasserman, Bill Wolkoff',
|
||||||
|
DateTime(2022, 6, 9),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
7,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
'The Serene Squall',
|
||||||
|
'Sydney Freeland',
|
||||||
|
'Beau DeMayo, Sarah Tarkoff',
|
||||||
|
DateTime(2022, 6, 16),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
8,
|
||||||
|
'The Elysian Kingdom',
|
||||||
|
'Amanda Row',
|
||||||
|
'Akela Cooper, Onitra Johnson',
|
||||||
|
DateTime(2022, 6, 23),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
9,
|
||||||
|
1,
|
||||||
|
9,
|
||||||
|
'All Those Who Wander',
|
||||||
|
'Christopher J. Byrne',
|
||||||
|
'Davy Perez',
|
||||||
|
DateTime(2022, 6, 30),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
10,
|
||||||
|
2,
|
||||||
|
10,
|
||||||
|
'A Quality of Mercy',
|
||||||
|
'Chris Fisher',
|
||||||
|
'Henry Alonso Myers, Akiva Goldsman',
|
||||||
|
DateTime(2022, 7, 7),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
11,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
'The Broken Circle',
|
||||||
|
'Chris Fisher',
|
||||||
|
'Henry Alonso Myers, Akiva Goldsman',
|
||||||
|
DateTime(2023, 6, 15),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
12,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
'Ad Astra per Aspera',
|
||||||
|
'Valerie Weiss',
|
||||||
|
'Dana Horgan',
|
||||||
|
DateTime(2023, 6, 22),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
13,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
'Tomorrow and Tomorrow and Tomorrow',
|
||||||
|
'Amanda Row',
|
||||||
|
'David Reed',
|
||||||
|
DateTime(2023, 6, 29),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
14,
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
'Among the Lotus Eaters',
|
||||||
|
'Eduardo Sánchez',
|
||||||
|
'Kirsten Beyer, Davy Perez',
|
||||||
|
DateTime(2023, 7, 6),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
15,
|
||||||
|
2,
|
||||||
|
5,
|
||||||
|
'Charades',
|
||||||
|
'Jordan Canning',
|
||||||
|
'Kathryn Lyn, Henry Alonso Myers',
|
||||||
|
DateTime(2023, 7, 13),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
16,
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
'Lost in Translation',
|
||||||
|
'Dan Liu',
|
||||||
|
'Onitra Johnson, David Reed',
|
||||||
|
DateTime(2023, 7, 20),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
17,
|
||||||
|
2,
|
||||||
|
7,
|
||||||
|
'Those Old Scientists',
|
||||||
|
'Jonathan Frakes',
|
||||||
|
'Kathryn Lyn, Bill Wolkoff',
|
||||||
|
DateTime(2023, 7, 22),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
18,
|
||||||
|
2,
|
||||||
|
8,
|
||||||
|
'Under the Cloak of War',
|
||||||
|
'',
|
||||||
|
'Davy Perez',
|
||||||
|
DateTime(2023, 7, 27),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
19,
|
||||||
|
2,
|
||||||
|
9,
|
||||||
|
'Subspace Rhapsody',
|
||||||
|
'',
|
||||||
|
'Dana Horgan, Bill Wolkoff',
|
||||||
|
DateTime(2023, 8, 3),
|
||||||
|
],
|
||||||
|
<Comparable<Object>>[
|
||||||
|
20,
|
||||||
|
2,
|
||||||
|
10,
|
||||||
|
'Hegemony',
|
||||||
|
'',
|
||||||
|
'Henry Alonso Myers',
|
||||||
|
DateTime(2023, 8, 10),
|
||||||
|
],
|
||||||
|
];
|
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter_api_samples/material/paginated_data_table/paginated_data_table.0.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('PaginatedDataTable 0', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const example.DataTableExampleApp());
|
||||||
|
expect(find.text('Associate Professor'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_api_samples/material/paginated_data_table/paginated_data_table.1.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('PaginatedDataTable 1', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const example.DataTableExampleApp());
|
||||||
|
expect(find.text('Strange New Worlds'), findsOneWidget);
|
||||||
|
await tester.tap(find.byIcon(Icons.arrow_upward).at(1));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Strange New Worlds'), findsNothing);
|
||||||
|
await tester.tap(find.byIcon(Icons.chevron_right));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Strange New Worlds'), findsOneWidget);
|
||||||
|
await tester.tap(find.byIcon(Icons.arrow_upward).at(1));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Strange New Worlds'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -444,7 +444,7 @@ class DataTable extends StatelessWidget {
|
|||||||
this.clipBehavior = Clip.none,
|
this.clipBehavior = Clip.none,
|
||||||
}) : assert(columns.isNotEmpty),
|
}) : assert(columns.isNotEmpty),
|
||||||
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
|
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
|
||||||
assert(!rows.any((DataRow row) => row.cells.length != columns.length)),
|
assert(!rows.any((DataRow row) => row.cells.length != columns.length), 'All rows must have the same number of cells as there are header cells (${columns.length})'),
|
||||||
assert(dividerThickness == null || dividerThickness >= 0),
|
assert(dividerThickness == null || dividerThickness >= 0),
|
||||||
assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight),
|
assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight),
|
||||||
assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
|
assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
|
||||||
@ -467,6 +467,8 @@ class DataTable extends StatelessWidget {
|
|||||||
///
|
///
|
||||||
/// When this is null, it implies that the table's sort order does
|
/// When this is null, it implies that the table's sort order does
|
||||||
/// not correspond to any of the columns.
|
/// not correspond to any of the columns.
|
||||||
|
///
|
||||||
|
/// The direction of the sort is specified using [sortAscending].
|
||||||
final int? sortColumnIndex;
|
final int? sortColumnIndex;
|
||||||
|
|
||||||
/// Whether the column mentioned in [sortColumnIndex], if any, is sorted
|
/// Whether the column mentioned in [sortColumnIndex], if any, is sorted
|
||||||
@ -479,6 +481,8 @@ class DataTable extends StatelessWidget {
|
|||||||
/// If false, the order is descending (meaning the rows with the
|
/// If false, the order is descending (meaning the rows with the
|
||||||
/// smallest values for the current sort column are last in the
|
/// smallest values for the current sort column are last in the
|
||||||
/// table).
|
/// table).
|
||||||
|
///
|
||||||
|
/// Ascending order is represented by an upwards-facing arrow.
|
||||||
final bool sortAscending;
|
final bool sortAscending;
|
||||||
|
|
||||||
/// Invoked when the user selects or unselects every row, using the
|
/// Invoked when the user selects or unselects every row, using the
|
||||||
|
@ -18,21 +18,32 @@ import 'data_table.dart';
|
|||||||
///
|
///
|
||||||
/// DataTableSource objects are expected to be long-lived, not recreated with
|
/// DataTableSource objects are expected to be long-lived, not recreated with
|
||||||
/// each build.
|
/// each build.
|
||||||
|
///
|
||||||
|
/// If a [DataTableSource] is used with a [PaginatedDataTable] that supports
|
||||||
|
/// sortable columns (see [DataColumn.onSort] and
|
||||||
|
/// [PaginatedDataTable.sortColumnIndex]), the rows reported by the data source
|
||||||
|
/// must be reported in the sorted order.
|
||||||
abstract class DataTableSource extends ChangeNotifier {
|
abstract class DataTableSource extends ChangeNotifier {
|
||||||
/// Called to obtain the data about a particular row.
|
/// Called to obtain the data about a particular row.
|
||||||
///
|
///
|
||||||
|
/// Rows should be keyed so that state can be maintained when the data source
|
||||||
|
/// is sorted (e.g. in response to [DataColumn.onSort]). Keys should be
|
||||||
|
/// consistent for a given [DataRow] regardless of the sort order (i.e. the
|
||||||
|
/// key represents the data's identity, not the row position).
|
||||||
|
///
|
||||||
/// The [DataRow.byIndex] constructor provides a convenient way to construct
|
/// The [DataRow.byIndex] constructor provides a convenient way to construct
|
||||||
/// [DataRow] objects for this callback's purposes without having to worry about
|
/// [DataRow] objects for this method's purposes without having to worry about
|
||||||
/// independently keying each row.
|
/// independently keying each row. The index passed to that constructor is the
|
||||||
|
/// index of the underlying data, which is different than the `index`
|
||||||
|
/// parameter for [getRow], which represents the _sorted_ position.
|
||||||
///
|
///
|
||||||
/// If the given index does not correspond to a row, or if no data is yet
|
/// If the given index does not correspond to a row, or if no data is yet
|
||||||
/// available for a row, then return null. The row will be left blank and a
|
/// available for a row, then return null. The row will be left blank and a
|
||||||
/// loading indicator will be displayed over the table. Once data is available
|
/// loading indicator will be displayed over the table. Once data is available
|
||||||
/// or once it is firmly established that the row index in question is beyond
|
/// or once it is firmly established that the row index in question is beyond
|
||||||
/// the end of the table, call [notifyListeners].
|
/// the end of the table, call [notifyListeners]. (See [rowCount].)
|
||||||
///
|
///
|
||||||
/// Data returned from this method must be consistent for the lifetime of the
|
/// If the underlying data changes, call [notifyListeners].
|
||||||
/// object. If the row count changes, then a new delegate must be provided.
|
|
||||||
DataRow? getRow(int index);
|
DataRow? getRow(int index);
|
||||||
|
|
||||||
/// Called to obtain the number of rows to tell the user are available.
|
/// Called to obtain the number of rows to tell the user are available.
|
||||||
@ -58,5 +69,7 @@ abstract class DataTableSource extends ChangeNotifier {
|
|||||||
/// Called to obtain the number of rows that are currently selected.
|
/// Called to obtain the number of rows that are currently selected.
|
||||||
///
|
///
|
||||||
/// If the selected row count changes, call [notifyListeners].
|
/// If the selected row count changes, call [notifyListeners].
|
||||||
|
///
|
||||||
|
/// Selected rows are those whose [DataRow.selected] property is set to true.
|
||||||
int get selectedRowCount;
|
int get selectedRowCount;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,23 @@ import 'theme.dart';
|
|||||||
/// If the [key] is a [PageStorageKey], the [initialFirstRowIndex] is persisted
|
/// If the [key] is a [PageStorageKey], the [initialFirstRowIndex] is persisted
|
||||||
/// to [PageStorage].
|
/// to [PageStorage].
|
||||||
///
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
///
|
||||||
|
/// This sample shows how to display a [DataTable] with three columns: name,
|
||||||
|
/// age, and role. The columns are defined by three [DataColumn] objects. The
|
||||||
|
/// table contains three rows of data for three example users, the data for
|
||||||
|
/// which is defined by three [DataRow] objects.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
///
|
||||||
|
/// This example shows how paginated data tables can supported sorted data.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [DataTable], which is not paginated.
|
/// * [DataTable], which is not paginated.
|
||||||
@ -142,13 +159,15 @@ class PaginatedDataTable extends StatefulWidget {
|
|||||||
|
|
||||||
/// The current primary sort key's column.
|
/// The current primary sort key's column.
|
||||||
///
|
///
|
||||||
/// See [DataTable.sortColumnIndex].
|
/// See [DataTable.sortColumnIndex] for details.
|
||||||
|
///
|
||||||
|
/// The direction of the sort is specified using [sortAscending].
|
||||||
final int? sortColumnIndex;
|
final int? sortColumnIndex;
|
||||||
|
|
||||||
/// Whether the column mentioned in [sortColumnIndex], if any, is sorted
|
/// Whether the column mentioned in [sortColumnIndex], if any, is sorted
|
||||||
/// in ascending order.
|
/// in ascending order.
|
||||||
///
|
///
|
||||||
/// See [DataTable.sortAscending].
|
/// See [DataTable.sortAscending] for details.
|
||||||
final bool sortAscending;
|
final bool sortAscending;
|
||||||
|
|
||||||
/// Invoked when the user selects or unselects every row, using the
|
/// Invoked when the user selects or unselects every row, using the
|
||||||
@ -297,10 +316,27 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
|||||||
if (oldWidget.source != widget.source) {
|
if (oldWidget.source != widget.source) {
|
||||||
oldWidget.source.removeListener(_handleDataSourceChanged);
|
oldWidget.source.removeListener(_handleDataSourceChanged);
|
||||||
widget.source.addListener(_handleDataSourceChanged);
|
widget.source.addListener(_handleDataSourceChanged);
|
||||||
_handleDataSourceChanged();
|
_updateCaches();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reassemble() {
|
||||||
|
super.reassemble();
|
||||||
|
// This function is called during hot reload.
|
||||||
|
//
|
||||||
|
// Normally, if the data source changes, it would notify its listeners and
|
||||||
|
// thus trigger _handleDataSourceChanged(), which clears the row cache and
|
||||||
|
// causes the widget to rebuild.
|
||||||
|
//
|
||||||
|
// During a hot reload, though, a data source can change in ways that will
|
||||||
|
// invalidate the row cache (e.g. adding or removing columns) without ever
|
||||||
|
// triggering a notification, leaving the PaginatedDataTable in an invalid
|
||||||
|
// state. This method handles this case by clearing the cache any time the
|
||||||
|
// widget is involved in a hot reload.
|
||||||
|
_updateCaches();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.source.removeListener(_handleDataSourceChanged);
|
widget.source.removeListener(_handleDataSourceChanged);
|
||||||
@ -308,12 +344,14 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleDataSourceChanged() {
|
void _handleDataSourceChanged() {
|
||||||
setState(() {
|
setState(_updateCaches);
|
||||||
_rowCount = widget.source.rowCount;
|
}
|
||||||
_rowCountApproximate = widget.source.isRowCountApproximate;
|
|
||||||
_selectedRowCount = widget.source.selectedRowCount;
|
void _updateCaches() {
|
||||||
_rows.clear();
|
_rowCount = widget.source.rowCount;
|
||||||
});
|
_rowCountApproximate = widget.source.isRowCountApproximate;
|
||||||
|
_selectedRowCount = widget.source.selectedRowCount;
|
||||||
|
_rows.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensures that the given row is visible.
|
/// Ensures that the given row is visible.
|
||||||
|
@ -469,11 +469,11 @@ void main() {
|
|||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
home: Material(child: buildTable()),
|
home: Material(child: buildTable()),
|
||||||
));
|
));
|
||||||
// The `tester.widget` ensures that there is exactly one upward arrow.
|
|
||||||
final Finder iconFinder = find.descendant(
|
final Finder iconFinder = find.descendant(
|
||||||
of: find.byType(DataTable),
|
of: find.byType(DataTable),
|
||||||
matching: find.widgetWithIcon(Transform, Icons.arrow_upward),
|
matching: find.widgetWithIcon(Transform, Icons.arrow_upward),
|
||||||
);
|
);
|
||||||
|
// The `tester.widget` ensures that there is exactly one upward arrow.
|
||||||
Transform transformOfArrow = tester.widget<Transform>(iconFinder);
|
Transform transformOfArrow = tester.widget<Transform>(iconFinder);
|
||||||
expect(
|
expect(
|
||||||
transformOfArrow.transform.getRotation(),
|
transformOfArrow.transform.getRotation(),
|
||||||
@ -521,11 +521,11 @@ void main() {
|
|||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
home: Material(child: buildTable()),
|
home: Material(child: buildTable()),
|
||||||
));
|
));
|
||||||
// The `tester.widget` ensures that there is exactly one upward arrow.
|
|
||||||
final Finder iconFinder = find.descendant(
|
final Finder iconFinder = find.descendant(
|
||||||
of: find.byType(DataTable),
|
of: find.byType(DataTable),
|
||||||
matching: find.widgetWithIcon(Transform, Icons.arrow_upward),
|
matching: find.widgetWithIcon(Transform, Icons.arrow_upward),
|
||||||
);
|
);
|
||||||
|
// The `tester.widget` ensures that there is exactly one upward arrow.
|
||||||
Transform transformOfArrow = tester.widget<Transform>(iconFinder);
|
Transform transformOfArrow = tester.widget<Transform>(iconFinder);
|
||||||
expect(
|
expect(
|
||||||
transformOfArrow.transform.getRotation(),
|
transformOfArrow.transform.getRotation(),
|
||||||
@ -574,11 +574,11 @@ void main() {
|
|||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
home: Material(child: buildTable()),
|
home: Material(child: buildTable()),
|
||||||
));
|
));
|
||||||
// The `tester.widget` ensures that there is exactly one upward arrow.
|
|
||||||
final Finder iconFinder = find.descendant(
|
final Finder iconFinder = find.descendant(
|
||||||
of: find.byType(DataTable),
|
of: find.byType(DataTable),
|
||||||
matching: find.widgetWithIcon(Transform, Icons.arrow_upward),
|
matching: find.widgetWithIcon(Transform, Icons.arrow_upward),
|
||||||
);
|
);
|
||||||
|
// The `tester.widget` ensures that there is exactly one upward arrow.
|
||||||
Transform transformOfArrow = tester.widget<Transform>(iconFinder);
|
Transform transformOfArrow = tester.widget<Transform>(iconFinder);
|
||||||
expect(
|
expect(
|
||||||
transformOfArrow.transform.getRotation(),
|
transformOfArrow.transform.getRotation(),
|
||||||
|
@ -12,6 +12,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:matcher/expect.dart';
|
import 'package:matcher/expect.dart';
|
||||||
import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports
|
import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports
|
||||||
|
import 'package:vector_math/vector_math_64.dart' show Matrix3;
|
||||||
|
|
||||||
import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage;
|
import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage;
|
||||||
import 'accessibility.dart';
|
import 'accessibility.dart';
|
||||||
@ -345,10 +346,24 @@ Matcher rectMoreOrLessEquals(Rect value, { double epsilon = precisionErrorTolera
|
|||||||
///
|
///
|
||||||
/// * [moreOrLessEquals], which is for [double]s.
|
/// * [moreOrLessEquals], which is for [double]s.
|
||||||
/// * [offsetMoreOrLessEquals], which is for [Offset]s.
|
/// * [offsetMoreOrLessEquals], which is for [Offset]s.
|
||||||
|
/// * [matrix3MoreOrLessEquals], which is for [Matrix3]s.
|
||||||
Matcher matrixMoreOrLessEquals(Matrix4 value, { double epsilon = precisionErrorTolerance }) {
|
Matcher matrixMoreOrLessEquals(Matrix4 value, { double epsilon = precisionErrorTolerance }) {
|
||||||
return _IsWithinDistance<Matrix4>(_matrixDistance, value, epsilon);
|
return _IsWithinDistance<Matrix4>(_matrixDistance, value, epsilon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Asserts that two [Matrix3]s are equal, within some tolerated error.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.flutter_test.moreOrLessEquals}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [moreOrLessEquals], which is for [double]s.
|
||||||
|
/// * [offsetMoreOrLessEquals], which is for [Offset]s.
|
||||||
|
/// * [matrixMoreOrLessEquals], which is for [Matrix4]s.
|
||||||
|
Matcher matrix3MoreOrLessEquals(Matrix3 value, { double epsilon = precisionErrorTolerance }) {
|
||||||
|
return _IsWithinDistance<Matrix3>(_matrix3Distance, value, epsilon);
|
||||||
|
}
|
||||||
|
|
||||||
/// Asserts that two [Offset]s are equal, within some tolerated error.
|
/// Asserts that two [Offset]s are equal, within some tolerated error.
|
||||||
///
|
///
|
||||||
/// {@macro flutter.flutter_test.moreOrLessEquals}
|
/// {@macro flutter.flutter_test.moreOrLessEquals}
|
||||||
@ -1443,6 +1458,14 @@ double _matrixDistance(Matrix4 a, Matrix4 b) {
|
|||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _matrix3Distance(Matrix3 a, Matrix3 b) {
|
||||||
|
double delta = 0.0;
|
||||||
|
for (int i = 0; i < 9; i += 1) {
|
||||||
|
delta = math.max<double>((a[i] - b[i]).abs(), delta);
|
||||||
|
}
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
double _sizeDistance(Size a, Size b) {
|
double _sizeDistance(Size a, Size b) {
|
||||||
// TODO(a14n): remove ignore when lint is updated, https://github.com/dart-lang/linter/issues/1843
|
// TODO(a14n): remove ignore when lint is updated, https://github.com/dart-lang/linter/issues/1843
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
@ -10,6 +10,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:vector_math/vector_math_64.dart' show Matrix3;
|
||||||
|
|
||||||
/// Class that makes it easy to mock common toStringDeep behavior.
|
/// Class that makes it easy to mock common toStringDeep behavior.
|
||||||
class _MockToStringDeep {
|
class _MockToStringDeep {
|
||||||
@ -251,6 +252,35 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('matrix3MoreOrLessEquals', () {
|
||||||
|
expect(
|
||||||
|
Matrix3.rotationZ(math.pi),
|
||||||
|
matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
|
||||||
|
-1, 0, 0,
|
||||||
|
0, -1, 0,
|
||||||
|
0, 0, 1,
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Matrix3.rotationZ(math.pi),
|
||||||
|
matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
|
||||||
|
-2, 0, 0,
|
||||||
|
0, -2, 0,
|
||||||
|
0, 0, 1,
|
||||||
|
]), epsilon: 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Matrix3.rotationZ(math.pi),
|
||||||
|
isNot(matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
|
||||||
|
-2, 0, 0,
|
||||||
|
0, -2, 0,
|
||||||
|
0, 0, 1,
|
||||||
|
])))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('rectMoreOrLessEquals', () {
|
test('rectMoreOrLessEquals', () {
|
||||||
expect(
|
expect(
|
||||||
const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
|
const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
|
||||||
|
Loading…
Reference in New Issue
Block a user