Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sorting functionality to flutter tables. #1738

Merged
merged 6 commits into from
Mar 16, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/devtools_app/lib/src/flutter/common_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

import '../ui/flutter/label.dart';
import 'theme.dart';

const tooltipWait = Duration(milliseconds: 500);

Expand Down Expand Up @@ -232,7 +233,7 @@ Widget _recordingStatus({Key key, String recordedObject}) {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Recording $recordedObject'),
const SizedBox(height: 16.0),
const SizedBox(height: defaultSpacing),
const CircularProgressIndicator(),
],
);
Expand All @@ -249,10 +250,10 @@ Widget processingInfo({
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Processing $processedObject'),
const SizedBox(height: 16.0),
const SizedBox(height: defaultSpacing),
SizedBox(
width: 200.0,
height: 16.0,
height: defaultSpacing,
child: LinearProgressIndicator(
value: progressValue,
),
Expand Down Expand Up @@ -321,7 +322,7 @@ class ToggleButton extends StatelessWidget {
waitDuration: tooltipWait,
preferBelow: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
child: MaterialIconLabel(
icon,
text,
Expand Down
3 changes: 2 additions & 1 deletion packages/devtools_app/lib/src/flutter/screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../performance/flutter/performance_screen.dart';
import '../timeline/flutter/timeline_screen.dart';
import 'connect_screen.dart';
import 'scaffold.dart';
import 'theme.dart';

/// Defines pages shown in the tabbar of the app.
@immutable
Expand Down Expand Up @@ -41,7 +42,7 @@ abstract class Screen {
key: tabKey,
child: Row(
children: <Widget>[
Icon(icon, size: 16.0),
Icon(icon, size: defaultIconSize),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(title),
Expand Down
139 changes: 78 additions & 61 deletions packages/devtools_app/lib/src/flutter/table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class FlatTable<T> extends StatefulWidget {
@required this.data,
@required this.keyFactory,
@required this.onItemSelected,
@required this.startingSortColumn,
@required this.startingSortDirection,
@required this.sortColumn,
@required this.sortDirection,
}) : assert(columns != null),
assert(keyFactory != null),
assert(data != null),
Expand All @@ -45,31 +45,43 @@ class FlatTable<T> extends StatefulWidget {

final ItemCallback<T> onItemSelected;

final ColumnData<T> startingSortColumn;
final ColumnData<T> sortColumn;

final SortDirection startingSortDirection;
final SortDirection sortDirection;

@override
_FlatTableState<T> createState() => _FlatTableState<T>();
FlatTableState<T> createState() => FlatTableState<T>();
}

class _FlatTableState<T> extends State<FlatTable<T>> with TableSortMixin<T> {
class FlatTableState<T> extends State<FlatTable<T>>
implements SortableTable<T> {
List<double> columnWidths;

List<T> data;

@override
void initState() {
super.initState();
sortData(widget.startingSortColumn, widget.startingSortDirection);
_initData();
columnWidths = _computeColumnWidths();
}

@override
void didUpdateWidget(FlatTable<T> oldWidget) {
super.didUpdateWidget(oldWidget);
sortData(widget.startingSortColumn, widget.startingSortDirection);
if (widget.sortColumn != oldWidget.sortColumn ||
widget.sortDirection != oldWidget.sortDirection ||
!collectionEquals(widget.data, oldWidget.data)) {
_initData();
}
columnWidths = _computeColumnWidths();
}

void _initData() {
data = List.from(widget.data);
sortData(widget.sortColumn, widget.sortDirection);
}

List<double> _computeColumnWidths() {
final widths = <double>[];
for (ColumnData<T> column in widget.columns) {
Expand All @@ -92,8 +104,8 @@ class _FlatTableState<T> extends State<FlatTable<T>> with TableSortMixin<T> {
columnWidths: columnWidths,
startAtBottom: true,
rowBuilder: _buildRow,
startingSortColumn: widget.startingSortColumn,
startingSortDirection: widget.startingSortDirection,
sortColumn: widget.sortColumn,
sortDirection: widget.sortDirection,
onSortChanged: _sortDataAndUpdate,
);
}
Expand Down Expand Up @@ -123,7 +135,7 @@ class _FlatTableState<T> extends State<FlatTable<T>> with TableSortMixin<T> {

@override
void sortData(ColumnData column, SortDirection direction) {
widget.data.sort((T a, T b) => _compareData(a, b, column, direction));
data.sort((T a, T b) => _compareData<T>(a, b, column, direction));
}
}

Expand All @@ -137,7 +149,7 @@ class _FlatTableState<T> extends State<FlatTable<T>> with TableSortMixin<T> {
/// The [ColumnData] gives this table information about how to size its
/// columns, how to present each row of `data`, and which rows to show.
///
/// To lay the contents out, this table's [_TreeTableState] creates a flattened
/// To lay the contents out, this table's [TreeTableState] creates a flattened
/// list of the tree hierarchy. It then uses the nesting level of the
/// deepest row in [dataRoots] to determine how wide to make the [treeColumn].
///
Expand All @@ -151,10 +163,10 @@ class TreeTable<T extends TreeNode<T>> extends StatefulWidget {
@required this.treeColumn,
@required this.dataRoots,
@required this.keyFactory,
@required this.startingSortColumn,
@required this.startingSortDirection,
@required this.sortColumn,
@required this.sortDirection,
}) : assert(columns.contains(treeColumn)),
assert(columns.contains(startingSortColumn)),
assert(columns.contains(sortColumn)),
assert(columns != null),
assert(keyFactory != null),
assert(dataRoots != null),
Expand All @@ -172,19 +184,22 @@ class TreeTable<T extends TreeNode<T>> extends StatefulWidget {
/// Factory that creates keys for each row in this table.
final Key Function(T) keyFactory;

final ColumnData<T> startingSortColumn;
final ColumnData<T> sortColumn;

final SortDirection startingSortDirection;
final SortDirection sortDirection;

@override
_TreeTableState<T> createState() => _TreeTableState<T>();
TreeTableState<T> createState() => TreeTableState<T>();
}

class _TreeTableState<T extends TreeNode<T>> extends State<TreeTable<T>>
with TickerProviderStateMixin, TableSortMixin<T> {
class TreeTableState<T extends TreeNode<T>> extends State<TreeTable<T>>
with TickerProviderStateMixin
implements SortableTable<T> {
/// The number of items to show when animating out the tree table.
static const itemsToShowWhenAnimating = 50;

List<T> dataRoots;

List<T> items;
List<T> animatingChildren = [];
Set<T> animatingChildrenSet = {};
Expand All @@ -195,29 +210,38 @@ class _TreeTableState<T extends TreeNode<T>> extends State<TreeTable<T>>
@override
void initState() {
super.initState();
sortData(widget.startingSortColumn, widget.startingSortDirection);
rootsExpanded = List.generate(
widget.dataRoots.length, (index) => widget.dataRoots[index].isExpanded);
_initData();
rootsExpanded =
List.generate(dataRoots.length, (index) => dataRoots[index].isExpanded);
_updateItems();
}

@override
void didUpdateWidget(TreeTable oldWidget) {
super.didUpdateWidget(oldWidget);
sortData(widget.startingSortColumn, widget.startingSortDirection);
rootsExpanded = List.generate(
widget.dataRoots.length, (index) => widget.dataRoots[index].isExpanded);
if (widget.sortColumn != oldWidget.sortColumn ||
widget.sortDirection != oldWidget.sortDirection ||
!collectionEquals(widget.dataRoots, oldWidget.dataRoots)) {
_initData();
}
rootsExpanded =
List.generate(dataRoots.length, (index) => dataRoots[index].isExpanded);
_updateItems();
}

void _initData() {
dataRoots = List.from(widget.dataRoots);
sortData(widget.sortColumn, widget.sortDirection);
}

@override
void dispose() {
super.dispose();
}

void _updateItems() {
setState(() {
items = _buildFlatList(widget.dataRoots);
items = _buildFlatList(dataRoots);
// Leave enough space for the animating children during the animation.
columnWidths = _computeColumnWidths([...items, ...animatingChildren]);
});
Expand Down Expand Up @@ -255,16 +279,16 @@ class _TreeTableState<T extends TreeNode<T>> extends State<TreeTable<T>>
animatingChildrenSet = Set.of(animatingChildren);

// Update the tracked expansion of the root node if needed.
if (widget.dataRoots.contains(node)) {
final rootIndex = widget.dataRoots.indexOf(node);
if (dataRoots.contains(node)) {
final rootIndex = dataRoots.indexOf(node);
rootsExpanded[rootIndex] = node.isExpanded;
}
});
_updateItems();
}

List<double> _computeColumnWidths(List<T> flattenedList) {
final firstRoot = widget.dataRoots.first;
final firstRoot = dataRoots.first;
TreeNode deepest = firstRoot;
// This will use the width of all rows in the table, even the rows
// that are hidden by nesting.
Expand Down Expand Up @@ -324,8 +348,8 @@ class _TreeTableState<T extends TreeNode<T>> extends State<TreeTable<T>>
itemCount: items.length,
columnWidths: columnWidths,
rowBuilder: _buildRow,
startingSortColumn: widget.startingSortColumn,
startingSortDirection: widget.startingSortDirection,
sortColumn: widget.sortColumn,
sortDirection: widget.sortDirection,
onSortChanged: _sortDataAndUpdate,
);
}
Expand Down Expand Up @@ -367,12 +391,12 @@ class _TreeTableState<T extends TreeNode<T>> extends State<TreeTable<T>>
void sortData(ColumnData column, SortDirection direction) {
void _sort(T dataObject) {
dataObject.children
..sort((T a, T b) => _compareData(a, b, column, direction))
..sort((T a, T b) => _compareData<T>(a, b, column, direction))
..forEach(_sort);
}

widget.dataRoots.where((dataRoot) => dataRoot.level == 0).toList()
..sort((T a, T b) => _compareData(a, b, column, direction))
dataRoots.where((dataRoot) => dataRoot.level == 0).toList()
..sort((T a, T b) => _compareData<T>(a, b, column, direction))
..forEach(_sort);
}

Expand All @@ -389,8 +413,8 @@ class _Table<T> extends StatefulWidget {
@required this.columns,
@required this.columnWidths,
@required this.rowBuilder,
@required this.startingSortColumn,
@required this.startingSortDirection,
@required this.sortColumn,
@required this.sortDirection,
@required this.onSortChanged,
this.startAtBottom = false})
: super(key: key);
Expand All @@ -400,8 +424,8 @@ class _Table<T> extends StatefulWidget {
final List<ColumnData<T>> columns;
final List<double> columnWidths;
final IndexedScrollableWidgetBuilder rowBuilder;
final ColumnData<T> startingSortColumn;
final SortDirection startingSortDirection;
final ColumnData<T> sortColumn;
final SortDirection sortDirection;
final Function(ColumnData<T> column, SortDirection direction) onSortChanged;

/// The width to assume for columns that don't specify a width.
Expand All @@ -413,10 +437,6 @@ class _Table<T> extends StatefulWidget {
/// a height of 0 and a height of [defaultRowHeight].
static const defaultRowHeight = 42.0;

/// How much padding to place around the beginning
/// and end of each row in the table.
static const rowHorizontalPadding = 16.0;

@override
_TableState<T> createState() => _TableState<T>();
}
Expand All @@ -430,14 +450,13 @@ class _TableState<T> extends State<_Table<T>> {
void initState() {
super.initState();
_linkedHorizontalScrollControllerGroup = LinkedScrollControllerGroup();
sortColumn = widget.startingSortColumn;
sortDirection = widget.startingSortDirection;
sortColumn = widget.sortColumn;
sortDirection = widget.sortDirection;
}

/// The width of all columns in the table, with additional padding.
double get tableWidth =>
widget.columnWidths.reduce((x, y) => x + y) +
(2 * _Table.rowHorizontalPadding);
widget.columnWidths.reduce((x, y) => x + y) + (2 * defaultSpacing);

Widget _buildItem(BuildContext context, int index) {
if (widget.startAtBottom) {
Expand Down Expand Up @@ -714,7 +733,7 @@ class _TableRowState<T> extends State<TableRow<T>>
widget.sortDirection == SortDirection.ascending
? Icons.expand_less
: Icons.expand_more,
size: 16.0,
size: defaultIconSize,
),
const SizedBox(width: 4.0),
Text(
Expand All @@ -737,10 +756,10 @@ class _TableRowState<T> extends State<TableRow<T>>
turns: expandArrowAnimation,
child: const Icon(
Icons.expand_more,
size: 16.0,
size: defaultIconSize,
),
)
: const SizedBox(width: 16.0, height: 16.0);
: const SizedBox(width: defaultIconSize, height: defaultIconSize);
content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Expand Down Expand Up @@ -768,9 +787,7 @@ class _TableRowState<T> extends State<TableRow<T>>
}

return Padding(
padding: const EdgeInsets.symmetric(
horizontal: _Table.rowHorizontalPadding,
),
padding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
child: ListView.builder(
scrollDirection: Axis.horizontal,
controller: scrollController,
Expand All @@ -795,7 +812,7 @@ class _TableRowState<T> extends State<TableRow<T>>
void _handleSortChange(ColumnData<T> columnData) {
SortDirection direction;
if (columnData.title == widget.sortColumn.title) {
direction = widget.sortDirection.opposite();
direction = widget.sortDirection.reverse();
} else if (columnData.numeric) {
direction = SortDirection.descending;
} else {
Expand All @@ -805,13 +822,13 @@ class _TableRowState<T> extends State<TableRow<T>>
}
}

mixin TableSortMixin<T> {
int _compareFactor(SortDirection direction) =>
direction == SortDirection.ascending ? 1 : -1;
abstract class SortableTable<T> {
void sortData(ColumnData column, SortDirection direction);
}

int _compareData(T a, T b, ColumnData column, SortDirection direction) {
return column.compare(a, b) * _compareFactor(direction);
}
int _compareFactor(SortDirection direction) =>
direction == SortDirection.ascending ? 1 : -1;

void sortData(ColumnData column, SortDirection direction);
int _compareData<T>(T a, T b, ColumnData column, SortDirection direction) {
return column.compare(a, b) * _compareFactor(direction);
}
Loading