Commit 0618da7c authored by Ian Hickson's avatar Ian Hickson

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.
parent bf6ae3ee
......@@ -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:
......
// 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);
}
}
}
......
// 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;
}
\ No newline at end of file
// 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.
......
// 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();
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment