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;
|
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 {
|
class DataTableDemo extends StatefulWidget {
|
||||||
static const String routeName = '/data-table';
|
static const String routeName = '/data-table';
|
||||||
|
|
||||||
@ -27,35 +132,13 @@ class DataTableDemo extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DataTableDemoState extends State<DataTableDemo> {
|
class _DataTableDemoState extends State<DataTableDemo> {
|
||||||
|
|
||||||
int _sortColumnIndex;
|
int _sortColumnIndex;
|
||||||
bool _sortAscending = true;
|
bool _sortAscending = true;
|
||||||
|
DesertDataSource _deserts = new DesertDataSource();
|
||||||
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),
|
|
||||||
];
|
|
||||||
|
|
||||||
void _sort/*<T>*/(Comparable<dynamic/*=T*/> getField(Desert d), int columnIndex, bool ascending) {
|
void _sort/*<T>*/(Comparable<dynamic/*=T*/> getField(Desert d), int columnIndex, bool ascending) {
|
||||||
|
_deserts._sort/*<T>*/(getField, ascending);
|
||||||
setState(() {
|
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;
|
_sortColumnIndex = columnIndex;
|
||||||
_sortAscending = ascending;
|
_sortAscending = ascending;
|
||||||
});
|
});
|
||||||
@ -67,77 +150,61 @@ class _DataTableDemoState extends State<DataTableDemo> {
|
|||||||
appBar: new AppBar(title: new Text('Data tables')),
|
appBar: new AppBar(title: new Text('Data tables')),
|
||||||
body: new Block(
|
body: new Block(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Material(
|
new IntrinsicHeight(
|
||||||
child: new IntrinsicHeight(
|
child: new Block(
|
||||||
child: new Block(
|
padding: const EdgeInsets.all(20.0),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new DataTable(
|
new PaginatedDataTable(
|
||||||
sortColumnIndex: _sortColumnIndex,
|
rowsPerPage: 10,
|
||||||
sortAscending: _sortAscending,
|
sortColumnIndex: _sortColumnIndex,
|
||||||
columns: <DataColumn>[
|
sortAscending: _sortAscending,
|
||||||
new DataColumn(
|
columns: <DataColumn>[
|
||||||
label: new Text('Dessert (100g serving)'),
|
new DataColumn(
|
||||||
onSort: (int columnIndex, bool ascending) => _sort/*<String>*/((Desert d) => d.name, columnIndex, ascending)
|
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'),
|
new DataColumn(
|
||||||
tooltip: 'The total amount of food energy in the given serving size.',
|
label: new Text('Calories'),
|
||||||
numeric: true,
|
tooltip: 'The total amount of food energy in the given serving size.',
|
||||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending)
|
numeric: true,
|
||||||
),
|
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calories, columnIndex, ascending)
|
||||||
new DataColumn(
|
),
|
||||||
label: new Text('Fat (g)'),
|
new DataColumn(
|
||||||
numeric: true,
|
label: new Text('Fat (g)'),
|
||||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending)
|
numeric: true,
|
||||||
),
|
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.fat, columnIndex, ascending)
|
||||||
new DataColumn(
|
),
|
||||||
label: new Text('Carbs (g)'),
|
new DataColumn(
|
||||||
numeric: true,
|
label: new Text('Carbs (g)'),
|
||||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending)
|
numeric: true,
|
||||||
),
|
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.carbs, columnIndex, ascending)
|
||||||
new DataColumn(
|
),
|
||||||
label: new Text('Protein (g)'),
|
new DataColumn(
|
||||||
numeric: true,
|
label: new Text('Protein (g)'),
|
||||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending)
|
numeric: true,
|
||||||
),
|
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.protein, columnIndex, ascending)
|
||||||
new DataColumn(
|
),
|
||||||
label: new Text('Sodium (mg)'),
|
new DataColumn(
|
||||||
numeric: true,
|
label: new Text('Sodium (mg)'),
|
||||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending)
|
numeric: true,
|
||||||
),
|
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.sodium, columnIndex, ascending)
|
||||||
new DataColumn(
|
),
|
||||||
label: new Text('Calcium (%)'),
|
new DataColumn(
|
||||||
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
|
label: new Text('Calcium (%)'),
|
||||||
numeric: true,
|
tooltip: 'The amount of calcium as a percentage of the recommended daily amount.',
|
||||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending)
|
numeric: true,
|
||||||
),
|
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.calcium, columnIndex, ascending)
|
||||||
new DataColumn(
|
),
|
||||||
label: new Text('Iron (%)'),
|
new DataColumn(
|
||||||
numeric: true,
|
label: new Text('Iron (%)'),
|
||||||
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
|
numeric: true,
|
||||||
),
|
onSort: (int columnIndex, bool ascending) => _sort/*<num>*/((Desert d) => d.iron, columnIndex, ascending)
|
||||||
],
|
),
|
||||||
rows: _deserts.map/*<DataRow>*/((Desert desert) {
|
],
|
||||||
return new DataRow(
|
source: _deserts
|
||||||
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)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
@ -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';
|
|||||||
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';
|
|||||||
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 @@
|
|||||||
|
|
||||||
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>();
|
|||||||
/// 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> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
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
|
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
/// 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 {
|
|||||||
}
|
}
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
///
|
///
|
||||||
/// 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.
|
||||||
|
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