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'; |
8 | library; |
9 | |
10 | import 'dart:math' as math; |
11 | |
12 | import 'package:flutter/gestures.dart' show DragStartBehavior; |
13 | import 'package:flutter/widgets.dart'; |
14 | |
15 | import 'card.dart'; |
16 | import 'constants.dart'; |
17 | import 'data_table.dart'; |
18 | import 'data_table_source.dart'; |
19 | import 'debug.dart'; |
20 | import 'dropdown.dart'; |
21 | import 'icon_button.dart'; |
22 | import 'icons.dart'; |
23 | import 'ink_decoration.dart'; |
24 | import 'material_localizations.dart'; |
25 | import 'material_state.dart'; |
26 | import 'progress_indicator.dart'; |
27 | import '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> |
66 | class 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. |
340 | class 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 | |