1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'card_theme.dart';
6/// @docImport 'data_table_theme.dart';
7/// @docImport 'text_button.dart';
8library;
9
10import 'dart:math' as math;
11
12import 'package:flutter/gestures.dart' show DragStartBehavior;
13import 'package:flutter/widgets.dart';
14
15import 'card.dart';
16import 'constants.dart';
17import 'data_table.dart';
18import 'data_table_source.dart';
19import 'debug.dart';
20import 'dropdown.dart';
21import 'icon_button.dart';
22import 'icons.dart';
23import 'ink_decoration.dart';
24import 'material_localizations.dart';
25import 'material_state.dart';
26import 'progress_indicator.dart';
27import 'theme.dart';
28
29/// A table that follows the
30/// [Material 2](https://material.io/go/design-data-tables)
31/// design specification, using multiple pages to display data.
32///
33/// A paginated data table shows [rowsPerPage] rows of data per page and
34/// provides controls for showing other pages.
35///
36/// Data is read lazily from a [DataTableSource]. The widget is presented
37/// as a [Card].
38///
39/// If the [key] is a [PageStorageKey], the [initialFirstRowIndex] is persisted
40/// to [PageStorage].
41///
42/// {@tool dartpad}
43///
44/// This sample shows how to display a [DataTable] with three columns: name,
45/// age, and role. The columns are defined by three [DataColumn] objects. The
46/// table contains three rows of data for three example users, the data for
47/// which is defined by three [DataRow] objects.
48///
49/// ** See code in examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart **
50/// {@end-tool}
51///
52/// {@tool dartpad}
53///
54/// This example shows how paginated data tables can supported sorted data.
55///
56/// ** See code in examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart **
57/// {@end-tool}
58///
59/// See also:
60///
61/// * [DataTable], which is not paginated.
62/// * `TableView` from the
63/// [two_dimensional_scrollables](https://pub.dev/packages/two_dimensional_scrollables)
64/// package, for displaying large amounts of data without pagination.
65/// * <https://material.io/go/design-data-tables>
66class PaginatedDataTable extends StatefulWidget {
67 /// Creates a widget describing a paginated [DataTable] on a [Card].
68 ///
69 /// The [header] should give the card's header, typically a [Text] widget.
70 ///
71 /// The [columns] argument must be a list of as many [DataColumn] objects as
72 /// the table is to have columns, ignoring the leading checkbox column if any.
73 /// The [columns] argument must have a length greater than zero and cannot be
74 /// null.
75 ///
76 /// If the table is sorted, the column that provides the current primary key
77 /// should be specified by index in [sortColumnIndex], 0 meaning the first
78 /// column in [columns], 1 being the next one, and so forth.
79 ///
80 /// The actual sort order can be specified using [sortAscending]; if the sort
81 /// order is ascending, this should be true (the default), otherwise it should
82 /// be false.
83 ///
84 /// The [source] should be a long-lived [DataTableSource]. The same source
85 /// should be provided each time a particular [PaginatedDataTable] widget is
86 /// created; avoid creating a new [DataTableSource] with each new instance of
87 /// the [PaginatedDataTable] widget unless the data table really is to now
88 /// show entirely different data from a new source.
89 ///
90 /// Themed by [DataTableTheme]. [DataTableThemeData.decoration] is ignored.
91 /// To modify the border or background color of the [PaginatedDataTable], use
92 /// [CardTheme], since a [Card] wraps the inner [DataTable].
93 PaginatedDataTable({
94 super.key,
95 this.header,
96 this.actions,
97 required this.columns,
98 this.sortColumnIndex,
99 this.sortAscending = true,
100 this.onSelectAll,
101 @Deprecated(
102 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
103 'This feature was deprecated after v3.7.0-5.0.pre.',
104 )
105 double? dataRowHeight,
106 double? dataRowMinHeight,
107 double? dataRowMaxHeight,
108 this.headingRowHeight = 56.0,
109 this.horizontalMargin = 24.0,
110 this.columnSpacing = 56.0,
111 this.showCheckboxColumn = true,
112 this.showFirstLastButtons = false,
113 this.initialFirstRowIndex = 0,
114 this.onPageChanged,
115 this.rowsPerPage = defaultRowsPerPage,
116 this.availableRowsPerPage = const <int>[
117 defaultRowsPerPage,
118 defaultRowsPerPage * 2,
119 defaultRowsPerPage * 5,
120 defaultRowsPerPage * 10,
121 ],
122 this.onRowsPerPageChanged,
123 this.dragStartBehavior = DragStartBehavior.start,
124 this.arrowHeadColor,
125 required this.source,
126 this.checkboxHorizontalMargin,
127 this.controller,
128 this.primary,
129 this.headingRowColor,
130 this.dividerThickness,
131 this.showEmptyRows = true,
132 }) : assert(actions == null || (header != null)),
133 assert(columns.isNotEmpty),
134 assert(
135 sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length),
136 ),
137 assert(
138 dataRowMinHeight == null ||
139 dataRowMaxHeight == null ||
140 dataRowMaxHeight >= dataRowMinHeight,
141 ),
142 assert(
143 dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null),
144 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.',
145 ),
146 dataRowMinHeight = dataRowHeight ?? dataRowMinHeight,
147 dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight,
148 assert(rowsPerPage > 0),
149 assert(dividerThickness == null || dividerThickness >= 0),
150 assert(() {
151 if (onRowsPerPageChanged != null) {
152 assert(availableRowsPerPage.contains(rowsPerPage));
153 }
154 return true;
155 }()),
156 assert(
157 !(controller != null && (primary ?? false)),
158 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
159 'You cannot both set primary to true and pass an explicit controller.',
160 );
161
162 /// The table card's optional header.
163 ///
164 /// This is typically a [Text] widget, but can also be a [Row] of
165 /// [TextButton]s. To show icon buttons at the top end side of the table with
166 /// a header, set the [actions] property.
167 ///
168 /// If items in the table are selectable, then, when the selection is not
169 /// empty, the header is replaced by a count of the selected items. The
170 /// [actions] are still visible when items are selected.
171 final Widget? header;
172
173 /// Icon buttons to show at the top end side of the table. The [header] must
174 /// not be null to show the actions.
175 ///
176 /// Typically, the exact actions included in this list will vary based on
177 /// whether any rows are selected or not.
178 ///
179 /// These should be size 24.0 with default padding (8.0).
180 final List<Widget>? actions;
181
182 /// The configuration and labels for the columns in the table.
183 final List<DataColumn> columns;
184
185 /// The current primary sort key's column.
186 ///
187 /// See [DataTable.sortColumnIndex] for details.
188 ///
189 /// The direction of the sort is specified using [sortAscending].
190 final int? sortColumnIndex;
191
192 /// Whether the column mentioned in [sortColumnIndex], if any, is sorted
193 /// in ascending order.
194 ///
195 /// See [DataTable.sortAscending] for details.
196 final bool sortAscending;
197
198 /// Invoked when the user selects or unselects every row, using the
199 /// checkbox in the heading row.
200 ///
201 /// See [DataTable.onSelectAll].
202 final ValueSetter<bool?>? onSelectAll;
203
204 /// The height of each row (excluding the row that contains column headings).
205 ///
206 /// This value is optional and defaults to kMinInteractiveDimension if not
207 /// specified.
208 @Deprecated(
209 'Migrate to use dataRowMinHeight and dataRowMaxHeight instead. '
210 'This feature was deprecated after v3.7.0-5.0.pre.',
211 )
212 double? get dataRowHeight => dataRowMinHeight == dataRowMaxHeight ? dataRowMinHeight : null;
213
214 /// The minimum height of each row (excluding the row that contains column headings).
215 ///
216 /// This value is optional and defaults to [kMinInteractiveDimension] if not
217 /// specified.
218 final double? dataRowMinHeight;
219
220 /// The maximum height of each row (excluding the row that contains column headings).
221 ///
222 /// This value is optional and defaults to [kMinInteractiveDimension] if not
223 /// specified.
224 final double? dataRowMaxHeight;
225
226 /// The height of the heading row.
227 ///
228 /// This value is optional and defaults to 56.0 if not specified.
229 final double headingRowHeight;
230
231 /// The horizontal margin between the edges of the table and the content
232 /// in the first and last cells of each row.
233 ///
234 /// When a checkbox is displayed, it is also the margin between the checkbox
235 /// the content in the first data column.
236 ///
237 /// This value defaults to 24.0 to adhere to the Material Design specifications.
238 ///
239 /// If [checkboxHorizontalMargin] is null, then [horizontalMargin] is also the
240 /// margin between the edge of the table and the checkbox, as well as the
241 /// margin between the checkbox and the content in the first data column.
242 final double horizontalMargin;
243
244 /// The horizontal margin between the contents of each data column.
245 ///
246 /// This value defaults to 56.0 to adhere to the Material Design specifications.
247 final double columnSpacing;
248
249 /// {@macro flutter.material.dataTable.showCheckboxColumn}
250 final bool showCheckboxColumn;
251
252 /// Flag to display the pagination buttons to go to the first and last pages.
253 final bool showFirstLastButtons;
254
255 /// The index of the first row to display when the widget is first created.
256 final int? initialFirstRowIndex;
257
258 /// {@macro flutter.material.dataTable.dividerThickness}
259 ///
260 /// If null, [DataTableThemeData.dividerThickness] is used. This value
261 /// defaults to 1.0.
262 final double? dividerThickness;
263
264 /// Invoked when the user switches to another page.
265 ///
266 /// The value is the index of the first row on the currently displayed page.
267 final ValueChanged<int>? onPageChanged;
268
269 /// The number of rows to show on each page.
270 ///
271 /// See also:
272 ///
273 /// * [onRowsPerPageChanged]
274 /// * [defaultRowsPerPage]
275 final int rowsPerPage;
276
277 /// The default value for [rowsPerPage].
278 ///
279 /// Useful when initializing the field that will hold the current
280 /// [rowsPerPage], when implemented [onRowsPerPageChanged].
281 static const int defaultRowsPerPage = 10;
282
283 /// The options to offer for the rowsPerPage.
284 ///
285 /// The current [rowsPerPage] must be a value in this list.
286 ///
287 /// The values in this list should be sorted in ascending order.
288 final List<int> availableRowsPerPage;
289
290 /// Invoked when the user selects a different number of rows per page.
291 ///
292 /// If this is null, then the value given by [rowsPerPage] will be used
293 /// and no affordance will be provided to change the value.
294 final ValueChanged<int?>? onRowsPerPageChanged;
295
296 /// The data source which provides data to show in each row.
297 ///
298 /// This object should generally have a lifetime longer than the
299 /// [PaginatedDataTable] widget itself; it should be reused each time the
300 /// [PaginatedDataTable] constructor is called.
301 final DataTableSource source;
302
303 /// {@macro flutter.widgets.scrollable.dragStartBehavior}
304 final DragStartBehavior dragStartBehavior;
305
306 /// Horizontal margin around the checkbox, if it is displayed.
307 ///
308 /// If null, then [horizontalMargin] is used as the margin between the edge
309 /// of the table and the checkbox, as well as the margin between the checkbox
310 /// and the content in the first data column. This value defaults to 24.0.
311 final double? checkboxHorizontalMargin;
312
313 /// Defines the color of the arrow heads in the footer.
314 final Color? arrowHeadColor;
315
316 /// {@macro flutter.widgets.scroll_view.controller}
317 final ScrollController? controller;
318
319 /// {@macro flutter.widgets.scroll_view.primary}
320 final bool? primary;
321
322 /// {@macro flutter.material.dataTable.headingRowColor}
323 final MaterialStateProperty<Color?>? headingRowColor;
324
325 /// Controls the visibility of empty rows on the last page of a
326 /// [PaginatedDataTable].
327 ///
328 /// Defaults to `true`, which means empty rows will be populated on the
329 /// last page of the table if there is not enough content.
330 /// When set to `false`, empty rows will not be created.
331 final bool showEmptyRows;
332
333 @override
334 PaginatedDataTableState createState() => PaginatedDataTableState();
335}
336
337/// Holds the state of a [PaginatedDataTable].
338///
339/// The table can be programmatically paged using the [pageTo] method.
340class PaginatedDataTableState extends State<PaginatedDataTable> {
341 late int _firstRowIndex;
342 late int _rowCount;
343 late bool _rowCountApproximate;
344 int _selectedRowCount = 0;
345 final Map<int, DataRow?> _rows = <int, DataRow?>{};
346
347 @protected
348 @override
349 void initState() {
350 super.initState();
351 _firstRowIndex =
352 PageStorage.maybeOf(context)?.readState(context) as int? ??
353 widget.initialFirstRowIndex ??
354 0;
355 widget.source.addListener(_handleDataSourceChanged);
356 _handleDataSourceChanged();
357 }
358
359 @protected
360 @override
361 void didUpdateWidget(PaginatedDataTable oldWidget) {
362 super.didUpdateWidget(oldWidget);
363 if (oldWidget.source != widget.source) {
364 oldWidget.source.removeListener(_handleDataSourceChanged);
365 widget.source.addListener(_handleDataSourceChanged);
366 _updateCaches();
367 }
368 }
369
370 @protected
371 @override
372 void reassemble() {
373 super.reassemble();
374 // This function is called during hot reload.
375 //
376 // Normally, if the data source changes, it would notify its listeners and
377 // thus trigger _handleDataSourceChanged(), which clears the row cache and
378 // causes the widget to rebuild.
379 //
380 // During a hot reload, though, a data source can change in ways that will
381 // invalidate the row cache (e.g. adding or removing columns) without ever
382 // triggering a notification, leaving the PaginatedDataTable in an invalid
383 // state. This method handles this case by clearing the cache any time the
384 // widget is involved in a hot reload.
385 _updateCaches();
386 }
387
388 @protected
389 @override
390 void dispose() {
391 widget.source.removeListener(_handleDataSourceChanged);
392 super.dispose();
393 }
394
395 void _handleDataSourceChanged() {
396 setState(_updateCaches);
397 }
398
399 void _updateCaches() {
400 _rowCount = widget.source.rowCount;
401 _rowCountApproximate = widget.source.isRowCountApproximate;
402 _selectedRowCount = widget.source.selectedRowCount;
403 _rows.clear();
404 }
405
406 /// Ensures that the given row is visible.
407 void pageTo(int rowIndex) {
408 final int oldFirstRowIndex = _firstRowIndex;
409 setState(() {
410 final int rowsPerPage = widget.rowsPerPage;
411 _firstRowIndex = (rowIndex ~/ rowsPerPage) * rowsPerPage;
412 });
413 if ((widget.onPageChanged != null) && (oldFirstRowIndex != _firstRowIndex)) {
414 widget.onPageChanged!(_firstRowIndex);
415 }
416 }
417
418 DataRow _getBlankRowFor(int index) {
419 return DataRow.byIndex(
420 index: index,
421 cells: widget.columns.map<DataCell>((DataColumn column) => DataCell.empty).toList(),
422 );
423 }
424
425 DataRow _getProgressIndicatorRowFor(int index) {
426 bool haveProgressIndicator = false;
427 final List<DataCell> cells =
428 widget.columns.map<DataCell>((DataColumn column) {
429 if (!column.numeric) {
430 haveProgressIndicator = true;
431 return const DataCell(CircularProgressIndicator());
432 }
433 return DataCell.empty;
434 }).toList();
435 if (!haveProgressIndicator) {
436 haveProgressIndicator = true;
437 cells[0] = const DataCell(CircularProgressIndicator());
438 }
439 return DataRow.byIndex(index: index, cells: cells);
440 }
441
442 List<DataRow> _getRows(int firstRowIndex, int rowsPerPage) {
443 final List<DataRow> result = <DataRow>[];
444 final int nextPageFirstRowIndex = firstRowIndex + rowsPerPage;
445 bool haveProgressIndicator = false;
446 for (int index = firstRowIndex; index < nextPageFirstRowIndex; index += 1) {
447 DataRow? row;
448 if (index < _rowCount || _rowCountApproximate) {
449 row = _rows.putIfAbsent(index, () => widget.source.getRow(index));
450 if (row == null && !haveProgressIndicator) {
451 row ??= _getProgressIndicatorRowFor(index);
452 haveProgressIndicator = true;
453 }
454 }
455
456 if (widget.showEmptyRows) {
457 row ??= _getBlankRowFor(index);
458 }
459
460 if (row != null) {
461 result.add(row);
462 }
463 }
464 return result;
465 }
466
467 void _handleFirst() {
468 pageTo(0);
469 }
470
471 void _handlePrevious() {
472 pageTo(math.max(_firstRowIndex - widget.rowsPerPage, 0));
473 }
474
475 void _handleNext() {
476 pageTo(_firstRowIndex + widget.rowsPerPage);
477 }
478
479 void _handleLast() {
480 pageTo(((_rowCount - 1) / widget.rowsPerPage).floor() * widget.rowsPerPage);
481 }
482
483 bool _isNextPageUnavailable() =>
484 !_rowCountApproximate && (_firstRowIndex + widget.rowsPerPage >= _rowCount);
485
486 final GlobalKey _tableKey = GlobalKey();
487
488 @protected
489 @override
490 Widget build(BuildContext context) {
491 // TODO(ianh): This whole build function doesn't handle RTL yet.
492 assert(debugCheckHasMaterialLocalizations(context));
493 final ThemeData themeData = Theme.of(context);
494 final MaterialLocalizations localizations = MaterialLocalizations.of(context);
495 // HEADER
496 final List<Widget> headerWidgets = <Widget>[];
497 if (_selectedRowCount == 0 && widget.header != null) {
498 headerWidgets.add(Expanded(child: widget.header!));
499 } else if (widget.header != null) {
500 headerWidgets.add(
501 Expanded(child: Text(localizations.selectedRowCountTitle(_selectedRowCount))),
502 );
503 }
504 if (widget.actions != null) {
505 headerWidgets.addAll(
506 widget.actions!.map<Widget>((Widget action) {
507 return Padding(
508 // 8.0 is the default padding of an icon button
509 padding: const EdgeInsetsDirectional.only(start: 24.0 - 8.0 * 2.0),
510 child: action,
511 );
512 }).toList(),
513 );
514 }
515
516 // FOOTER
517 final TextStyle? footerTextStyle = themeData.textTheme.bodySmall;
518 final List<Widget> footerWidgets = <Widget>[];
519 if (widget.onRowsPerPageChanged != null) {
520 final List<Widget> availableRowsPerPage =
521 widget.availableRowsPerPage
522 .where((int value) => value <= _rowCount || value == widget.rowsPerPage)
523 .map<DropdownMenuItem<int>>((int value) {
524 return DropdownMenuItem<int>(value: value, child: Text('$value'));
525 })
526 .toList();
527 footerWidgets.addAll(<Widget>[
528 // Match trailing padding, in case we overflow and end up scrolling.
529 const SizedBox(width: 14.0),
530 Text(localizations.rowsPerPageTitle),
531 ConstrainedBox(
532 constraints: const BoxConstraints(minWidth: 64.0), // 40.0 for the text, 24.0 for the icon
533 child: Align(
534 alignment: AlignmentDirectional.centerEnd,
535 child: DropdownButtonHideUnderline(
536 child: DropdownButton<int>(
537 items: availableRowsPerPage.cast<DropdownMenuItem<int>>(),
538 value: widget.rowsPerPage,
539 onChanged: widget.onRowsPerPageChanged,
540 style: footerTextStyle,
541 ),
542 ),
543 ),
544 ),
545 ]);
546 }
547 footerWidgets.addAll(<Widget>[
548 const SizedBox(width: 32.0),
549 Text(
550 localizations.pageRowsInfoTitle(
551 _firstRowIndex + 1,
552 math.min(_firstRowIndex + widget.rowsPerPage, _rowCount),
553 _rowCount,
554 _rowCountApproximate,
555 ),
556 ),
557 const SizedBox(width: 32.0),
558 if (widget.showFirstLastButtons)
559 IconButton(
560 icon: const Icon(Icons.skip_previous),
561 padding: EdgeInsets.zero,
562 color: widget.arrowHeadColor,
563 tooltip: localizations.firstPageTooltip,
564 onPressed: _firstRowIndex <= 0 ? null : _handleFirst,
565 ),
566 IconButton(
567 icon: const Icon(Icons.chevron_left),
568 padding: EdgeInsets.zero,
569 color: widget.arrowHeadColor,
570 tooltip: localizations.previousPageTooltip,
571 onPressed: _firstRowIndex <= 0 ? null : _handlePrevious,
572 ),
573 const SizedBox(width: 24.0),
574 IconButton(
575 icon: const Icon(Icons.chevron_right),
576 padding: EdgeInsets.zero,
577 color: widget.arrowHeadColor,
578 tooltip: localizations.nextPageTooltip,
579 onPressed: _isNextPageUnavailable() ? null : _handleNext,
580 ),
581 if (widget.showFirstLastButtons)
582 IconButton(
583 icon: const Icon(Icons.skip_next),
584 padding: EdgeInsets.zero,
585 color: widget.arrowHeadColor,
586 tooltip: localizations.lastPageTooltip,
587 onPressed: _isNextPageUnavailable() ? null : _handleLast,
588 ),
589 const SizedBox(width: 14.0),
590 ]);
591
592 // CARD
593 return Card(
594 semanticContainer: false,
595 child: LayoutBuilder(
596 builder: (BuildContext context, BoxConstraints constraints) {
597 return Column(
598 crossAxisAlignment: CrossAxisAlignment.stretch,
599 children: <Widget>[
600 if (headerWidgets.isNotEmpty)
601 Semantics(
602 container: true,
603 child: DefaultTextStyle(
604 // These typographic styles aren't quite the regular ones. We pick the closest ones from the regular
605 // list and then tweak them appropriately.
606 // See https://material.io/design/components/data-tables.html#tables-within-cards
607 style:
608 _selectedRowCount > 0
609 ? themeData.textTheme.titleMedium!.copyWith(
610 color: themeData.colorScheme.secondary,
611 )
612 : themeData.textTheme.titleLarge!.copyWith(fontWeight: FontWeight.w400),
613 child: IconTheme.merge(
614 data: const IconThemeData(opacity: 0.54),
615 child: Ink(
616 height: 64.0,
617 color: _selectedRowCount > 0 ? themeData.secondaryHeaderColor : null,
618 child: Padding(
619 padding: const EdgeInsetsDirectional.only(start: 24, end: 14.0),
620 child: Row(
621 mainAxisAlignment: MainAxisAlignment.end,
622 children: headerWidgets,
623 ),
624 ),
625 ),
626 ),
627 ),
628 ),
629 SingleChildScrollView(
630 scrollDirection: Axis.horizontal,
631 primary: widget.primary,
632 controller: widget.controller,
633 dragStartBehavior: widget.dragStartBehavior,
634 child: ConstrainedBox(
635 constraints: BoxConstraints(minWidth: constraints.minWidth),
636 child: DataTable(
637 key: _tableKey,
638 columns: widget.columns,
639 sortColumnIndex: widget.sortColumnIndex,
640 sortAscending: widget.sortAscending,
641 onSelectAll: widget.onSelectAll,
642 dividerThickness: widget.dividerThickness,
643 // Make sure no decoration is set on the DataTable
644 // from the theme, as its already wrapped in a Card.
645 decoration: const BoxDecoration(),
646 dataRowMinHeight: widget.dataRowMinHeight,
647 dataRowMaxHeight: widget.dataRowMaxHeight,
648 headingRowHeight: widget.headingRowHeight,
649 horizontalMargin: widget.horizontalMargin,
650 checkboxHorizontalMargin: widget.checkboxHorizontalMargin,
651 columnSpacing: widget.columnSpacing,
652 showCheckboxColumn: widget.showCheckboxColumn,
653 showBottomBorder: true,
654 rows: _getRows(_firstRowIndex, widget.rowsPerPage),
655 headingRowColor: widget.headingRowColor,
656 ),
657 ),
658 ),
659 if (!widget.showEmptyRows)
660 SizedBox(
661 height:
662 (widget.dataRowMaxHeight ?? kMinInteractiveDimension) *
663 (widget.rowsPerPage - _rowCount + _firstRowIndex).clamp(
664 0,
665 widget.rowsPerPage,
666 ),
667 ),
668 DefaultTextStyle(
669 style: footerTextStyle!,
670 child: IconTheme.merge(
671 data: const IconThemeData(opacity: 0.54),
672 child: SizedBox(
673 // TODO(bkonyi): this won't handle text zoom correctly,
674 // https://github.com/flutter/flutter/issues/48522
675 height: 56.0,
676 child: SingleChildScrollView(
677 dragStartBehavior: widget.dragStartBehavior,
678 scrollDirection: Axis.horizontal,
679 reverse: true,
680 child: Row(children: footerWidgets),
681 ),
682 ),
683 ),
684 ),
685 ],
686 );
687 },
688 ),
689 );
690 }
691}
692

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com