PaginatedDataTable (part 1) (#4306)
This introduces the key parts of a paginated data table, not including the built-in pagination features. * Provide more data for the data table demo, so there's data to page. * Introduce a ChangeNotifier class which abstracts out addListener/removeListener/notifyListeners. We might be able to use this to simplify existing classes as well, though this patch doesn't do that. * Introduce DataTableSource, a delegate for getting data for data tables. This will also be used by ScrollingDataTable in due course. * Introduce PaginatedDataTable, a widget that wraps DataTable and only shows N rows at a time, fed by a DataTableSource.
This commit is contained in:
parent
bf6ae3ee76
commit
0618da7ca2
@ -19,6 +19,111 @@ class Desert {
|
||||
bool selected = false;
|
||||
}
|
||||
|
||||
class DesertDataSource extends DataTableSource {
|
||||
final List<Desert> _deserts = <Desert>[
|
||||
new Desert('Frozen yogurt', 159, 6.0, 24, 4.0, 87, 14, 1),
|
||||
new Desert('Ice cream sandwich', 237, 9.0, 37, 4.3, 129, 8, 1),
|
||||
new Desert('Eclair', 262, 16.0, 24, 6.0, 337, 6, 7),
|
||||
new Desert('Cupcake', 305, 3.7, 67, 4.3, 413, 3, 8),
|
||||
new Desert('Gingerbread', 356, 16.0, 49, 3.9, 327, 7, 16),
|
||||
new Desert('Jelly bean', 375, 0.0, 94, 0.0, 50, 0, 0),
|
||||
new Desert('Lollipop', 392, 0.2, 98, 0.0, 38, 0, 2),
|
||||
new Desert('Honeycomb', 408, 3.2, 87, 6.5, 562, 0, 45),
|
||||
new Desert('Donut', 452, 25.0, 51, 4.9, 326, 2, 22),
|
||||
new Desert('KitKat', 518, 26.0, 65, 7.0, 54, 12, 6),
|
||||
|
||||
new Desert('Frozen yogurt with sugar', 168, 6.0, 26, 4.0, 87, 14, 1),
|
||||
new Desert('Ice cream sandwich with sugar', 246, 9.0, 39, 4.3, 129, 8, 1),
|
||||
new Desert('Eclair with sugar', 271, 16.0, 26, 6.0, 337, 6, 7),
|
||||
new Desert('Cupcake with sugar', 314, 3.7, 69, 4.3, 413, 3, 8),
|
||||
new Desert('Gingerbread with sugar', 345, 16.0, 51, 3.9, 327, 7, 16),
|
||||
new Desert('Jelly bean with sugar', 364, 0.0, 96, 0.0, 50, 0, 0),
|
||||
new Desert('Lollipop with sugar', 401, 0.2, 100, 0.0, 38, 0, 2),
|
||||
new Desert('Honeycomb with sugar', 417, 3.2, 89, 6.5, 562, 0, 45),
|
||||
new Desert('Donut with sugar', 461, 25.0, 53, 4.9, 326, 2, 22),
|
||||
new Desert('KitKat with sugar', 527, 26.0, 67, 7.0, 54, 12, 6),
|
||||
|
||||
new Desert('Frozen yogurt with honey', 223, 6.0, 36, 4.0, 87, 14, 1),
|
||||
new Desert('Ice cream sandwich with honey', 301, 9.0, 49, 4.3, 129, 8, 1),
|
||||
new Desert('Eclair with honey', 326, 16.0, 36, 6.0, 337, 6, 7),
|
||||
new Desert('Cupcake with honey', 369, 3.7, 79, 4.3, 413, 3, 8),
|
||||
new Desert('Gingerbread with honey', 420, 16.0, 61, 3.9, 327, 7, 16),
|
||||
new Desert('Jelly bean with honey', 439, 0.0, 106, 0.0, 50, 0, 0),
|
||||
new Desert('Lollipop with honey', 456, 0.2, 110, 0.0, 38, 0, 2),
|
||||
new Desert('Honeycomb with honey', 472, 3.2, 99, 6.5, 562, 0, 45),
|
||||
new Desert('Donut with honey', 516, 25.0, 63, 4.9, 326, 2, 22),
|
||||
new Desert('KitKat with honey', 582, 26.0, 77, 7.0, 54, 12, 6),
|
||||
|
||||
new Desert('Frozen yogurt with milk', 262, 8.4, 36, 12.0, 194, 44, 1),
|
||||
new Desert('Ice cream sandwich with milk', 339, 11.4, 49, 12.3, 236, 38, 1),
|
||||
new Desert('Eclair with milk', 365, 18.4, 36, 14.0, 444, 36, 7),
|
||||
new Desert('Cupcake with milk', 408, 6.1, 79, 12.3, 520, 33, 8),
|
||||
new Desert('Gingerbread with milk', 459, 18.4, 61, 11.9, 434, 37, 16),
|
||||
new Desert('Jelly bean with milk', 478, 2.4, 106, 8.0, 157, 30, 0),
|
||||
new Desert('Lollipop with milk', 495, 2.6, 110, 8.0, 145, 30, 2),
|
||||
new Desert('Honeycomb with milk', 511, 5.6, 99, 14.5, 669, 30, 45),
|
||||
new Desert('Donut with milk', 555, 27.4, 63, 12.9, 433, 32, 22),
|
||||
new Desert('KitKat with milk', 621, 28.4, 77, 15.0, 161, 42, 6),
|
||||
|
||||
new Desert('Coconut slice and frozen yogurt', 318, 21.0, 31, 5.5, 96, 14, 7),
|
||||
new Desert('Coconut slice and ice cream sandwich', 396, 24.0, 44, 5.8, 138, 8, 7),
|
||||
new Desert('Coconut slice and eclair', 421, 31.0, 31, 7.5, 346, 6, 13),
|
||||
new Desert('Coconut slice and cupcake', 464, 18.7, 74, 5.8, 422, 3, 14),
|
||||
new Desert('Coconut slice and gingerbread', 515, 31.0, 56, 5.4, 316, 7, 22),
|
||||
new Desert('Coconut slice and jelly bean', 534, 15.0, 101, 1.5, 59, 0, 6),
|
||||
new Desert('Coconut slice and lollipop', 551, 15.2, 105, 1.5, 47, 0, 8),
|
||||
new Desert('Coconut slice and honeycomb', 567, 18.2, 94, 8.0, 571, 0, 51),
|
||||
new Desert('Coconut slice and donut', 611, 40.0, 58, 6.4, 335, 2, 28),
|
||||
new Desert('Coconut slice and KitKat', 677, 41.0, 72, 8.5, 63, 12, 12),
|
||||
];
|
||||
|
||||
void _sort/*<T>*/(Comparable<dynamic/*=T*/> getField(Desert d), bool ascending) {
|
||||
_deserts.sort((Desert a, Desert b) {
|
||||
if (!ascending) {
|
||||
final Desert c = a;
|
||||
a = b;
|
||||
b = c;
|
||||
}
|
||||
final Comparable<dynamic/*=T*/> aValue = getField(a);
|
||||
final Comparable<dynamic/*=T*/> bValue = getField(b);
|
||||
return Comparable.compare(aValue, bValue);
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
DataRow getRow(int index) {
|
||||
assert(index >= 0);
|
||||
if (index >= _deserts.length)
|
||||
return null;
|
||||
final Desert desert = _deserts[index];
|
||||
return new DataRow.byIndex(
|
||||
index: index,
|
||||
selected: desert.selected,
|
||||
onSelectChanged: (bool value) {
|
||||
desert.selected = value;
|
||||
notifyListeners();
|
||||
},
|
||||
cells: <DataCell>[
|
||||
new DataCell(new Text('${desert.name}')),
|
||||
new DataCell(new Text('${desert.calories}')),
|
||||
new DataCell(new Text('${desert.fat.toStringAsFixed(1)}')),
|
||||
new DataCell(new Text('${desert.carbs}')),
|
||||
new DataCell(new Text('${desert.protein.toStringAsFixed(1)}')),
|
||||
new DataCell(new Text('${desert.sodium}')),
|
||||
new DataCell(new Text('${desert.calcium}%')),
|
||||
new DataCell(new Text('${desert.iron}%')),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get rowCount => _deserts.length;
|
||||
|
||||
@override
|
||||
bool get isRowCountApproximate => true;
|
||||
}
|
||||
|
||||
class DataTableDemo extends StatefulWidget {
|
||||
static const String routeName = '/data-table';
|
||||
|
||||
@ -27,35 +132,13 @@ class DataTableDemo extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DataTableDemoState extends State<DataTableDemo> {
|
||||
|
||||
int _sortColumnIndex;
|
||||
bool _sortAscending = true;
|
||||
|
||||
final List<Desert> _deserts = <Desert>[
|
||||
new Desert('Frozen yogurt', 159, 6.0, 24, 4.0, 87, 14, 1),
|
||||
new Desert('Ice cream sandwich', 237, 9.0, 37, 4.3, 129, 8, 1),
|
||||
new Desert('Eclair', 262, 16.0, 24, 6.0, 337, 6, 7),
|
||||
new Desert('Cupcake', 305, 3.7, 67, 4.3, 413, 3, 8),
|
||||
new Desert('Gingerbread', 356, 16.0, 49, 3.9, 327, 7, 16),
|
||||
new Desert('Jelly bean', 375, 0.0, 94, 0.0, 50, 0, 0),
|
||||
new Desert('Lollipop', 392, 0.2, 98, 0.0, 38, 0, 2),
|
||||
new Desert('Honeycomb', 408, 3.2, 87, 6.5, 562, 0, 45),
|
||||
new Desert('Donut', 452, 25.0, 51, 4.9, 326, 2, 22),
|
||||
new Desert('KitKat', 518, 26.0, 65, 7.0, 54, 12, 6),
|
||||
];
|
||||
DesertDataSource _deserts = new DesertDataSource();
|
||||
|
||||
void _sort/*<T>*/(Comparable<dynamic/*=T*/> getField(Desert d), int columnIndex, bool ascending) {
|
||||
_deserts._sort/*<T>*/(getField, ascending);
|
||||
setState(() {
|
||||
_deserts.sort((Desert a, Desert b) {
|
||||
if (!ascending) {
|
||||
final Desert c = a;
|
||||
a = b;
|
||||
b = c;
|
||||
}
|
||||
final Comparable<dynamic/*=T*/> aValue = getField(a);
|
||||
final Comparable<dynamic/*=T*/> bValue = getField(b);
|
||||
return Comparable.compare(aValue, bValue);
|
||||
});
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
});
|
||||
@ -67,77 +150,61 @@ class _DataTableDemoState extends State<DataTableDemo> {
|
||||
appBar: new AppBar(title: new Text('Data tables')),
|
||||
body: new Block(
|
||||
children: <Widget>[
|
||||
new Material(
|
||||
child: new IntrinsicHeight(
|
||||
child: new Block(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
new DataTable(
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
columns: <DataColumn>[
|
||||
new DataColumn(
|
||||
label: new Text('Dessert (100g serving)'),
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<String>*/((Desert d) => d.name, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Calories'),
|
||||
tooltip: 'The total amount of food energy in the given serving size.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Fat (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Carbs (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Protein (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Sodium (mg)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Calcium (%)'),
|
||||
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Iron (%)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
|
||||
),
|
||||
],
|
||||
rows: _deserts.map/*<DataRow>*/((Desert desert) {
|
||||
return new DataRow(
|
||||
key: new ValueKey<Desert>(desert),
|
||||
selected: desert.selected,
|
||||
onSelectChanged: (bool selected) { setState(() { desert.selected = selected; }); },
|
||||
cells: <DataCell>[
|
||||
new DataCell(new Text('${desert.name}')),
|
||||
new DataCell(new Text('${desert.calories}')),
|
||||
new DataCell(new Text('${desert.fat.toStringAsFixed(1)}')),
|
||||
new DataCell(new Text('${desert.carbs}')),
|
||||
new DataCell(new Text('${desert.protein.toStringAsFixed(1)}')),
|
||||
new DataCell(new Text('${desert.sodium}')),
|
||||
new DataCell(new Text('${desert.calcium}%')),
|
||||
new DataCell(new Text('${desert.iron}%')),
|
||||
]
|
||||
);
|
||||
}).toList(growable: false)
|
||||
)
|
||||
]
|
||||
)
|
||||
new IntrinsicHeight(
|
||||
child: new Block(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
new PaginatedDataTable(
|
||||
rowsPerPage: 10,
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
columns: <DataColumn>[
|
||||
new DataColumn(
|
||||
label: new Text('Dessert (100g serving)'),
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<String>*/((Desert d) => d.name, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Calories'),
|
||||
tooltip: 'The total amount of food energy in the given serving size.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Fat (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Carbs (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Protein (g)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Sodium (mg)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Calcium (%)'),
|
||||
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending)
|
||||
),
|
||||
new DataColumn(
|
||||
label: new Text('Iron (%)'),
|
||||
numeric: true,
|
||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
|
||||
),
|
||||
],
|
||||
source: _deserts
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
|
@ -12,4 +12,5 @@ library foundation;
|
||||
export 'src/foundation/assertions.dart';
|
||||
export 'src/foundation/basic_types.dart';
|
||||
export 'src/foundation/binding.dart';
|
||||
export 'src/foundation/change_notifier.dart';
|
||||
export 'src/foundation/print.dart';
|
||||
|
@ -23,6 +23,7 @@ export 'src/material/circle_avatar.dart';
|
||||
export 'src/material/colors.dart';
|
||||
export 'src/material/constants.dart';
|
||||
export 'src/material/data_table.dart';
|
||||
export 'src/material/data_table_source.dart';
|
||||
export 'src/material/date_picker.dart';
|
||||
export 'src/material/date_picker_dialog.dart';
|
||||
export 'src/material/dialog.dart';
|
||||
@ -48,6 +49,7 @@ export 'src/material/list_item.dart';
|
||||
export 'src/material/material.dart';
|
||||
export 'src/material/overscroll_indicator.dart';
|
||||
export 'src/material/page.dart';
|
||||
export 'src/material/paginated_data_table.dart';
|
||||
export 'src/material/popup_menu.dart';
|
||||
export 'src/material/progress_indicator.dart';
|
||||
export 'src/material/radio.dart';
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
// COMMON SIGNATURES
|
||||
|
||||
export 'dart:ui' show VoidCallback;
|
||||
|
||||
/// Signature for callbacks that report that an underlying value has changed.
|
||||
@ -28,6 +30,9 @@ typedef T ValueGetter<T>();
|
||||
/// Signature for callbacks that filter an iterable.
|
||||
typedef Iterable<T> IterableFilter<T>(Iterable<T> input);
|
||||
|
||||
|
||||
// BITFIELD
|
||||
|
||||
/// The largest SMI value.
|
||||
///
|
||||
/// See <https://www.dartlang.org/articles/numeric-computation/#smis-and-mints>
|
||||
@ -86,6 +91,9 @@ class BitField<T extends dynamic> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// LAZY CACHING ITERATOR
|
||||
|
||||
/// A lazy caching version of [Iterable].
|
||||
///
|
||||
/// This iterable is efficient in the following ways:
|
||||
|
65
packages/flutter/lib/src/foundation/change_notifier.dart
Normal file
65
packages/flutter/lib/src/foundation/change_notifier.dart
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2015 The Chromium 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:meta/meta.dart';
|
||||
|
||||
import 'assertions.dart';
|
||||
import 'basic_types.dart';
|
||||
|
||||
/// Abstract class that can be extended or mixed in that provides
|
||||
/// a change notification API using [VoidCallback] for notifications.
|
||||
abstract class ChangeNotifier {
|
||||
List<VoidCallback> _listeners;
|
||||
|
||||
/// Register a closure to be called when the object changes.
|
||||
void addListener(VoidCallback listener) {
|
||||
_listeners ??= <VoidCallback>[];
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
/// Remove a previously registered closure from the list of closures that are
|
||||
/// notified when the object changes.
|
||||
void removeListener(VoidCallback listener) {
|
||||
_listeners?.remove(listener);
|
||||
}
|
||||
|
||||
/// Discards any resources used by the object. After this is called, the object
|
||||
/// is not in a usable state and should be discarded.
|
||||
///
|
||||
/// This method should only be called by the object's owner.
|
||||
@mustCallSuper
|
||||
void dispose() {
|
||||
_listeners = null;
|
||||
}
|
||||
|
||||
/// Call all the registered listeners.
|
||||
///
|
||||
/// Call this method whenever the object changes, to notify any clients the
|
||||
/// object may have.
|
||||
///
|
||||
/// Exceptions thrown by listeners will be caught and reported using
|
||||
/// [FlutterError.reportError].
|
||||
@protected
|
||||
void notifyListeners() {
|
||||
if (_listeners != null) {
|
||||
List<VoidCallback> listeners = new List<VoidCallback>.from(_listeners);
|
||||
for (VoidCallback listener in listeners) {
|
||||
try {
|
||||
listener();
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(new FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'foundation library',
|
||||
context: 'while dispatching notifications for $runtimeType',
|
||||
informationCollector: (StringBuffer information) {
|
||||
information.writeln('The $runtimeType sending notification was:');
|
||||
information.write(' $this');
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -91,6 +91,17 @@ class DataRow {
|
||||
this.cells
|
||||
});
|
||||
|
||||
/// Creates the configuration for a row of a [DataTable], deriving
|
||||
/// the key from a row index.
|
||||
///
|
||||
/// The [cells] argument must not be null.
|
||||
DataRow.byIndex({
|
||||
int index,
|
||||
this.selected: false,
|
||||
this.onSelectChanged,
|
||||
this.cells
|
||||
}) : key = new ValueKey<int>(index);
|
||||
|
||||
/// A [Key] that uniquely identifies this row. This is used to
|
||||
/// ensure that if a row is added or removed, any stateful widgets
|
||||
/// related to this row (e.g. an in-progress checkbox animation)
|
||||
@ -154,6 +165,8 @@ class DataCell {
|
||||
this.onTap
|
||||
});
|
||||
|
||||
static final DataCell empty = new DataCell(new Container(width: 0.0, height: 0.0));
|
||||
|
||||
/// The data for the row.
|
||||
///
|
||||
/// Typically a [Text] widget or a [DropDownButton] widget.
|
||||
@ -196,16 +209,19 @@ class DataCell {
|
||||
/// dimensions to use for each column, and once to actually lay out
|
||||
/// the table given the results of the negotiation.
|
||||
///
|
||||
// /// For this reason, if you have a lot of data (say, more than a dozen
|
||||
// /// rows with a dozen columns, though the precise limits depend on the
|
||||
// /// target device), it is suggested that you use a [DataCard] which
|
||||
// /// automatically splits the data into multiple pages.
|
||||
// ///
|
||||
/// For this reason, if you have a lot of data (say, more than a dozen
|
||||
/// rows with a dozen columns, though the precise limits depend on the
|
||||
/// target device), it is suggested that you use a
|
||||
/// [PaginatedDataTable] which automatically splits the data into
|
||||
/// multiple pages.
|
||||
// TODO(ianh): Also suggest [ScrollingDataTable] once we have it.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DataColumn]
|
||||
/// * [DataRow]
|
||||
/// * [DataCell]
|
||||
/// * [PaginatedDataTable]
|
||||
/// * <https://www.google.com/design/spec/components/data-tables.html>
|
||||
class DataTable extends StatelessWidget {
|
||||
/// Creates a widget describing a data table.
|
||||
@ -213,7 +229,7 @@ class DataTable extends StatelessWidget {
|
||||
/// The [columns] argument must be a list of as many [DataColumn]
|
||||
/// objects as the table is to have columns, ignoring the leading
|
||||
/// checkbox column if any. The [columns] argument must have a
|
||||
/// length greater than zero.
|
||||
/// length greater than zero and cannot be null.
|
||||
///
|
||||
/// The [rows] argument must be a list of as many [DataRow] objects
|
||||
/// as the table is to have rows, ignoring the leading heading row
|
||||
@ -237,15 +253,16 @@ class DataTable extends StatelessWidget {
|
||||
List<DataColumn> columns,
|
||||
this.sortColumnIndex,
|
||||
this.sortAscending: true,
|
||||
this.onSelectAll,
|
||||
this.rows
|
||||
}) : columns = columns,
|
||||
_onlyTextColumn = _initOnlyTextColumn(columns), super(key: key) {
|
||||
assert(columns != null);
|
||||
assert(columns.length > 0);
|
||||
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
|
||||
assert(sortAscending != null);
|
||||
assert(rows != null);
|
||||
assert(!rows.any((DataRow row) => row.cells.length != columns.length));
|
||||
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
|
||||
}
|
||||
|
||||
/// The configuration and labels for the columns in the table.
|
||||
@ -276,6 +293,17 @@ class DataTable extends StatelessWidget {
|
||||
/// table).
|
||||
final bool sortAscending;
|
||||
|
||||
/// Invoked when the user selects or unselects every row, using the
|
||||
/// checkbox in the heading row.
|
||||
///
|
||||
/// If this is null, then the [DataRow.onSelectChanged] callback of
|
||||
/// every row in the table is invoked appropriately instead.
|
||||
///
|
||||
/// To control whether a particular row is selectable or not, see
|
||||
/// [DataRow.onSelectChanged]. This callback is only relevant if any
|
||||
/// row is selectable.
|
||||
final ValueSetter<bool> onSelectAll;
|
||||
|
||||
/// The data to show in each row (excluding the row that contains
|
||||
/// the column headings). Must be non-null, but may be empty.
|
||||
final List<DataRow> rows;
|
||||
@ -304,9 +332,13 @@ class DataTable extends StatelessWidget {
|
||||
static final LocalKey _headingRowKey = new UniqueKey();
|
||||
|
||||
void _handleSelectAll(bool checked) {
|
||||
for (DataRow row in rows) {
|
||||
if ((row.onSelectChanged != null) && (row.selected != checked))
|
||||
row.onSelectChanged(checked);
|
||||
if (onSelectAll != null) {
|
||||
onSelectAll(checked);
|
||||
} else {
|
||||
for (DataRow row in rows) {
|
||||
if ((row.onSelectChanged != null) && (row.selected != checked))
|
||||
row.onSelectChanged(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
57
packages/flutter/lib/src/material/data_table_source.dart
Normal file
57
packages/flutter/lib/src/material/data_table_source.dart
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2016 The Chromium 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/foundation.dart';
|
||||
import 'data_table.dart';
|
||||
|
||||
/// A data source for obtaining row data for [PaginatedDataTable] objects.
|
||||
///
|
||||
/// A data table source provides two main pieces of information:
|
||||
///
|
||||
/// * The number of rows in the data table ([rowCount]).
|
||||
/// * The data for each row (indexed from `0` to `rowCount - 1`).
|
||||
///
|
||||
/// It also provides a listener API ([addListener]/[removeListener]) so that
|
||||
/// consumers of the data can be notified when it changes. When the data
|
||||
/// changes, call [notifyListeners] to send the notifications.
|
||||
///
|
||||
/// DataTableSource objects are expected to be long-lived, not recreated with
|
||||
/// each build.
|
||||
abstract class DataTableSource extends ChangeNotifier {
|
||||
/// Called to obtain the data about a particular row.
|
||||
///
|
||||
/// The [new DataRow.byIndex] constructor provides a convenient way to construct
|
||||
/// [DataRow] objects for this callback's purposes without having to worry about
|
||||
/// independently keying each row.
|
||||
///
|
||||
/// 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
|
||||
/// 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
|
||||
/// the end of the table, call [notifyListeners].
|
||||
///
|
||||
/// Data returned from this method must be consistent for the lifetime of the
|
||||
/// object. If the row count changes, then a new delegate must be provided.
|
||||
DataRow getRow(int index);
|
||||
|
||||
/// Called to obtain the number of rows to tell the user are available.
|
||||
///
|
||||
/// If [isRowCountApproximate] is false, then this must be an accurate number,
|
||||
/// and [getRow] must return a non-null value for all indices in the range 0
|
||||
/// to one less than the row count.
|
||||
///
|
||||
/// If [isRowCountApproximate] is true, then the user will be allowed to
|
||||
/// attempt to display rows up to this [rowCount], and the display will
|
||||
/// indicate that the count is approximate. The row count should therefore be
|
||||
/// greater than the actual number of rows if at all possible.
|
||||
///
|
||||
/// If the row count changes, call [notifyListeners].
|
||||
int get rowCount;
|
||||
|
||||
/// Called to establish if [rowCount] is a precise number or might be an
|
||||
/// over-estimate. If this returns true (i.e. the count is approximate), and
|
||||
/// then later the exact number becomes available, then call
|
||||
/// [notifyListeners].
|
||||
bool get isRowCountApproximate;
|
||||
}
|
237
packages/flutter/lib/src/material/paginated_data_table.dart
Normal file
237
packages/flutter/lib/src/material/paginated_data_table.dart
Normal file
@ -0,0 +1,237 @@
|
||||
// Copyright 2016 The Chromium 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/widgets.dart';
|
||||
|
||||
import 'card.dart';
|
||||
import 'data_table.dart';
|
||||
import 'data_table_source.dart';
|
||||
import 'progress_indicator.dart';
|
||||
|
||||
/// A wrapper for [DataTable] that obtains data lazily from a [DataTableSource]
|
||||
/// and displays it one page at a time. The widget is presented as a [Card].
|
||||
class PaginatedDataTable extends StatefulWidget {
|
||||
/// Creates a widget describing a paginated [DataTable] on a [Card].
|
||||
///
|
||||
/// The [columns] argument must be a list of as many [DataColumn] objects as
|
||||
/// the table is to have columns, ignoring the leading checkbox column if any.
|
||||
/// The [columns] argument must have a length greater than zero and cannot be
|
||||
/// null.
|
||||
///
|
||||
/// If the table is sorted, the column that provides the current primary key
|
||||
/// should be specified by index in [sortColumnIndex], 0 meaning the first
|
||||
/// column in [columns], 1 being the next one, and so forth.
|
||||
///
|
||||
/// The actual sort order can be specified using [sortAscending]; if the sort
|
||||
/// order is ascending, this should be true (the default), otherwise it should
|
||||
/// be false.
|
||||
///
|
||||
/// The [source] must not be null. The [source] should be a long-lived
|
||||
/// [DataTableSource]. The same source should be provided each time a
|
||||
/// particular [PaginatedDataTable] widget is created; avoid creating a new
|
||||
/// [DataTableSource] with each new instance of the [PaginatedDataTable]
|
||||
/// widget unless the data table really is to now show entirely different
|
||||
/// data from a new source.
|
||||
PaginatedDataTable({
|
||||
Key key,
|
||||
this.columns,
|
||||
this.sortColumnIndex,
|
||||
this.sortAscending: true,
|
||||
this.onSelectAll,
|
||||
this.initialFirstRowIndex: 0,
|
||||
this.onPageChanged,
|
||||
this.rowsPerPage: 10,
|
||||
this.onRowsPerPageChanged,
|
||||
this.source
|
||||
}) : super(key: key) {
|
||||
assert(columns != null);
|
||||
assert(columns.length > 0);
|
||||
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
|
||||
assert(sortAscending != null);
|
||||
assert(source != null);
|
||||
}
|
||||
|
||||
/// The configuration and labels for the columns in the table.
|
||||
final List<DataColumn> columns;
|
||||
|
||||
/// The current primary sort key's column.
|
||||
///
|
||||
/// See [DataTable.sortColumnIndex].
|
||||
final int sortColumnIndex;
|
||||
|
||||
/// Whether the column mentioned in [sortColumnIndex], if any, is sorted
|
||||
/// in ascending order.
|
||||
///
|
||||
/// See [DataTable.sortAscending].
|
||||
final bool sortAscending;
|
||||
|
||||
/// Invoked when the user selects or unselects every row, using the
|
||||
/// checkbox in the heading row.
|
||||
///
|
||||
/// See [DataTable.onSelectAll].
|
||||
final ValueSetter<bool> onSelectAll;
|
||||
|
||||
/// The index of the first row to display when the widget is first created.
|
||||
final int initialFirstRowIndex;
|
||||
|
||||
/// Invoked when the user switches to another page.
|
||||
final ValueChanged<int> onPageChanged;
|
||||
|
||||
/// The number of rows to show on each page.
|
||||
///
|
||||
/// See also [onRowsPerPageChanged].
|
||||
final int rowsPerPage;
|
||||
|
||||
/// Invoked when the user selects a different number of rows per page.
|
||||
///
|
||||
/// If this is null, then the value given by [rowsPerPage] will be used
|
||||
/// and no affordance will be provided to change the value.
|
||||
final ValueChanged<int> onRowsPerPageChanged;
|
||||
|
||||
/// The data source which provides data to show in each row. Must be non-null.
|
||||
///
|
||||
/// This object should generally have a lifetime longer than the
|
||||
/// [PaginatedDataTable] widget itself; it should be reused each time the
|
||||
/// [PaginatedDataTable] constructor is called.
|
||||
final DataTableSource source;
|
||||
|
||||
@override
|
||||
PaginatedDataTableState createState() => new PaginatedDataTableState();
|
||||
}
|
||||
|
||||
/// Holds the state of a [PaginatedDataTable].
|
||||
///
|
||||
/// The table can be programmatically paged using the [pageTo] method.
|
||||
class PaginatedDataTableState extends State<PaginatedDataTable> {
|
||||
int _firstRowIndex;
|
||||
int _rowCount;
|
||||
bool _rowCountApproximate;
|
||||
final Map<int, DataRow> _rows = <int, DataRow>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_firstRowIndex = PageStorage.of(context)?.readState(context) ?? config.initialFirstRowIndex ?? 0;
|
||||
config.source.addListener(_handleDataSourceChanged);
|
||||
_handleDataSourceChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateConfig(PaginatedDataTable oldConfig) {
|
||||
super.didUpdateConfig(oldConfig);
|
||||
if (oldConfig.source != config.source) {
|
||||
oldConfig.source.removeListener(_handleDataSourceChanged);
|
||||
config.source.addListener(_handleDataSourceChanged);
|
||||
_handleDataSourceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
config.source.removeListener(_handleDataSourceChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleDataSourceChanged() {
|
||||
setState(() {
|
||||
_rowCount = config.source.rowCount;
|
||||
_rowCountApproximate = config.source.isRowCountApproximate;
|
||||
_rows.clear();
|
||||
});
|
||||
}
|
||||
|
||||
/// Ensures that the given row is visible.
|
||||
void pageTo(double rowIndex) {
|
||||
setState(() {
|
||||
final int rowsPerPage = config.rowsPerPage;
|
||||
_firstRowIndex = (rowIndex ~/ rowsPerPage) * rowsPerPage;
|
||||
});
|
||||
}
|
||||
|
||||
DataRow _getBlankRowFor(int index) {
|
||||
return new DataRow.byIndex(
|
||||
index: index,
|
||||
cells: config.columns.map/*<DataCell>*/((DataColumn column) => DataCell.empty).toList()
|
||||
);
|
||||
}
|
||||
|
||||
DataRow _getProgressIndicatorRowFor(int index) {
|
||||
bool haveProgressIndicator = false;
|
||||
final List<DataCell> cells = config.columns.map/*<DataCell>*/((DataColumn column) {
|
||||
if (!column.numeric) {
|
||||
haveProgressIndicator = true;
|
||||
return new DataCell(new CircularProgressIndicator());
|
||||
}
|
||||
return DataCell.empty;
|
||||
}).toList();
|
||||
if (!haveProgressIndicator) {
|
||||
haveProgressIndicator = true;
|
||||
cells[0] = new DataCell(new CircularProgressIndicator());
|
||||
}
|
||||
return new DataRow.byIndex(
|
||||
index: index,
|
||||
cells: cells
|
||||
);
|
||||
}
|
||||
|
||||
List<DataRow> _getRows(int firstRowIndex, int rowsPerPage) {
|
||||
final List<DataRow> result = <DataRow>[];
|
||||
final int nextPageFirstRowIndex = firstRowIndex + rowsPerPage;
|
||||
bool haveProgressIndicator = false;
|
||||
for (int index = firstRowIndex; index < nextPageFirstRowIndex; index += 1) {
|
||||
DataRow row;
|
||||
if (index < _rowCount || _rowCountApproximate) {
|
||||
row = _rows.putIfAbsent(index, () => config.source.getRow(index));
|
||||
if (row == null && !haveProgressIndicator) {
|
||||
row ??= _getProgressIndicatorRowFor(index);
|
||||
haveProgressIndicator = true;
|
||||
}
|
||||
}
|
||||
row ??= _getBlankRowFor(index);
|
||||
result.add(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
final GlobalKey _tableKey = new GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<DataRow> rows = _getRows(_firstRowIndex, config.rowsPerPage);
|
||||
Widget table = new DataTable(
|
||||
key: _tableKey,
|
||||
columns: config.columns,
|
||||
sortColumnIndex: config.sortColumnIndex,
|
||||
sortAscending: config.sortAscending,
|
||||
onSelectAll: config.onSelectAll,
|
||||
rows: rows
|
||||
);
|
||||
return new Card(
|
||||
// TODO(ianh): data table card headers
|
||||
child: table
|
||||
// TODO(ianh): data table card footers: prev/next page, rows per page, etc
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
DataTableCard
|
||||
- top: 64px
|
||||
- caption, top left
|
||||
- 20px Roboto Regular, black87
|
||||
- persistent actions, top left
|
||||
- header when there's a selection
|
||||
- accent 50?
|
||||
- show number of selected items
|
||||
- different actions
|
||||
- actions, top right
|
||||
- 24px icons, black54
|
||||
|
||||
bottom:
|
||||
- 56px
|
||||
- handles pagination
|
||||
- 12px Roboto Regular, black54
|
||||
|
||||
*/
|
@ -76,7 +76,7 @@ abstract class FlowDelegate {
|
||||
/// By default, the children will receive the given constraints, which are the
|
||||
/// constrains the constraints used to size the container. The children need
|
||||
/// not respect the given constraints, but they are required to respect the
|
||||
/// returned constraints. For example, the incoming constraings might require
|
||||
/// returned constraints. For example, the incoming constraints might require
|
||||
/// the container to have a width of exactly 100.0 and a height of exactly
|
||||
/// 100.0, but this function might give the children looser constraints that
|
||||
/// let them be larger or smaller than 100.0 by 100.0.
|
||||
|
@ -1353,7 +1353,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
}
|
||||
if (targetFrame != null && targetFrame < stack.length) {
|
||||
information.writeln(
|
||||
'These invalid constraints were provided to $runtimeType\'s method() '
|
||||
'These invalid constraints were provided to $runtimeType\'s layout() '
|
||||
'function by the following function, which probably computed the '
|
||||
'invalid constraints in question:'
|
||||
);
|
||||
|
@ -448,7 +448,7 @@ class RenderAspectRatio extends RenderProxyBox {
|
||||
}
|
||||
|
||||
// Similar to RenderImage, we iteratively attempt to fit within the given
|
||||
// constraings while maintaining the given aspect ratio. The order of
|
||||
// constraints while maintaining the given aspect ratio. The order of
|
||||
// applying the constraints is also biased towards inferring the height
|
||||
// from the width.
|
||||
|
||||
|
@ -617,7 +617,7 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
|
||||
///
|
||||
/// If non-null, the child is given a tight width constraint that is the max
|
||||
/// incoming width constraint multipled by this factor. If null, the child is
|
||||
/// given the incoming width constraings.
|
||||
/// given the incoming width constraints.
|
||||
double get widthFactor => _widthFactor;
|
||||
double _widthFactor;
|
||||
set widthFactor (double value) {
|
||||
@ -632,7 +632,7 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
|
||||
///
|
||||
/// If non-null, the child is given a tight height constraint that is the max
|
||||
/// incoming width constraint multipled by this factor. If null, the child is
|
||||
/// given the incoming width constraings.
|
||||
/// given the incoming width constraints.
|
||||
double get heightFactor => _heightFactor;
|
||||
double _heightFactor;
|
||||
set heightFactor (double value) {
|
||||
|
@ -847,12 +847,18 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
|
||||
///
|
||||
/// If non-null, the child is given a tight width constraint that is the max
|
||||
/// incoming width constraint multipled by this factor.
|
||||
///
|
||||
/// If null, the incoming width constraints are passed to the child
|
||||
/// unmodified.
|
||||
final double widthFactor;
|
||||
|
||||
/// If non-null, the fraction of the incoming height given to the child.
|
||||
///
|
||||
/// If non-null, the child is given a tight height constraint that is the max
|
||||
/// incoming height constraint multipled by this factor.
|
||||
///
|
||||
/// If null, the incoming height constraints are passed to the child
|
||||
/// unmodified.
|
||||
final double heightFactor;
|
||||
|
||||
/// How to align the child.
|
||||
|
116
packages/flutter/test/foundation/change_notifier_test.dart
Normal file
116
packages/flutter/test/foundation/change_notifier_test.dart
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright 2016 The Chromium 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/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class TestNotifier extends ChangeNotifier {
|
||||
void notify() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('ChangeNotifier', (WidgetTester tester) async {
|
||||
final List<String> log = <String>[];
|
||||
final VoidCallback listener = () { log.add('listener'); };
|
||||
final VoidCallback listener1 = () { log.add('listener1'); };
|
||||
final VoidCallback listener2 = () { log.add('listener2'); };
|
||||
final VoidCallback badListener = () { log.add('badListener'); throw null; };
|
||||
|
||||
final TestNotifier test = new TestNotifier();
|
||||
|
||||
test.addListener(listener);
|
||||
test.addListener(listener);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener', 'listener']));
|
||||
log.clear();
|
||||
|
||||
test.removeListener(listener);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener']));
|
||||
log.clear();
|
||||
|
||||
test.removeListener(listener);
|
||||
test.notify();
|
||||
expect(log, equals(<String>[]));
|
||||
log.clear();
|
||||
|
||||
test.removeListener(listener);
|
||||
test.notify();
|
||||
expect(log, equals(<String>[]));
|
||||
log.clear();
|
||||
|
||||
test.addListener(listener);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener']));
|
||||
log.clear();
|
||||
|
||||
test.addListener(listener1);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener', 'listener1']));
|
||||
log.clear();
|
||||
|
||||
test.addListener(listener2);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener', 'listener1', 'listener2']));
|
||||
log.clear();
|
||||
|
||||
test.removeListener(listener1);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener', 'listener2']));
|
||||
log.clear();
|
||||
|
||||
test.addListener(listener1);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener', 'listener2', 'listener1']));
|
||||
log.clear();
|
||||
|
||||
test.addListener(badListener);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener', 'listener2', 'listener1', 'badListener']));
|
||||
expect(tester.takeException(), isNullThrownError);
|
||||
log.clear();
|
||||
|
||||
test.addListener(listener1);
|
||||
test.removeListener(listener);
|
||||
test.removeListener(listener1);
|
||||
test.removeListener(listener2);
|
||||
test.addListener(listener2);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['badListener', 'listener1', 'listener2']));
|
||||
expect(tester.takeException(), isNullThrownError);
|
||||
log.clear();
|
||||
});
|
||||
|
||||
testWidgets('ChangeNotifier with mutating listener', (WidgetTester tester) async {
|
||||
final TestNotifier test = new TestNotifier();
|
||||
final List<String> log = <String>[];
|
||||
|
||||
final VoidCallback listener1 = () { log.add('listener1'); };
|
||||
final VoidCallback listener3 = () { log.add('listener3'); };
|
||||
final VoidCallback listener4 = () { log.add('listener4'); };
|
||||
final VoidCallback listener2 = () {
|
||||
log.add('listener2');
|
||||
test.removeListener(listener1);
|
||||
test.removeListener(listener3);
|
||||
test.addListener(listener4);
|
||||
};
|
||||
|
||||
test.addListener(listener1);
|
||||
test.addListener(listener2);
|
||||
test.addListener(listener3);
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener1', 'listener2', 'listener3']));
|
||||
log.clear();
|
||||
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener2', 'listener4']));
|
||||
log.clear();
|
||||
|
||||
test.notify();
|
||||
expect(log, equals(<String>['listener2', 'listener4', 'listener4']));
|
||||
log.clear();
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user