Skip to content

Commit

Permalink
Add logic to conditionally show available extensions (#6048)
Browse files Browse the repository at this point in the history
  • Loading branch information
kenzieschmoll authored Jul 18, 2023
1 parent 61a4b54 commit d6b74b5
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 24 deletions.
57 changes: 37 additions & 20 deletions packages/devtools_app/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'example/conditional_screen.dart';
import 'extensions/extension_model.dart';
import 'extensions/ui/extension_screen.dart';
import 'framework/framework_core.dart';
import 'framework/home_screen.dart';
import 'framework/initializer.dart';
Expand Down Expand Up @@ -66,13 +68,13 @@ const showVmDeveloperMode = false;
@immutable
class DevToolsApp extends StatefulWidget {
const DevToolsApp(
this.screens,
this.originalScreens,
this.analyticsController, {
super.key,
this.sampleData = const [],
});

final List<DevToolsScreen> screens;
final List<DevToolsScreen> originalScreens;
final AnalyticsController analyticsController;
final List<DevToolsJsonFile> sampleData;

Expand All @@ -87,7 +89,18 @@ class DevToolsApp extends StatefulWidget {
// TODO(https://github.com/flutter/devtools/issues/1146): Introduce tests that
// navigate the full app.
class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin {
List<Screen> get _screens => widget.screens.map((s) => s.screen).toList();
List<Screen> get _screens => [
..._originalScreens,
if (FeatureFlags.devToolsExtensions) ..._extensionScreens,
];

List<Screen> get _originalScreens =>
widget.originalScreens.map((s) => s.screen).toList();

Iterable<Screen> get _extensionScreens =>
extensionService.availableExtensions.value.map(
(e) => DevToolsScreen<void>(ExtensionScreen(e)).screen,
);

bool get isDarkThemeEnabled => _isDarkThemeEnabled;
bool _isDarkThemeEnabled = true;
Expand Down Expand Up @@ -115,6 +128,14 @@ class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin {

unawaited(ga.setupDimensions());

if (FeatureFlags.devToolsExtensions) {
addAutoDisposeListener(extensionService.availableExtensions, () {
setState(() {
_clearCachedRoutes();
});
});
}

addAutoDisposeListener(serviceManager.isolateManager.mainIsolate, () {
setState(() {
_clearCachedRoutes();
Expand Down Expand Up @@ -215,10 +236,11 @@ class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin {

Widget scaffoldBuilder() {
// Force regeneration of visible screens when VM developer mode is
// enabled.
return ValueListenableBuilder<bool>(
valueListenable: preferences.vmDeveloperModeEnabled,
builder: (_, __, child) {
// enabled and when the list of available extensions change.
return DualValueListenableBuilder<bool, List<DevToolsExtensionConfig>>(
firstListenable: preferences.vmDeveloperModeEnabled,
secondListenable: extensionService.availableExtensions,
builder: (_, __, ___, child) {
final screens = _visibleScreens()
.where((p) => embed && page != null ? p.screenId == page : true)
.where((p) => !hide.contains(p.screenId))
Expand Down Expand Up @@ -257,8 +279,7 @@ class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin {
Map<String, UrlParametersBuilder> get pages {
return _routes ??= {
homeScreenId: _buildTabbedPage,
for (final screen in widget.screens)
screen.screen.screenId: _buildTabbedPage,
for (final screen in _screens) screen.screenId: _buildTabbedPage,
snapshotScreenId: (_, __, args, ___) {
final snapshotArgs = OfflineDataArguments.fromArgs(args);
final embed = isEmbedded(args);
Expand Down Expand Up @@ -305,7 +326,9 @@ class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin {
List<Screen> _visibleScreens() => _screens.where(shouldShowScreen).toList();

List<Provider> _providedControllers({bool offline = false}) {
return widget.screens
// We use [widget.originalScreens] here instead of [_screens] because
// extension screens do not provide a controller through this mechanism.
return widget.originalScreens
.where(
(s) => s.providesController && (offline ? s.supportsOffline : true),
)
Expand Down Expand Up @@ -360,10 +383,10 @@ class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin {
}

/// DevTools screen wrapper that is responsible for creating and providing the
/// screen's controller, as well as enabling offline support.
/// screen's controller, if one exists, as well as enabling offline support.
///
/// [C] corresponds to the type of the screen's controller, which is created by
/// [createController] and provided by [controllerProvider].
/// [createController] or provided by [controllerProvider].
class DevToolsScreen<C> {
const DevToolsScreen(
this.screen, {
Expand Down Expand Up @@ -459,10 +482,7 @@ List<DevToolsScreen> defaultScreens({
List<DevToolsJsonFile> sampleData = const [],
}) {
return devtoolsScreens ??= <DevToolsScreen>[
DevToolsScreen<void>(
HomeScreen(sampleData: sampleData),
createController: (_) {},
),
DevToolsScreen<void>(HomeScreen(sampleData: sampleData)),
DevToolsScreen<InspectorController>(
InspectorScreen(),
createController: (_) => InspectorController(
Expand Down Expand Up @@ -503,10 +523,7 @@ List<DevToolsScreen> defaultScreens({
LoggingScreen(),
createController: (_) => LoggingController(),
),
DevToolsScreen<void>(
ProviderScreen(),
createController: (_) {},
),
DevToolsScreen<void>(ProviderScreen()),
DevToolsScreen<AppSizeController>(
AppSizeScreen(),
createController: (_) => AppSizeController(),
Expand Down
83 changes: 83 additions & 0 deletions packages/devtools_app/lib/src/extensions/extension_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

// TODO(kenz): share this with devtools_server so that we do not duplicate.

/// Describes an extension that can be dynamically loaded into a custom screen
/// in DevTools.
class DevToolsExtensionConfig {
DevToolsExtensionConfig._({
required this.name,
required this.path,
required this.issueTrackerLink,
required this.version,
required this.materialIconCodePoint,
});

factory DevToolsExtensionConfig.parse(Map<String, Object?> json) {
// Defaults to the code point for [Icons.extensions_outlined] if null.
final codePoint = json[materialIconCodePointKey] as int? ?? 0xf03f;
return DevToolsExtensionConfig._(
name: json[nameKey]! as String,
path: json[pathKey]! as String,
issueTrackerLink: json[issueTrackerKey]! as String,
version: json[versionKey]! as String,
materialIconCodePoint: codePoint,
);
}

static const nameKey = 'name';
static const pathKey = 'path';
static const issueTrackerKey = 'issueTracker';
static const versionKey = 'version';
static const materialIconCodePointKey = 'materialIconCodePoint';

final String name;
final String path;
final String issueTrackerLink;
final String version;
final int materialIconCodePoint;

Map<String, Object?> toJson() => {
nameKey: name,
pathKey: path,
issueTrackerKey: issueTrackerLink,
versionKey: version,
materialIconCodePointKey: materialIconCodePoint,
};
}

extension ExtensionConfigExtension on DevToolsExtensionConfig {
IconData get icon => IconData(
materialIconCodePoint,
fontFamily: 'MaterialIcons',
);
}

// TODO(kenz): remove these once the DevTools extensions feature has shipped.
final List<DevToolsExtensionConfig> debugPlugins = [
DevToolsExtensionConfig.parse({
DevToolsExtensionConfig.nameKey: 'foo',
DevToolsExtensionConfig.issueTrackerKey: 'www.google.com',
DevToolsExtensionConfig.versionKey: '1.0.0',
DevToolsExtensionConfig.pathKey: '/path/to/foo',
}),
DevToolsExtensionConfig.parse({
DevToolsExtensionConfig.nameKey: 'bar',
DevToolsExtensionConfig.issueTrackerKey: 'www.google.com',
DevToolsExtensionConfig.versionKey: '2.0.0',
DevToolsExtensionConfig.materialIconCodePointKey: 0xe638,
DevToolsExtensionConfig.pathKey: '/path/to/bar',
}),
DevToolsExtensionConfig.parse({
DevToolsExtensionConfig.nameKey: 'provider',
DevToolsExtensionConfig.issueTrackerKey:
'https://github.com/rrousselGit/provider/issues',
DevToolsExtensionConfig.versionKey: '3.0.0',
DevToolsExtensionConfig.materialIconCodePointKey: 0xe50a,
DevToolsExtensionConfig.pathKey: '/path/to/provider',
}),
];
30 changes: 30 additions & 0 deletions packages/devtools_app/lib/src/extensions/extension_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';

import '../shared/globals.dart';
import '../shared/primitives/auto_dispose.dart';
import 'extension_model.dart';

class ExtensionService extends DisposableController
with AutoDisposeControllerMixin {
ValueListenable<List<DevToolsExtensionConfig>> get availableExtensions =>
_availableExtensions;
final _availableExtensions = ValueNotifier<List<DevToolsExtensionConfig>>([]);

void initialize() {
addAutoDisposeListener(serviceManager.connectedState, () {
_refreshAvailablePlugins();
});
}

// TODO(kenz): actually look up the available plugins from the server, based
// on the root path(s) from the available isolate(s).
int _count = 0;
void _refreshAvailablePlugins() {
_availableExtensions.value =
debugPlugins.sublist(0, _count++ % (debugPlugins.length + 1));
}
}
32 changes: 32 additions & 0 deletions packages/devtools_app/lib/src/extensions/ui/extension_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import '../../shared/primitives/listenable.dart';
import '../../shared/screen.dart';
import '../extension_model.dart';

class ExtensionScreen extends Screen {
ExtensionScreen(this.extensionConfig)
: super.conditional(
// TODO(kenz): we may need to ensure this is a unique id.
id: extensionConfig.name,
title: extensionConfig.name,
icon: extensionConfig.icon,
// TODO(kenz): support static DevTools extensions.
requiresConnection: true,
);

final DevToolsExtensionConfig extensionConfig;

@override
ValueListenable<bool> get showIsolateSelector =>
const FixedValueListenable<bool>(true);

@override
Widget build(BuildContext context) =>
Text('TODO: iFrame for ${extensionConfig.name}');
}
2 changes: 2 additions & 0 deletions packages/devtools_app/lib/src/framework/framework_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:async';
import 'package:logging/logging.dart';

import '../../devtools.dart' as devtools show version;
import '../extensions/extension_service.dart';
import '../screens/debugger/breakpoint_manager.dart';
import '../service/service.dart';
import '../service/service_manager.dart';
Expand Down Expand Up @@ -40,6 +41,7 @@ class FrameworkCore {
setGlobal(BannerMessagesController, BannerMessagesController());
setGlobal(BreakpointManager, BreakpointManager());
setGlobal(EvalService, EvalService());
setGlobal(ExtensionService, ExtensionService());
}

static void init() {
Expand Down
11 changes: 7 additions & 4 deletions packages/devtools_app/lib/src/framework/scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
widget.screens.indexOf(oldWidget.screens[_tabController!.index]);
}
// Create a new tab controller to reflect the changed tabs.
_setupTabController();
_tabController!.index = newIndex;
_setupTabController(startingIndex: newIndex);
} else if (widget.screens[_tabController!.index].screenId != widget.page) {
// If the page changed (eg. the route was modified by pressing back in the
// browser), animate to the new one.
Expand All @@ -170,9 +169,13 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
super.dispose();
}

void _setupTabController() {
void _setupTabController({int startingIndex = 0}) {
_tabController?.dispose();
_tabController = TabController(length: widget.screens.length, vsync: this);
_tabController = TabController(
initialIndex: startingIndex,
length: widget.screens.length,
vsync: this,
);

if (widget.page != null) {
final initialIndex =
Expand Down
5 changes: 5 additions & 0 deletions packages/devtools_app/lib/src/service/service_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import '../shared/connected_app.dart';
import '../shared/console/console_service.dart';
import '../shared/diagnostics/inspector_service.dart';
import '../shared/error_badge_manager.dart';
import '../shared/feature_flags.dart';
import '../shared/globals.dart';
import '../shared/primitives/utils.dart';
import '../shared/title.dart';
Expand Down Expand Up @@ -321,6 +322,10 @@ class ServiceConnectionManager {
return;
}

if (FeatureFlags.devToolsExtensions) {
extensionService.initialize();
}

_connectedState.value = const ConnectedState(true);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/devtools_app/lib/src/shared/feature_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ abstract class FeatureFlags {
/// https://github.com/flutter/devtools/issues/6013
static bool deepLinkValidation = enableExperiments;

/// Flag to enable DevTools extensions.
///
/// https://github.com/flutter/devtools/issues/1632
static bool devToolsExtensions = enableExperiments;

/// Stores a map of all the feature flags for debugging purposes.
///
/// When adding a new flag, you are responsible for adding it to this map as
Expand Down
4 changes: 4 additions & 0 deletions packages/devtools_app/lib/src/shared/globals.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../extensions/extension_service.dart';
import '../screens/debugger/breakpoint_manager.dart';
import '../service/service_manager.dart';
import '../shared/banner_messages.dart';
Expand Down Expand Up @@ -60,6 +61,9 @@ BreakpointManager get breakpointManager =>

EvalService get evalService => globals[EvalService] as EvalService;

ExtensionService get extensionService =>
globals[ExtensionService] as ExtensionService;

void setGlobal(Type clazz, Object instance) {
globals[clazz] = instance;
}
Expand Down

0 comments on commit d6b74b5

Please sign in to comment.