Commit 68f92d4f authored by Ian Hickson's avatar Ian Hickson

Provide UI to paginate PaginatedDataTable (#4382)

Also:
* Make PaginatedDataTable able to scroll itself horizontally.
* Make drop down buttons support having an explicit text style and icon
  size given.
* Fix a bug with drop-down buttons asserting when opened partly
  off-screen.
* Make sure to pop the drop-down button's route if the drop-down button
  is discarded while the route is up.
* Remove extraneous padding on drop-down buttons. (Couldn't figure out
  why it was there, and it breaks alignment when a drop-down is mixed
  with other text.)
* Some docs improvements.
* Add Route.isActive
* Add a setState() method to ModalRoutes.
parent 5e6baf4a
......@@ -121,7 +121,7 @@ class DesertDataSource extends DataTableSource {
int get rowCount => _deserts.length;
@override
bool get isRowCountApproximate => true;
bool get isRowCountApproximate => false;
}
class DataTableDemo extends StatefulWidget {
......@@ -132,6 +132,7 @@ class DataTableDemo extends StatefulWidget {
}
class _DataTableDemoState extends State<DataTableDemo> {
int _rowsPerPage = PaginatedDataTable.defaultRowsPerPage;
int _sortColumnIndex;
bool _sortAscending = true;
DesertDataSource _deserts = new DesertDataSource();
......@@ -149,63 +150,57 @@ class _DataTableDemoState extends State<DataTableDemo> {
return new Scaffold(
appBar: new AppBar(title: new Text('Data tables')),
body: new Block(
padding: const EdgeInsets.all(20.0),
children: <Widget>[
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
)
]
)
new PaginatedDataTable(
rowsPerPage: _rowsPerPage,
onRowsPerPageChanged: (int value) { setState(() { _rowsPerPage = value; }); },
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
)
]
)
......
......@@ -16,7 +16,6 @@ import 'theme.dart';
import 'material.dart';
const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
const double _kTopMargin = 6.0;
const double _kMenuItemHeight = 48.0;
const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0);
const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 36.0);
......@@ -136,6 +135,7 @@ class _DropDownMenu<T> extends StatusTransitionWidget {
),
child: new Material(
type: MaterialType.transparency,
textStyle: route.style,
child: new ScrollableList(
padding: _kMenuVerticalPadding,
itemExtent: _kMenuItemHeight,
......@@ -182,8 +182,17 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit);
top = bottom - childSize.height;
}
assert(top >= 0.0);
assert(top + childSize.height <= size.height);
assert(() {
final Rect container = Point.origin & size;
if (container.intersect(buttonRect) == buttonRect) {
// If the button was entirely on-screen, then verify
// that the menu is also on-screen.
// If the button was a bit off-screen, then, oh well.
assert(top >= 0.0);
assert(top + childSize.height <= size.height);
}
return true;
});
return new Offset(buttonRect.left, top);
}
......@@ -220,14 +229,28 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> {
this.items,
this.buttonRect,
this.selectedIndex,
this.elevation: 8
}) : super(completer: completer);
this.elevation: 8,
TextStyle style
}) : _style = style, super(completer: completer) {
assert(style != null);
}
final List<DropDownMenuItem<T>> items;
final Rect buttonRect;
final int selectedIndex;
final int elevation;
TextStyle get style => _style;
TextStyle _style;
set style (TextStyle value) {
assert(value != null);
if (_style == value)
return;
setState(() {
_style = value;
});
}
@override
Duration get transitionDuration => _kDropDownMenuDuration;
......@@ -277,13 +300,10 @@ class DropDownMenuItem<T> extends StatelessWidget {
return new Container(
height: _kMenuItemHeight,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: new DefaultTextStyle(
style: Theme.of(context).textTheme.subhead,
child: new Baseline(
baselineType: TextBaseline.alphabetic,
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
child: child
)
child: new Baseline(
baselineType: TextBaseline.alphabetic,
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
child: child
)
);
}
......@@ -332,12 +352,17 @@ class DropDownButton<T> extends StatefulWidget {
/// Creates a drop down button.
///
/// The [items] must have distinct values and [value] must be among them.
///
/// The [elevation] and [iconSize] arguments must not be null (they both have
/// defaults, so do not need to be specified).
DropDownButton({
Key key,
this.items,
this.value,
this.onChanged,
this.elevation: 8
this.elevation: 8,
this.style,
this.iconSize: 36.0
}) : super(key: key) {
assert(items != null);
assert(items.where((DropDownMenuItem<T> item) => item.value == value).length == 1);
......@@ -357,6 +382,18 @@ class DropDownButton<T> extends StatefulWidget {
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
final int elevation;
/// The text style to use for text in the drop down button and the drop down
/// menu that appears when you tap the button.
///
/// Defaults to the [TextTheme.subhead] value of the current
/// [ThemeData.textTheme] of the current [Theme].
final TextStyle style;
/// The size to use for the drop-down button's down arrow icon button.
///
/// Defaults to 36.0.
final double iconSize;
@override
_DropDownButtonState<T> createState() => new _DropDownButtonState<T>();
}
......@@ -388,18 +425,26 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
}
}
TextStyle get _textStyle => config.style ?? Theme.of(context).textTheme.subhead;
_DropDownRoute<T> _currentRoute;
void _handleTap() {
assert(_currentRoute == null);
final RenderBox itemBox = _itemKey.currentContext.findRenderObject();
final Rect itemRect = itemBox.localToGlobal(Point.origin) & itemBox.size;
final Completer<_DropDownRouteResult<T>> completer = new Completer<_DropDownRouteResult<T>>();
Navigator.push(context, new _DropDownRoute<T>(
_currentRoute = new _DropDownRoute<T>(
completer: completer,
items: config.items,
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
selectedIndex: _selectedIndex,
elevation: config.elevation
));
elevation: config.elevation,
style: _textStyle
);
Navigator.push(context, _currentRoute);
completer.future.then((_DropDownRouteResult<T> newValue) {
_currentRoute = null;
if (!mounted || newValue == null)
return;
if (config.onChanged != null)
......@@ -410,28 +455,28 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
Widget result = new Row(
mainAxisAlignment: MainAxisAlignment.collapse,
children: <Widget>[
// We use an IndexedStack to make sure we have enough width to show any
// possible item as the selected item without changing size.
new IndexedStack(
children: config.items,
key: _itemKey,
index: _selectedIndex,
alignment: FractionalOffset.centerLeft
),
new Icon(icon: Icons.arrow_drop_down, size: 36.0)
]
final TextStyle style = _textStyle;
if (_currentRoute != null)
_currentRoute.style = style;
Widget result = new DefaultTextStyle(
style: style,
child: new Row(
mainAxisAlignment: MainAxisAlignment.collapse,
children: <Widget>[
// We use an IndexedStack to make sure we have enough width to show any
// possible item as the selected item without changing size.
new IndexedStack(
key: _itemKey,
index: _selectedIndex,
alignment: FractionalOffset.centerLeft,
children: config.items
),
new Icon(icon: Icons.arrow_drop_down, size: config.iconSize)
]
)
);
if (DropDownButtonHideUnderline.at(context)) {
result = new Padding(
padding: const EdgeInsets.only(top: _kTopMargin, bottom: _kBottomBorderHeight),
child: result
);
} else {
if (!DropDownButtonHideUnderline.at(context)) {
result = new Container(
padding: const EdgeInsets.only(top: _kTopMargin),
decoration: const BoxDecoration(border: _kDropDownUnderline),
child: result
);
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'debug.dart';
import 'icon.dart';
......@@ -35,12 +36,18 @@ class IconButton extends StatelessWidget {
/// be used in many other places as well.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// The [size], [padding], and [alignment] arguments must not be null (though
/// they each have default values).
///
/// The [icon] argument must be specified. See [Icons] for a list of icons to
/// use for this argument.
const IconButton({
Key key,
this.size: 24.0,
this.padding: const EdgeInsets.all(8.0),
this.alignment: FractionalOffset.center,
this.icon,
@required this.icon,
this.color,
this.disabledColor,
this.onPressed,
......@@ -48,16 +55,24 @@ class IconButton extends StatelessWidget {
}) : super(key: key);
/// The size of the icon inside the button.
///
/// This property must not be null. It defaults to 24.0.
final double size;
/// The padding around the button's icon. The entire padded icon will react
/// to input gestures.
///
/// This property must not be null. It defaults to 8.0 padding on all sides.
final EdgeInsets padding;
/// Defines how the icon is positioned within the IconButton.
///
/// This property must not be null. It defaults to [FractionalOffset.center].
final FractionalOffset alignment;
/// The icon to display inside the button.
/// The icon to display inside the button, from the list in [Icons].
///
/// This property must not be null.
final IconData icon;
/// The color to use for the icon inside the button, if the icon is enabled.
......
......@@ -2,12 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'card.dart';
import 'data_table.dart';
import 'data_table_source.dart';
import 'drop_down.dart';
import 'icon_button.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'icons.dart';
import 'progress_indicator.dart';
import 'theme.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].
......@@ -33,6 +42,9 @@ class PaginatedDataTable extends StatefulWidget {
/// [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.
///
/// The [rowsPerPage] and [availableRowsPerPage] must not be null (though they
/// both have defaults, so don't have to be specified).
PaginatedDataTable({
Key key,
this.columns,
......@@ -41,7 +53,8 @@ class PaginatedDataTable extends StatefulWidget {
this.onSelectAll,
this.initialFirstRowIndex: 0,
this.onPageChanged,
this.rowsPerPage: 10,
this.rowsPerPage: defaultRowsPerPage,
this.availableRowsPerPage: const <int>[defaultRowsPerPage, defaultRowsPerPage * 2, defaultRowsPerPage * 5, defaultRowsPerPage * 10],
this.onRowsPerPageChanged,
this.source
}) : super(key: key) {
......@@ -49,6 +62,10 @@ class PaginatedDataTable extends StatefulWidget {
assert(columns.length > 0);
assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length));
assert(sortAscending != null);
assert(rowsPerPage != null);
assert(rowsPerPage > 0);
assert(availableRowsPerPage != null);
assert(availableRowsPerPage.contains(rowsPerPage));
assert(source != null);
}
......@@ -76,13 +93,31 @@ class PaginatedDataTable extends StatefulWidget {
final int initialFirstRowIndex;
/// Invoked when the user switches to another page.
///
/// The value is the index of the first row on the currently displayed page.
final ValueChanged<int> onPageChanged;
/// The number of rows to show on each page.
///
/// See also [onRowsPerPageChanged].
/// See also:
///
/// * [onRowsPerPageChanged]
/// * [defaultRowsPerPage]
final int rowsPerPage;
/// The default value for [rowsPerPage].
///
/// Useful when initializing the field that will hold the current
/// [rowsPerPage], when implemented [onRowsPerPageChanged].
static const int defaultRowsPerPage = 10;
/// The options to offer for the rowsPerPage.
///
/// The current [rowsPerPage] must be a value in this list.
///
/// The values in this list should be sorted in ascending order.
final List<int> availableRowsPerPage;
/// 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
......@@ -142,11 +177,15 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
}
/// Ensures that the given row is visible.
void pageTo(double rowIndex) {
void pageTo(int rowIndex) {
final int oldFirstRowIndex = _firstRowIndex;
setState(() {
final int rowsPerPage = config.rowsPerPage;
_firstRowIndex = (rowIndex ~/ rowsPerPage) * rowsPerPage;
});
if ((config.onPageChanged != null) &&
(oldFirstRowIndex != _firstRowIndex))
config.onPageChanged(_firstRowIndex);
}
DataRow _getBlankRowFor(int index) {
......@@ -198,40 +237,97 @@ class PaginatedDataTableState extends State<PaginatedDataTable> {
@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
);
final TextStyle textStyle = Theme.of(context).textTheme.caption;
final List<Widget> footerWidgets = <Widget>[];
if (config.onRowsPerPageChanged != null) {
List<Widget> availableRowsPerPage = config.availableRowsPerPage
.where((int value) => value <= _rowCount)
.map/*<DropDownMenuItem<int>>*/((int value) {
return new DropDownMenuItem<int>(
value: value,
child: new Text('$value')
);
})
.toList();
footerWidgets.addAll(<Widget>[
new Text('Rows per page:'),
new DropDownButtonHideUnderline(
child: new DropDownButton<int>(
items: availableRowsPerPage,
value: config.rowsPerPage,
onChanged: config.onRowsPerPageChanged,
style: textStyle,
iconSize: 24.0
)
),
]);
}
footerWidgets.addAll(<Widget>[
new Container(width: 32.0),
new Text(
'${_firstRowIndex + 1}\u2013${_firstRowIndex + config.rowsPerPage} ${ _rowCountApproximate ? "of about" : "of" } $_rowCount'
),
new Container(width: 32.0),
new IconButton(
padding: EdgeInsets.zero,
icon: Icons.chevron_left,
onPressed: _firstRowIndex <= 0 ? null : () {
pageTo(math.max(_firstRowIndex - config.rowsPerPage, 0));
}
),
new Container(width: 24.0),
new IconButton(
padding: EdgeInsets.zero,
icon: Icons.chevron_right,
onPressed: (!_rowCountApproximate && (_firstRowIndex + config.rowsPerPage >= _rowCount)) ? null : () {
pageTo(_firstRowIndex + config.rowsPerPage);
}
),
new Container(width: 14.0),
]);
return new Card(
// TODO(ianh): data table card headers
child: table
// TODO(ianh): data table card footers: prev/next page, rows per page, etc
/*
- title, 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
*/
child: new BlockBody(
children: <Widget>[
new ScrollableViewport(
scrollDirection: Axis.horizontal,
child: new DataTable(
key: _tableKey,
columns: config.columns,
sortColumnIndex: config.sortColumnIndex,
sortAscending: config.sortAscending,
onSelectAll: config.onSelectAll,
rows: _getRows(_firstRowIndex, config.rowsPerPage)
)
),
new DefaultTextStyle(
style: textStyle,
child: new IconTheme(
data: new IconThemeData(
opacity: 0.54
),
child: new Container(
height: 56.0,
child: new Row(
mainAxisAlignment: MainAxisAlignment.end,
children: footerWidgets
)
)
)
)
]
)
);
}
}
/*
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
*/
......@@ -400,7 +400,9 @@ abstract class State<T extends StatefulWidget> {
/// Whenever you need to change internal state for a State object, make the
/// change in a function that you pass to setState(), as in:
///
/// setState(() { myState = newValue });
/// ```dart
/// setState(() { myState = newValue });
/// ```
///
/// If you just change the state directly without calling setState(), then the
/// widget will not be scheduled for rebuilding, meaning that its rendering
......
......@@ -71,12 +71,29 @@ abstract class Route<T> {
void dispose() { }
/// Whether this route is the top-most route on the navigator.
///
/// If this is true, then [isActive] is also true.
bool get isCurrent {
if (_navigator == null)
return false;
assert(_navigator._history.contains(this));
return _navigator._history.last == this;
}
/// Whether this route is on the navigator.
///
/// If the route is not only active, but also the current route (the top-most
/// route), then [isCurrent] will also be true.
///
/// If a later route is entirely opaque, then the route will be active but not
/// rendered. In particular, it's possible for a route to be active but for
/// stateful widgets within the route to not be instantiated.
bool get isActive {
if (_navigator == null)
return false;
assert(_navigator._history.contains(this));
return true;
}
}
/// Data that might be useful in constructing a [Route].
......
......@@ -4,6 +4,8 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'basic.dart';
import 'focus.dart';
import 'framework.dart';
......@@ -400,11 +402,8 @@ class _ModalScopeState extends State<_ModalScope> {
});
}
void _didChangeRouteOffStage() {
setState(() {
// We use the route's offstage bool in our build function, which means our
// state has changed.
});
void _routeSetState(VoidCallback fn) {
setState(fn);
}
@override
......@@ -466,6 +465,28 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return widget?.route;
}
/// Whenever you need to change internal state for a ModalRoute object, make
/// the change in a function that you pass to setState(), as in:
///
/// ```dart
/// setState(() { myState = newValue });
/// ```
///
/// If you just change the state directly without calling setState(), then the
/// route will not be scheduled for rebuilding, meaning that its rendering
/// will not be updated.
@protected
void setState(VoidCallback fn) {
if (_scopeKey.currentState != null) {
_scopeKey.currentState._routeSetState(fn);
} else {
// The route isn't currently visible, so we don't have to call its setState
// method, but we do still need to call the fn callback, otherwise the state
// in the route won't be updated!
fn();
}
}
// The API for subclasses to override - used by _ModalScope
......@@ -528,8 +549,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
set offstage (bool value) {
if (_offstage == value)
return;
_offstage = value;
_scopeKey.currentState?._didChangeRouteOffStage();
setState(() {
_offstage = value;
});
}
/// The build context for the subtree containing the primary content of this route.
......
......@@ -721,10 +721,11 @@ class ScrollNotification extends Notification {
///
/// See also:
///
/// * [ScrollableList]
/// * [PageableList]
/// * [ScrollableGrid]
/// * [LazyBlock]
/// * [ScrollableList], if you have many identically-sized children.
/// * [PageableList], if you have children that each take the entire screen.
/// * [ScrollableGrid], if your children are in a grid pattern.
/// * [LazyBlock], if you have many children of varying sizes.
/// * [Block], if your single child is a [BlockBody] or a [Column].
class ScrollableViewport extends StatelessWidget {
/// Creates a simple scrolling widget that has a single child.
///
......@@ -849,14 +850,18 @@ class ScrollableViewport extends StatelessWidget {
/// arrange in a block layout and that might exceed the height of its container
/// (and therefore need to scroll).
///
/// If you have a large number of children, consider using [LazyBlock] (if the
/// children have variable height) or [ScrollableList] (if the children all have
/// the same fixed height).
/// If you have a large number of children, or if you always expect this to need
/// to scroll, consider using [LazyBlock] (if the children have variable height)
/// or [ScrollableList] (if the children all have the same fixed height), as
/// they avoid doing work for children that are not visible.
///
/// If you have a single child, then use [ScrollableViewport] directly.
///
/// See also:
///
/// * [ScrollableList]
/// * [LazyBlock]
/// * [ScrollableViewport], if you only have one child
/// * [ScrollableList], if all your children are the same height
/// * [LazyBlock], if you have children with varying heights
class Block extends StatelessWidget {
/// Creates a scrollable array of children.
Block({
......
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