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