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
...@@ -12,4 +12,5 @@ library foundation; ...@@ -12,4 +12,5 @@ library foundation;
export 'src/foundation/assertions.dart'; export 'src/foundation/assertions.dart';
export 'src/foundation/basic_types.dart'; export 'src/foundation/basic_types.dart';
export 'src/foundation/binding.dart'; export 'src/foundation/binding.dart';
export 'src/foundation/change_notifier.dart';
export 'src/foundation/print.dart'; export 'src/foundation/print.dart';
...@@ -23,6 +23,7 @@ export 'src/material/circle_avatar.dart'; ...@@ -23,6 +23,7 @@ export 'src/material/circle_avatar.dart';
export 'src/material/colors.dart'; export 'src/material/colors.dart';
export 'src/material/constants.dart'; export 'src/material/constants.dart';
export 'src/material/data_table.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.dart';
export 'src/material/date_picker_dialog.dart'; export 'src/material/date_picker_dialog.dart';
export 'src/material/dialog.dart'; export 'src/material/dialog.dart';
...@@ -48,6 +49,7 @@ export 'src/material/list_item.dart'; ...@@ -48,6 +49,7 @@ export 'src/material/list_item.dart';
export 'src/material/material.dart'; export 'src/material/material.dart';
export 'src/material/overscroll_indicator.dart'; export 'src/material/overscroll_indicator.dart';
export 'src/material/page.dart'; export 'src/material/page.dart';
export 'src/material/paginated_data_table.dart';
export 'src/material/popup_menu.dart'; export 'src/material/popup_menu.dart';
export 'src/material/progress_indicator.dart'; export 'src/material/progress_indicator.dart';
export 'src/material/radio.dart'; export 'src/material/radio.dart';
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import 'dart:collection'; import 'dart:collection';
// COMMON SIGNATURES
export 'dart:ui' show VoidCallback; export 'dart:ui' show VoidCallback;
/// Signature for callbacks that report that an underlying value has changed. /// Signature for callbacks that report that an underlying value has changed.
...@@ -28,6 +30,9 @@ typedef T ValueGetter<T>(); ...@@ -28,6 +30,9 @@ typedef T ValueGetter<T>();
/// Signature for callbacks that filter an iterable. /// Signature for callbacks that filter an iterable.
typedef Iterable<T> IterableFilter<T>(Iterable<T> input); typedef Iterable<T> IterableFilter<T>(Iterable<T> input);
// BITFIELD
/// The largest SMI value. /// The largest SMI value.
/// ///
/// See <https://www.dartlang.org/articles/numeric-computation/#smis-and-mints> /// See <https://www.dartlang.org/articles/numeric-computation/#smis-and-mints>
...@@ -86,6 +91,9 @@ class BitField<T extends dynamic> { ...@@ -86,6 +91,9 @@ class BitField<T extends dynamic> {
} }
} }
// LAZY CACHING ITERATOR
/// A lazy caching version of [Iterable]. /// A lazy caching version of [Iterable].
/// ///
/// This iterable is efficient in the following ways: /// 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 { ...@@ -91,6 +91,17 @@ class DataRow {
this.cells 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 /// A [Key] that uniquely identifies this row. This is used to
/// ensure that if a row is added or removed, any stateful widgets /// ensure that if a row is added or removed, any stateful widgets
/// related to this row (e.g. an in-progress checkbox animation) /// related to this row (e.g. an in-progress checkbox animation)
...@@ -154,6 +165,8 @@ class DataCell { ...@@ -154,6 +165,8 @@ class DataCell {
this.onTap this.onTap
}); });
static final DataCell empty = new DataCell(new Container(width: 0.0, height: 0.0));
/// The data for the row. /// The data for the row.
/// ///
/// Typically a [Text] widget or a [DropDownButton] widget. /// Typically a [Text] widget or a [DropDownButton] widget.
...@@ -196,16 +209,19 @@ class DataCell { ...@@ -196,16 +209,19 @@ class DataCell {
/// dimensions to use for each column, and once to actually lay out /// dimensions to use for each column, and once to actually lay out
/// the table given the results of the negotiation. /// the table given the results of the negotiation.
/// ///
// /// For this reason, if you have a lot of data (say, more than a dozen /// 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 /// rows with a dozen columns, though the precise limits depend on the
// /// target device), it is suggested that you use a [DataCard] which /// target device), it is suggested that you use a
// /// automatically splits the data into multiple pages. /// [PaginatedDataTable] which automatically splits the data into
// /// /// multiple pages.
// TODO(ianh): Also suggest [ScrollingDataTable] once we have it.
///
/// See also: /// See also:
/// ///
/// * [DataColumn] /// * [DataColumn]
/// * [DataRow] /// * [DataRow]
/// * [DataCell] /// * [DataCell]
/// * [PaginatedDataTable]
/// * <https://www.google.com/design/spec/components/data-tables.html> /// * <https://www.google.com/design/spec/components/data-tables.html>
class DataTable extends StatelessWidget { class DataTable extends StatelessWidget {
/// Creates a widget describing a data table. /// Creates a widget describing a data table.
...@@ -213,7 +229,7 @@ class DataTable extends StatelessWidget { ...@@ -213,7 +229,7 @@ class DataTable extends StatelessWidget {
/// The [columns] argument must be a list of as many [DataColumn] /// The [columns] argument must be a list of as many [DataColumn]
/// objects as the table is to have columns, ignoring the leading /// objects as the table is to have columns, ignoring the leading
/// checkbox column if any. The [columns] argument must have a /// 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 /// The [rows] argument must be a list of as many [DataRow] objects
/// as the table is to have rows, ignoring the leading heading row /// as the table is to have rows, ignoring the leading heading row
...@@ -237,15 +253,16 @@ class DataTable extends StatelessWidget { ...@@ -237,15 +253,16 @@ class DataTable extends StatelessWidget {
List<DataColumn> columns, List<DataColumn> columns,
this.sortColumnIndex, this.sortColumnIndex,
this.sortAscending: true, this.sortAscending: true,
this.onSelectAll,
this.rows this.rows
}) : columns = columns, }) : columns = columns,
_onlyTextColumn = _initOnlyTextColumn(columns), super(key: key) { _onlyTextColumn = _initOnlyTextColumn(columns), super(key: key) {
assert(columns != null); assert(columns != null);
assert(columns.length > 0); assert(columns.length > 0);
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
assert(sortAscending != null); assert(sortAscending != null);
assert(rows != null); assert(rows != null);
assert(!rows.any((DataRow row) => row.cells.length != columns.length)); 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. /// The configuration and labels for the columns in the table.
...@@ -276,6 +293,17 @@ class DataTable extends StatelessWidget { ...@@ -276,6 +293,17 @@ class DataTable extends StatelessWidget {
/// table). /// table).
final bool sortAscending; 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 data to show in each row (excluding the row that contains
/// the column headings). Must be non-null, but may be empty. /// the column headings). Must be non-null, but may be empty.
final List<DataRow> rows; final List<DataRow> rows;
...@@ -304,9 +332,13 @@ class DataTable extends StatelessWidget { ...@@ -304,9 +332,13 @@ class DataTable extends StatelessWidget {
static final LocalKey _headingRowKey = new UniqueKey(); static final LocalKey _headingRowKey = new UniqueKey();
void _handleSelectAll(bool checked) { void _handleSelectAll(bool checked) {
for (DataRow row in rows) { if (onSelectAll != null) {
if ((row.onSelectChanged != null) && (row.selected != checked)) onSelectAll(checked);
row.onSelectChanged(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 { ...@@ -76,7 +76,7 @@ abstract class FlowDelegate {
/// By default, the children will receive the given constraints, which are the /// By default, the children will receive the given constraints, which are the
/// constrains the constraints used to size the container. The children need /// constrains the constraints used to size the container. The children need
/// not respect the given constraints, but they are required to respect the /// 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 /// 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 /// 100.0, but this function might give the children looser constraints that
/// let them be larger or smaller than 100.0 by 100.0. /// let them be larger or smaller than 100.0 by 100.0.
......
...@@ -1353,7 +1353,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -1353,7 +1353,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
} }
if (targetFrame != null && targetFrame < stack.length) { if (targetFrame != null && targetFrame < stack.length) {
information.writeln( 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 ' 'function by the following function, which probably computed the '
'invalid constraints in question:' 'invalid constraints in question:'
); );
......
...@@ -448,7 +448,7 @@ class RenderAspectRatio extends RenderProxyBox { ...@@ -448,7 +448,7 @@ class RenderAspectRatio extends RenderProxyBox {
} }
// Similar to RenderImage, we iteratively attempt to fit within the given // 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 // applying the constraints is also biased towards inferring the height
// from the width. // from the width.
......
...@@ -617,7 +617,7 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox { ...@@ -617,7 +617,7 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
/// ///
/// If non-null, the child is given a tight width constraint that is the max /// 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 /// 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 get widthFactor => _widthFactor;
double _widthFactor; double _widthFactor;
set widthFactor (double value) { set widthFactor (double value) {
...@@ -632,7 +632,7 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox { ...@@ -632,7 +632,7 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
/// ///
/// If non-null, the child is given a tight height constraint that is the max /// 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 /// 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 get heightFactor => _heightFactor;
double _heightFactor; double _heightFactor;
set heightFactor (double value) { set heightFactor (double value) {
......
...@@ -847,12 +847,18 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget { ...@@ -847,12 +847,18 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
/// ///
/// If non-null, the child is given a tight width constraint that is the max /// If non-null, the child is given a tight width constraint that is the max
/// incoming width constraint multipled by this factor. /// incoming width constraint multipled by this factor.
///
/// If null, the incoming width constraints are passed to the child
/// unmodified.
final double widthFactor; final double widthFactor;
/// If non-null, the fraction of the incoming height given to the child. /// 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 /// If non-null, the child is given a tight height constraint that is the max
/// incoming height constraint multipled by this factor. /// incoming height constraint multipled by this factor.
///
/// If null, the incoming height constraints are passed to the child
/// unmodified.
final double heightFactor; final double heightFactor;
/// How to align the child. /// 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