refactor: Centralize table formatting logic into a new formatTable utility function. (#182196)

This resolves https://github.com/flutter/flutter/issues/180949

This is follow-up on https://github.com/flutter/flutter/pull/180098
This commit is contained in:
Jaime Wren 2026-02-13 15:19:59 -08:00 committed by GitHub
parent d802cab600
commit ac3aa6dad6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 102 additions and 48 deletions

View File

@ -45,7 +45,7 @@ dependencies:
term_glyph: 1.2.2
test_api: 0.7.9
typed_data: 1.4.0
unified_analytics: 8.0.10
unified_analytics: 8.0.11
url_launcher_android: 6.3.28
url_launcher_ios: 6.4.0
url_launcher_linux: 3.2.2
@ -68,4 +68,4 @@ dev_dependencies:
flutter:
uses-material-design: true
# PUBSPEC CHECKSUM: nepob0
# PUBSPEC CHECKSUM: fv45lk

View File

@ -611,3 +611,38 @@ extension StackTraceTransform<T> on Stream<T> {
final utf8LineDecoder = StreamTransformer<List<int>, String>.fromBind(
(stream) => stream.transformWithCallSite(utf8.decoder).transform(const LineSplitter()),
);
/// Formats a list of rows into a table with aligned columns.
///
/// [table] is a list of rows, where each row is a list of strings.
/// [separator] is the string used to separate columns (default is '').
///
/// Returns a list of strings, where each string is a formatted row.
List<String> formatTable(List<List<String>> table, {String separator = '', int indent = 0}) {
if (table.isEmpty) {
return <String>[];
}
// Calculate column widths
if (table.first.isEmpty) {
throw Exception('Table header cannot be empty');
}
final indices = List<int>.generate(table.first.length - 1, (int i) => i);
final widths = List<int>.filled(indices.length, 0);
for (final row in table) {
for (final i in indices) {
widths[i] = math.max(widths[i], row[i].length);
}
}
final String indentString = ' ' * indent;
// Join columns into lines of text
return table.map<String>((List<String> row) {
final String formatted = indices
.map<String>((int i) => row[i].padRight(widths[i]))
.followedBy(<String>[row.last])
.join(separator);
return '$indentString$formatted';
}).toList();
}

View File

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'package:multicast_dns/multicast_dns.dart';
@ -118,23 +117,8 @@ class RunningAppsCommand extends FlutterCommand {
table.add(<String>['$projectName ($mode)', deviceString, platform, vmServiceUri, age]);
}
// TODO(jwren): consider combining this logic with the logic in `flutter devices`,
// see https://github.com/flutter/flutter/issues/180949
// Calculate column widths
final indices = List<int>.generate(table[0].length - 1, (int i) => i);
List<int> widths = indices.map<int>((int i) => 0).toList();
for (final row in table) {
widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList();
}
// Join columns into lines of text
for (final row in table) {
final String rowString = indices
.map<String>((int i) => row[i].padRight(widths[i]))
.followedBy(<String>[row.last])
.join('');
_logger.printStatus(' $rowString');
}
_logger.printStatus(formatTable(table, indent: 2).join('\n'));
return FlutterCommandResult.success();
}

View File

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:meta/meta.dart';
@ -859,21 +858,8 @@ abstract class Device {
]);
}
// Calculate column widths
final indices = List<int>.generate(table[0].length - 1, (int i) => i);
List<int> widths = indices.map<int>((int i) => 0).toList();
for (final row in table) {
widths = indices.map<int>((int i) => math.max(widths[i], row[i].length)).toList();
}
// Join columns into lines of text
return <String>[
for (final List<String> row in table)
indices
.map<String>((int i) => row[i].padRight(widths[i]))
.followedBy(<String>[row.last])
.join(''),
];
return formatTable(table);
}
static Future<void> printDevices(

View File

@ -20,7 +20,7 @@ dependencies:
completion: 1.0.2
coverage: 1.15.0
crypto: 3.0.7
ffi: 2.1.5
ffi: 2.2.0
file: 7.0.1
flutter_template_images: 5.0.0
html: 0.15.6
@ -54,7 +54,7 @@ dependencies:
http_multi_server: 3.2.2
convert: 3.1.2
async: 2.13.0
unified_analytics: 8.0.10
unified_analytics: 8.0.11
pubspec_parse: 1.5.0
graphs: 2.3.2
@ -113,7 +113,7 @@ dependencies:
watcher: 1.2.1
web: 1.1.1
web_socket: 1.0.1
yaml_edit: 2.2.3
yaml_edit: 2.2.4
mdns_dart: ^2.1.0
dev_dependencies:
@ -129,4 +129,4 @@ dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
# PUBSPEC CHECKSUM: 8kslb0
# PUBSPEC CHECKSUM: j77gl7

View File

@ -568,4 +568,53 @@ needs to be wrapped.
await validCompleter.future;
await errorCompleter.future;
});
testWithoutContext('formatTable', () {
expect(formatTable(<List<String>>[]), isEmpty);
expect(() => formatTable(<List<String>>[<String>[]]), throwsException);
expect(() => formatTable(<List<String>>[<String>[], <String>[]]), throwsException);
expect(
formatTable(<List<String>>[
<String>['Col1', 'Col2'],
<String>['Value1', 'Value2'],
]).join('\n'),
'''
Col1 Col2
Value1 Value2''',
);
expect(
formatTable(<List<String>>[
<String>['A', 'B', 'C'],
<String>['LongValue', 'Short', 'Tiny'],
]).join('\n'),
'''
A B C
LongValue Short Tiny''',
);
expect(
formatTable(<List<String>>[
<String>['A', 'B'],
<String>['1', '2'],
], separator: ' | ').join('\n'),
'''
A | B
1 | 2''',
);
});
testWithoutContext('formatTable with indent', () {
expect(
formatTable(<List<String>>[
<String>['A', 'B'],
<String>['1', '2'],
], indent: 2).join('\n'),
'''
A B
1 2''',
);
});
}

View File

@ -214,10 +214,10 @@ packages:
dependency: "direct main"
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.2.0"
ffigen:
dependency: "direct dev"
description:
@ -955,10 +955,10 @@ packages:
dependency: "direct main"
description:
name: video_player_avfoundation
sha256: f46e9e20f1fe429760cf4dc118761336320d1bec0f50d255930c2355f2defb5b
sha256: f93b93a3baa12ca0ff7d00ca8bc60c1ecd96865568a01ff0c18a99853ee201a5
url: "https://pub.dev"
source: hosted
version: "2.9.1"
version: "2.9.3"
video_player_platform_interface:
dependency: "direct main"
description:
@ -1099,10 +1099,10 @@ packages:
dependency: "direct main"
description:
name: yaml_edit
sha256: ec709065bb2c911b336853b67f3732dd13e0336bd065cc2f1061d7610ddf45e3
sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
version: "2.2.4"
sdks:
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"

View File

@ -101,7 +101,7 @@ dependencies:
device_info: 2.0.3
device_info_platform_interface: 2.0.1
fake_async: 1.3.3
ffi: 2.1.5
ffi: 2.2.0
file: 7.0.1
fixnum: 1.1.1
flutter_gallery_assets: 1.0.2
@ -186,7 +186,7 @@ dependencies:
vector_math: 2.2.0
video_player: 2.10.1
video_player_android: 2.9.1
video_player_avfoundation: 2.9.1
video_player_avfoundation: 2.9.3
video_player_platform_interface: 6.6.0
video_player_web: 2.4.0
vm_service: 15.0.2
@ -211,9 +211,9 @@ dependencies:
lints: 6.1.0
pedantic: 1.11.1
quiver: 3.2.2
yaml_edit: 2.2.3
yaml_edit: 2.2.4
dev_dependencies:
ffigen: 20.1.1
# PUBSPEC CHECKSUM: q8irkm
# PUBSPEC CHECKSUM: r03o20