Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class InspectorServiceExtensionButtonGroup extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
const PlatformOverrideDropdown(),
const SizedBox(width: defaultSpacing),
ServiceExtensionButtonGroup(
fillColor: Theme.of(context).colorScheme.activeToggleButtonColor,
minScreenWidthForText: serviceExtensionButtonsIncludeTextWidth,
Expand Down
108 changes: 108 additions & 0 deletions packages/devtools_app/lib/src/service/service_extension_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import '../shared/globals.dart';
import '../shared/primitives/message_bus.dart';
import '../shared/primitives/utils.dart';
import '../shared/ui/common_widgets.dart';
import '../shared/ui/drop_down_button.dart';
import '../shared/ui/hover.dart';
import 'service_extensions.dart';
import 'service_registrations.dart';
Expand Down Expand Up @@ -1215,3 +1216,110 @@ class TogglePlatformSelector extends ServiceExtensionSelector {
}

*/

/// Dropdown that overrides the target platform of the connected Flutter app
/// using the [togglePlatformMode] service extension.
///
/// Hidden when the connected app does not expose the service extension, for
/// example when it runs in profile or release mode.
class PlatformOverrideDropdown extends StatelessWidget {
const PlatformOverrideDropdown({super.key});

@visibleForTesting
static const dropdownKey = Key('PlatformOverrideDropdown DropdownButton');

static const _fuchsia = 'fuchsia';

@override
Widget build(BuildContext context) {
final extensionManager =
serviceConnection.serviceManager.serviceExtensionManager;
final extensionName = togglePlatformMode.extension;
return ValueListenableBuilder<bool>(
valueListenable: extensionManager.hasServiceExtension(extensionName),
builder: (context, available, _) {
if (!available) return const SizedBox.shrink();
return ValueListenableBuilder<ServiceExtensionState>(
valueListenable: extensionManager.getServiceExtensionState(
extensionName,
),
builder: (context, state, _) {
// The value is null until DevTools has read it from the app.
final current = state.value as String?;
return AnalyticsDropDownButton<String>(
key: dropdownKey,
gaScreen: gac.inspector,
gaDropDownId: gac.togglePlatform,
message: togglePlatformMode.tooltip,
isDense: true,
value: current,
items: [
for (final platform in _platformsToShow(current))
_buildMenuItem(platform),
],
onChanged: _onPlatformChanged,
);
},
);
},
);
Comment on lines +1238 to +1265

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[CONCERN] Use the MultiValueListenableBuilder helper instead of nesting ValueListenableBuilders, as recommended by the DevTools Style Guide.

    return MultiValueListenableBuilder(
      listenables: [
        extensionManager.hasServiceExtension(extensionName),
        extensionManager.getServiceExtensionState(extensionName),
      ],
      builder: (context, values, _) {
        final available = values[0] as bool;
        if (!available) return const SizedBox.shrink();
        final state = values[1] as ServiceExtensionState;
        // The value is null until DevTools has read it from the app.
        final current = state.value as String?;
        return AnalyticsDropDownButton<String>(
          key: dropdownKey,
          gaScreen: gac.inspector,
          gaDropDownId: gac.togglePlatform,
          message: togglePlatformMode.tooltip,
          isDense: true,
          value: current,
          items: [
            for (final platform in _platformsToShow(current))
              _buildMenuItem(platform),
          ],
          onChanged: _onPlatformChanged,
        );
      },
    );
References
  1. Prefer MultiValueListenableBuilder instead of nested ValueListenableBuilders. (link)

}

/// The platforms to list in the menu.
///
/// This must include [current], or [DropdownButton] throws.
List<String> _platformsToShow(String? current) {
final platforms = togglePlatformMode.values;
return [
// Only list Fuchsia when the app is already using it.
for (final platform in platforms)
if (platform != _fuchsia || platform == current) platform,
// The app can report a platform that DevTools does not list yet, such as
// one added in a newer version of Flutter.
if (current != null && !platforms.contains(current)) current,
];
}

({DropdownMenuItem<String> item, String gaId}) _buildMenuItem(
String platform,
) {
final index = togglePlatformMode.values.indexOf(platform);
final label = index == -1
? platform
: togglePlatformMode.displayValues[index];
return (
item: DropdownMenuItem<String>(
value: platform,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_iconFor(platform), size: defaultIconSize),
const SizedBox(width: densePadding),
Text(label),
],
),
),
gaId: platform,
);
}

static IconData _iconFor(String platform) => switch (platform) {
'android' => Icons.android,
'iOS' => Icons.phone_iphone,
'macOS' => Icons.laptop_mac,
'windows' => Icons.desktop_windows,
_ => Icons.computer,
};

void _onPlatformChanged(String? platform) {
if (platform == null) return;
unawaited(
serviceConnection.serviceManager.serviceExtensionManager
.setServiceExtensionState(
togglePlatformMode.extension,
enabled: true,
value: platform,
),
);
Comment on lines +1316 to +1323

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[CONCERN] Use safeUnawaited instead of unawaited for fire-and-forget futures, as recommended by the DevTools Style Guide.

    safeUnawaited(
      serviceConnection.serviceManager.serviceExtensionManager
          .setServiceExtensionState(
            togglePlatformMode.extension,
            enabled: true,
            value: platform,
          ),
    );
References
  1. Prefer safeUnawaited(...) for fire-and-forget futures. (link)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -354,19 +354,11 @@ final slowAnimations = ToggleableServiceExtensionDescription<num>.from(
gaDocsItem: gac.slowAnimationDocs,
);

// TODO(https://github.com/flutter/devtools/issues/2780): hook this up in the UI.
// ignore: unused-code, TODO(https://github.com/flutter/devtools/issues/2780).
final togglePlatformMode = ServiceExtensionDescription<String>.from(
extensions.togglePlatformMode,
title: 'Override target platform',
iconAsset: 'icons/phone@2x.png',
displayValues: [
'Platform: iOS',
'Platform: Android',
'Platform: Fuchsia',
'Platform: MacOS',
'Platform: Linux',
],
displayValues: ['iOS', 'Android', 'Fuchsia', 'macOS', 'Linux', 'Windows'],
gaScreenName: gac.inspector,
gaItem: gac.togglePlatform,
tooltip: 'Override Target Platform',
Expand Down
3 changes: 2 additions & 1 deletion packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ the `package:provider` DevTools extension. [#10046](https://github.com/flutter/d

## Inspector updates

TODO: Remove this section if there are not any updates.
* Added a dropdown to the Inspector toolbar for overriding the target
platform of the connected app. [#10061](https://github.com/flutter/devtools/pull/10061)

## Performance updates

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app/src/screens/inspector/inspector_settings_dialog.dart';
import 'package:devtools_app/src/screens/inspector/layout_explorer/flex/flex.dart';
import 'package:devtools_app/src/screens/inspector/widget_details.dart';
import 'package:devtools_app/src/service/service_extension_widgets.dart';
import 'package:devtools_app/src/service/service_extensions.dart' as extensions;
import 'package:devtools_app/src/shared/feature_flags.dart';
import 'package:devtools_app/src/shared/ui/tab.dart';
Expand Down Expand Up @@ -256,6 +257,24 @@ void main() {
},
);

testWidgetsWithWindowSize(
'shows the platform override dropdown',
windowSize,
(WidgetTester tester) async {
final platformExtension = extensions.togglePlatformMode.extension;
fakeExtensionManager.extensionValueOnDevice = {
platformExtension: 'android',
};
await fakeExtensionManager.fakeAddServiceExtension(platformExtension);
await fakeExtensionManager.fakeFrame();

await tester.pumpWidget(buildInspectorScreen());
await tester.pump();
expect(find.byKey(PlatformOverrideDropdown.dropdownKey), findsOneWidget);
expect(find.text('Android'), findsOneWidget);
},
);

group('LayoutDetailsTab', () {
final renderObjectJson = jsonDecode('''
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,67 @@ void main() {
expect(checkboxWidget.onChanged, isNull);
});
});

group('PlatformOverrideDropdown', () {
final platformExtension = extensions.togglePlatformMode.extension;
late FakeServiceExtensionManager fakeExtensionManager;

setUp(() async {
fakeExtensionManager =
mockServiceManager.serviceExtensionManager
as FakeServiceExtensionManager;
await fakeExtensionManager.fakeFrame();
});

Future<void> pumpDropdown(WidgetTester tester) async {
await tester.pumpWidget(
wrap(const Scaffold(body: Center(child: PlatformOverrideDropdown()))),
);
}

testWidgets('overrides the platform of the connected app', (
WidgetTester tester,
) async {
fakeExtensionManager.extensionValueOnDevice = {
platformExtension: 'android',
};
await fakeExtensionManager.fakeAddServiceExtension(platformExtension);
await pumpDropdown(tester);
expect(find.text('Android'), findsOneWidget);

await tester.tap(find.byKey(PlatformOverrideDropdown.dropdownKey));
await tester.pumpAndSettle(); // finish the menu animation
expect(find.text('Fuchsia'), findsNothing);
expect(find.text('Windows'), findsOneWidget);
// The open menu shows each item again, so tap the last match.
await tester.tap(find.text('iOS').last);
await tester.pumpAndSettle(); // finish the menu animation

expect(
fakeExtensionManager.extensionValueOnDevice[platformExtension],
'iOS',
);
expect(find.text('iOS'), findsOneWidget);
});

testWidgets('lists a platform that DevTools does not know about', (
WidgetTester tester,
) async {
fakeExtensionManager.extensionValueOnDevice = {
platformExtension: 'newPlatform',
};
await fakeExtensionManager.fakeAddServiceExtension(platformExtension);
await pumpDropdown(tester);
expect(find.text('newPlatform'), findsOneWidget);
});

testWidgets('is hidden when the service extension is not available', (
WidgetTester tester,
) async {
await pumpDropdown(tester);
expect(find.byKey(PlatformOverrideDropdown.dropdownKey), findsNothing);
});
});
}

base class _UnavailableServiceExtensionManager
Expand Down
1 change: 1 addition & 0 deletions packages/devtools_app_shared/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ found in the LICENSE file or at https://developers.google.com/open-source/licens
such as the `file:` URI of a Dart CLI app started with `dart run`. Previously it
was the scheme of the URI, such as `file:`, which is not a package.
* Updates `devtools_shared` constraint to `^14.0.1`.
* Add `windows` to the values of the `togglePlatformMode` service extension.

## 0.5.1
* Add DevTools-styled text field `DevToolsTextField`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ final toggleOnDeviceWidgetInspector = ToggleableServiceExtension<bool>(
final togglePlatformMode = ServiceExtension<String>(
extension:
'$flutterExtensionPrefix${FoundationServiceExtensions.platformOverride.name}',
values: ['iOS', 'android', 'fuchsia', 'macOS', 'linux'],
values: ['iOS', 'android', 'fuchsia', 'macOS', 'linux', 'windows'],
);

/// Override the platform brightness (light mode, dark mode, or system default).
Expand Down
Loading