| // Copyright 2019 The Flutter Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. |
| |
| /// @docImport 'package:vm_service/vm_service.dart'; |
| library; |
| |
| import 'dart:async'; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:devtools_app_shared/shared.dart'; |
| import 'package:devtools_app_shared/ui.dart'; |
| import 'package:devtools_app_shared/utils.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:provider/provider.dart'; |
| |
| import 'extensions/extension_screen.dart'; |
| import 'framework/framework_core.dart'; |
| import 'framework/home_screen.dart'; |
| import 'framework/initializer.dart'; |
| import 'framework/notifications_view.dart'; |
| import 'framework/observer/disconnect_observer.dart'; |
| import 'framework/release_notes.dart'; |
| import 'framework/scaffold/scaffold.dart'; |
| import 'screens/app_size/app_size_controller.dart'; |
| import 'screens/app_size/app_size_screen.dart'; |
| import 'screens/debugger/debugger_controller.dart'; |
| import 'screens/debugger/debugger_screen.dart'; |
| import 'screens/deep_link_validation/deep_links_controller.dart'; |
| import 'screens/deep_link_validation/deep_links_screen.dart'; |
| import 'screens/dtd/dtd_tools_controller.dart'; |
| import 'screens/dtd/dtd_tools_screen.dart'; |
| import 'screens/inspector_shared/inspector_screen.dart'; |
| import 'screens/inspector_shared/inspector_screen_controller.dart'; |
| import 'screens/logging/logging_controller.dart'; |
| import 'screens/logging/logging_screen.dart'; |
| import 'screens/memory/framework/memory_controller.dart'; |
| import 'screens/memory/framework/memory_screen.dart'; |
| import 'screens/network/network_controller.dart'; |
| import 'screens/network/network_screen.dart'; |
| import 'screens/performance/performance_controller.dart'; |
| import 'screens/performance/performance_screen.dart'; |
| import 'screens/profiler/profiler_screen.dart'; |
| import 'screens/profiler/profiler_screen_controller.dart'; |
| import 'screens/provider/provider_screen.dart'; |
| import 'screens/vm_developer/vm_developer_tools_controller.dart'; |
| import 'screens/vm_developer/vm_developer_tools_screen.dart'; |
| import 'service/service_extension_widgets.dart'; |
| import 'shared/analytics/analytics_controller.dart'; |
| import 'shared/feature_flags.dart'; |
| import 'shared/framework/framework_controller.dart'; |
| import 'shared/framework/routing.dart'; |
| import 'shared/framework/screen.dart'; |
| import 'shared/framework/screen_controllers.dart'; |
| import 'shared/globals.dart'; |
| import 'shared/offline/offline_data.dart'; |
| import 'shared/offline/offline_screen.dart'; |
| import 'shared/primitives/query_parameters.dart'; |
| import 'shared/primitives/utils.dart'; |
| import 'shared/ui/common_widgets.dart'; |
| import 'shared/ui/hover.dart'; |
| import 'shared/utils/focus_utils.dart'; |
| import 'shared/utils/utils.dart'; |
| import 'standalone_ui/standalone_screen.dart'; |
| |
| /// Top-level configuration for the app. |
| @immutable |
| class DevToolsApp extends StatefulWidget { |
| const DevToolsApp( |
| this.originalScreens, |
| this.analyticsController, { |
| super.key, |
| }); |
| |
| final List<DevToolsScreen> originalScreens; |
| final AnalyticsController analyticsController; |
| |
| @override |
| State<DevToolsApp> createState() => DevToolsAppState(); |
| } |
| |
| /// Initializer for the [FrameworkCore] and the app's navigation. |
| /// |
| /// This manages the route generation, and marshals URL query parameters into |
| /// flutter route parameters. |
| class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin { |
| List<Screen> get _screens { |
| if (FeatureFlags.devToolsExtensions.isEnabled) { |
| // TODO(https://github.com/flutter/devtools/issues/6273): stop special |
| // casing the package:provider extension. |
| final containsProviderExtension = extensionService |
| .currentExtensions |
| .value |
| .visibleExtensions |
| .where((e) => e.name == 'provider') |
| .isNotEmpty; |
| final devToolsScreens = containsProviderExtension |
| ? _originalScreens |
| .where((s) => s.screenId != ScreenMetaData.provider.id) |
| .toList() |
| : _originalScreens; |
| return [...devToolsScreens, ..._extensionScreens]; |
| } |
| return _originalScreens; |
| } |
| |
| List<Screen> get _originalScreens => |
| widget.originalScreens.map((s) => s.screen).toList(); |
| |
| Iterable<Screen> get _extensionScreens => |
| extensionService.visibleExtensions.map( |
| (e) => |
| DevToolsScreen<DevToolsScreenController>(ExtensionScreen(e)).screen, |
| ); |
| |
| final hoverCardController = HoverCardController(); |
| |
| late ReleaseNotesController releaseNotesController; |
| |
| late final routerDelegate = DevToolsRouterDelegate(_getPage); |
| |
| @override |
| void initState() { |
| super.initState(); |
| setGlobal(GlobalKey<NavigatorState>, routerDelegate.navigatorKey); |
| |
| autoDisposeStreamSubscription( |
| frameworkController.onConnectVmEvent.listen(_connectVm), |
| ); |
| |
| _initScreenControllers( |
| connected: |
| serviceConnection.serviceManager.connectedState.value.connected, |
| offline: offlineDataController.showingOfflineData.value, |
| ); |
| addAutoDisposeListener(offlineDataController.showingOfflineData, () { |
| final offlineMode = offlineDataController.showingOfflineData.value; |
| // Dispose the current offline controllers, if any, for any change to the |
| // showing offline data value. |
| screenControllers.disposeOfflineControllers(); |
| if (offlineMode) { |
| _initScreenControllers(offline: offlineMode); |
| } |
| }); |
| addAutoDisposeListener(serviceConnection.serviceManager.connectedState, () { |
| final connectionState = |
| serviceConnection.serviceManager.connectedState.value; |
| |
| // When disconnecting from an app, prepare the offline state for reviewing |
| // history. |
| screenControllers.forEachInitialized((screenController) { |
| if (screenController is OfflineScreenControllerMixin && |
| !connectionState.connected && |
| !connectionState.userInitiatedConnectionState) { |
| screenController.maybePrepareDataForReviewingHistory(); |
| } |
| }); |
| |
| // Dispose the current connected controllers, if any, for any change to |
| // the connected state. |
| screenControllers.disposeConnectedControllers(); |
| _initScreenControllers(connected: connectionState.connected); |
| }); |
| |
| // TODO(https://github.com/flutter/devtools/issues/6018): Once |
| // https://github.com/flutter/flutter/issues/129692 is fixed, disable the |
| // browser's native context menu on secondary-click, and instead use the |
| // menu provided by Flutter: |
| // if (kIsWeb) { |
| // unawaited(BrowserContextMenu.disableContextMenu()); |
| // } |
| |
| void clearRoutesAndSetState() { |
| setState(() { |
| _clearCachedRoutes(); |
| }); |
| } |
| |
| if (FeatureFlags.devToolsExtensions.isEnabled) { |
| addAutoDisposeListener( |
| extensionService.currentExtensions, |
| clearRoutesAndSetState, |
| ); |
| } |
| |
| addAutoDisposeListener( |
| serviceConnection.serviceManager.isolateManager.mainIsolate, |
| clearRoutesAndSetState, |
| ); |
| |
| addAutoDisposeListener(preferences.darkModeEnabled); |
| |
| releaseNotesController = ReleaseNotesController(); |
| |
| // Workaround for https://github.com/flutter/flutter/issues/155265. |
| setUpTextFieldFocusFixHandler(); |
| } |
| |
| @override |
| void dispose() { |
| FrameworkCore.dispose(); |
| // Workaround for https://github.com/flutter/flutter/issues/155265. |
| removeTextFieldFocusFixHandler(); |
| super.dispose(); |
| } |
| |
| @override |
| void didUpdateWidget(DevToolsApp oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| _clearCachedRoutes(); |
| } |
| |
| /// Connects to the VM with the given URI. |
| /// |
| /// This request usually comes from the IDE via the server API to reuse the |
| /// DevTools window after being disconnected (for example if the user stops |
| /// a debug session then launches a new one). |
| Future<void> _connectVm(ConnectVmEvent event) async { |
| await routerDelegate.updateArgsIfChanged({ |
| 'uri': event.serviceProtocolUri.toString(), |
| if (event.notify) 'notify': 'true', |
| }); |
| } |
| |
| /// Gets the page for a given page/path and args. |
| Page _getPage( |
| BuildContext context, |
| String? page, |
| DevToolsQueryParams params, |
| DevToolsNavigationState? state, |
| ) { |
| // `page` will initially be null while the router is set up, then we will |
| // be called again with an empty string for the root. |
| if (FrameworkCore.vmServiceInitializationInProgress || page == null) { |
| return const MaterialPage(child: CenteredCircularProgressIndicator()); |
| } |
| |
| // Provide the appropriate page route. |
| if (pages.containsKey(page)) { |
| Widget widget = pages[page]!(context, page, params, state); |
| assert(() { |
| widget = _AlternateCheckedModeBanner( |
| builder: (context) => pages[page]!(context, page, params, state), |
| ); |
| return true; |
| }()); |
| return MaterialPage(child: widget); |
| } |
| |
| // Return a page not found. |
| return MaterialPage( |
| child: DevToolsScaffold.withChild( |
| key: const Key('not-found'), |
| embedMode: params.embedMode, |
| child: ScreenUnavailable( |
| title: "The '$page' page cannot be found.", |
| embedMode: params.embedMode, |
| routerDelegate: routerDelegate, |
| ), |
| ), |
| ); |
| } |
| |
| Widget _buildTabbedPage( |
| BuildContext _, |
| String? page, |
| DevToolsQueryParams queryParams, |
| DevToolsNavigationState? _, |
| ) { |
| final vmServiceUri = queryParams.vmServiceUri; |
| final embedMode = queryParams.embedMode; |
| |
| // TODO(dantup): We should be able simplify this a little, removing params['page'] |
| // and only supporting /inspector (etc.) instead of also &page=inspector if |
| // all IDEs switch over to those URLs. |
| if (page?.isEmpty ?? true) { |
| page = queryParams.legacyPage; |
| } |
| |
| final paramsContainVmServiceUri = |
| vmServiceUri != null && vmServiceUri.isNotEmpty; |
| |
| Widget scaffoldBuilder() { |
| // Force regeneration of visible screens when Advanced Developer Mode is |
| // enabled and when the list of available extensions change. |
| return MultiValueListenableBuilder( |
| listenables: [ |
| preferences.advancedDeveloperModeEnabled, |
| extensionService.currentExtensions, |
| ], |
| builder: (_, _, child) { |
| final screensInScaffold = _visibleScreens() |
| .where( |
| (s) => maybeIncludeOnlyEmbeddedScreen( |
| s, |
| page: page, |
| embedMode: embedMode, |
| ), |
| ) |
| .toList(); |
| |
| removeHiddenScreens(screensInScaffold, queryParams); |
| |
| DevToolsScaffold scaffold; |
| |
| final originalScreen = _screens.firstWhereOrNull( |
| (s) => s.screenId == page, |
| ); |
| final screenInOriginalScreens = originalScreen != null; |
| final screenInScaffoldScreens = screensInScaffold.any( |
| (s) => s.screenId == page, |
| ); |
| if (page != null && |
| screenInOriginalScreens && |
| !screenInScaffoldScreens) { |
| // The requested [page] is in the list of DevTools screens, but is |
| // not available in list of available screens for this scaffold. |
| scaffold = DevToolsScaffold.withChild( |
| key: const Key('screen-disabled'), |
| embedMode: embedMode, |
| child: ScreenUnavailable( |
| title: "The '$page' screen is unavailable.", |
| description: _screenDisabledMessage(originalScreen), |
| routerDelegate: routerDelegate, |
| embedMode: embedMode, |
| ), |
| ); |
| } else if (screensInScaffold.isEmpty) { |
| // TODO(https://github.com/dart-lang/pub-dev/issues/7216): add an |
| // extensions store or a link to a pub.dev query for packages with |
| // extensions. |
| scaffold = DevToolsScaffold.withChild( |
| embedMode: embedMode, |
| child: CenteredMessage( |
| message: |
| 'No DevTools ' |
| '${queryParams.hideAllExceptExtensions ? 'extensions' : 'screens'} ' |
| 'available for your project.', |
| ), |
| ); |
| } else { |
| final connectedToFlutterApp = |
| serviceConnection |
| .serviceManager |
| .connectedApp |
| ?.isFlutterAppNow ?? |
| false; |
| final connectedToDartWebApp = |
| serviceConnection |
| .serviceManager |
| .connectedApp |
| ?.isDartWebAppNow ?? |
| false; |
| scaffold = DevToolsScaffold( |
| embedMode: embedMode, |
| page: page, |
| screens: screensInScaffold, |
| actions: isEmbedded() |
| ? [] |
| : [ |
| if (paramsContainVmServiceUri) ...[ |
| // Hide the hot reload button for Dart web apps, where the |
| // hot reload service extension is not avilable and where the |
| // [service.reloadServices] RPC is not implemented. |
| // TODO(https://github.com/flutter/devtools/issues/6441): find |
| // a way to show this for Dart web apps when supported. |
| if (!connectedToDartWebApp) |
| HotReloadButton( |
| callOnVmServiceDirectly: !connectedToFlutterApp, |
| ), |
| // This button will hide itself based on whether the |
| // hot restart service is available for the connected app. |
| const HotRestartButton(), |
| ], |
| ...DevToolsScaffold.defaultActions(), |
| ], |
| ); |
| } |
| return scaffold; |
| }, |
| ); |
| } |
| |
| return paramsContainVmServiceUri |
| ? Initializer(builder: (_) => scaffoldBuilder()) |
| : scaffoldBuilder(); |
| } |
| |
| /// The pages that the app exposes. |
| Map<String, UrlParametersBuilder> get pages { |
| return _routes ??= { |
| homeScreenId: _buildTabbedPage, |
| for (final screen in _screens) screen.screenId: _buildTabbedPage, |
| snapshotScreenId: (_, _, params, _) { |
| // TODO(kenz): support multiple offline routes, or prevent an offline |
| // route from being pushed on top of another one. |
| // If an offline route is pushed on top of another offline route, this |
| // will cause the oldest set of controllers to be destroyed. |
| screenControllers.disposeOfflineControllers(); |
| _initScreenControllers(offline: true); |
| return DevToolsScaffold.withChild( |
| key: UniqueKey(), |
| embedMode: params.embedMode, |
| child: OfflineScreenBody(params.offlineScreenId, _screens), |
| ); |
| }, |
| ..._standaloneScreens, |
| }; |
| } |
| |
| Map<String, UrlParametersBuilder>? _routes; |
| |
| void _clearCachedRoutes() { |
| _routes = null; |
| routerDelegate.refreshPages(); |
| } |
| |
| Map<String, UrlParametersBuilder> get _standaloneScreens { |
| // TODO(dantup): Standalone screens do not use DevToolsScaffold which means |
| // they do not currently send an initial "currentPage" event to inform |
| // the server which page they are rendering. |
| return { |
| for (final type in StandaloneScreenType.values) |
| type.name: (_, _, args, _) => type.screen, |
| }; |
| } |
| |
| // TODO(kenz): consider showing all screens and displaying the reason why they |
| // are not available instead of hiding screens. |
| List<Screen> _visibleScreens() => |
| _screens.where((screen) => shouldShowScreen(screen).show).toList(); |
| |
| void _initScreenControllers({bool connected = false, bool offline = false}) { |
| // We use [widget.originalScreens] here instead of [_screens] because |
| // extension screens do not provide a controller through this mechanism. |
| final screensThatProvideController = widget.originalScreens.where( |
| (s) => s.providesController, |
| ); |
| var screens = screensThatProvideController; |
| if (offline) { |
| screens = screensThatProvideController.where( |
| (s) => s.screen.worksWithOfflineData, |
| ); |
| } else if (!connected) { |
| screens = screensThatProvideController.where( |
| (s) => !s.screen.requiresConnection, |
| ); |
| } |
| for (final s in screens) { |
| s.registerController(routerDelegate, offline: offline); |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return MaterialApp.router( |
| debugShowCheckedModeBanner: false, |
| themeMode: isDarkThemeEnabled() ? ThemeMode.dark : ThemeMode.light, |
| theme: themeFor( |
| isDarkTheme: false, |
| ideTheme: ideTheme, |
| theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme), |
| ), |
| darkTheme: themeFor( |
| isDarkTheme: true, |
| ideTheme: ideTheme, |
| theme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme), |
| ), |
| builder: (context, child) { |
| if (child == null) { |
| return const CenteredMessage( |
| message: 'Uh-oh, something went wrong. Please refresh the page.', |
| ); |
| } |
| return MultiProvider( |
| providers: [ |
| Provider<AnalyticsController>.value( |
| value: widget.analyticsController, |
| ), |
| Provider<HoverCardController>.value(value: hoverCardController), |
| Provider<ReleaseNotesController>.value( |
| value: releaseNotesController, |
| ), |
| ], |
| child: NotificationsView( |
| child: ReleaseNotesViewer( |
| controller: releaseNotesController, |
| child: DisconnectObserver( |
| routerDelegate: routerDelegate, |
| child: child, |
| ), |
| ), |
| ), |
| ); |
| }, |
| routerDelegate: routerDelegate, |
| routeInformationParser: DevToolsRouteInformationParser(), |
| // Disable default scrollbar behavior on web to fix duplicate scrollbars |
| // bug, see https://github.com/flutter/flutter/issues/90697: |
| scrollBehavior: const MaterialScrollBehavior().copyWith( |
| scrollbars: !kIsWeb, |
| ), |
| ); |
| } |
| |
| /// Helper function that will be used in a 'List.where' call to generate a |
| /// list of [Screen]s to pass to a [DevToolsScaffold]. |
| /// |
| /// When [embedMode] is [EmbedMode.embedOne], this method will return true |
| /// only when [screen] matches the specified [page]. Otherwise, this method |
| /// will return true for any [screen]. |
| @visibleForTesting |
| static bool maybeIncludeOnlyEmbeddedScreen( |
| Screen screen, { |
| required String? page, |
| required EmbedMode embedMode, |
| }) { |
| if (embedMode == EmbedMode.embedOne && page != null) { |
| return screen.screenId == page; |
| } |
| return true; |
| } |
| |
| /// Helper function that removes any hidden screens from [screens] based on |
| /// the value of the 'hide' query parameter in [params]. |
| @visibleForTesting |
| static void removeHiddenScreens( |
| List<Screen> screens, |
| DevToolsQueryParams params, |
| ) { |
| screens.removeWhere((s) => params.hiddenScreens.contains(s.screenId)); |
| |
| // When 'hide=extensions' is in the query parameters, this remove all |
| // extension screens. |
| if (params.hideExtensions) { |
| screens.removeWhere((s) => s is ExtensionScreen); |
| } |
| |
| // When 'hide=all-except-extensions' is in the query parameters, remove all |
| // non-extension screens. |
| if (params.hideAllExceptExtensions) { |
| screens.removeWhere((s) => s is! ExtensionScreen); |
| } |
| } |
| |
| String? _screenDisabledMessage(Screen screen) { |
| final reason = shouldShowScreen(screen).disabledReason; |
| String? disabledMessage; |
| if (reason == ScreenDisabledReason.requiresDartLibrary) { |
| // Special case for screens that require a library since the message |
| // needs to be generated dynamically. |
| disabledMessage = |
| 'The ${screen.title} screen requires library ' |
| '${screen.requiresLibrary}, but the library was not detected.'; |
| } else if (reason?.message case final String message) { |
| disabledMessage = 'The ${screen.title} screen $message'; |
| } |
| return disabledMessage; |
| } |
| } |
| |
| /// DevTools screen wrapper that is responsible for creating and providing the |
| /// 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]. |
| class DevToolsScreen<C extends DevToolsScreenController> { |
| const DevToolsScreen(this.screen, {this.createController}); |
| |
| final Screen screen; |
| |
| /// Responsible for creating the controller for this screen, if non-null. |
| /// |
| /// If [createController] and `controller` are both null, [screen] will be |
| /// responsible for creating and maintaining its own controller. |
| /// |
| /// In the controller initialization, if logic requires a connected [VmService] |
| /// object (`serviceConnection.serviceManager.service`), then the controller should first await |
| /// the `serviceConnection.serviceManager.onServiceAvailable` future to ensure the service has |
| /// been initialized. |
| /// The controller does not need to handle re-connection to the application. When reconnected, |
| /// DevTools will create a new controller. However, the controller should make sure |
| /// not to fail if the connection is lost. |
| final C Function(DevToolsRouterDelegate)? createController; |
| |
| /// Returns true if a controller was provided for [screen]. If false, |
| /// [screen] is responsible for creating and maintaining its own controller. |
| bool get providesController => createController != null; |
| |
| /// Registers a screen controller with the [ScreenControllers] manager. |
| void registerController( |
| DevToolsRouterDelegate routerDelegate, { |
| required bool offline, |
| }) { |
| screenControllers.register<C>( |
| () => createController!(routerDelegate), |
| offline: offline, |
| ); |
| } |
| } |
| |
| /// A [WidgetBuilder] that takes an additional map of URL query parameters and |
| /// args, as well a state not included in the URL. |
| typedef UrlParametersBuilder = |
| Widget Function( |
| BuildContext, |
| String?, |
| DevToolsQueryParams, |
| DevToolsNavigationState?, |
| ); |
| |
| /// Displays the checked mode banner in the bottom end corner instead of the |
| /// top end corner. |
| /// |
| /// This avoids issues with widgets in the appbar being hidden by the banner |
| /// in a web or desktop app. |
| class _AlternateCheckedModeBanner extends StatelessWidget { |
| const _AlternateCheckedModeBanner({required this.builder}); |
| final WidgetBuilder builder; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Banner( |
| message: 'DEBUG', |
| textDirection: TextDirection.ltr, |
| location: BannerLocation.bottomEnd, |
| child: Builder(builder: builder), |
| ); |
| } |
| } |
| |
| class ScreenUnavailable extends StatelessWidget { |
| const ScreenUnavailable({ |
| super.key, |
| required this.title, |
| required this.embedMode, |
| required this.routerDelegate, |
| this.description, |
| }); |
| |
| final String title; |
| final DevToolsRouterDelegate routerDelegate; |
| final EmbedMode embedMode; |
| final String? description; |
| |
| @override |
| Widget build(BuildContext context) { |
| final theme = Theme.of(context); |
| return Center( |
| child: Column( |
| mainAxisSize: MainAxisSize.min, |
| children: [ |
| Text(title, style: theme.textTheme.titleLarge), |
| const SizedBox(height: denseSpacing), |
| if (description != null) |
| Text(description!, style: theme.regularTextStyle), |
| if (embedMode == EmbedMode.none) ...[ |
| const SizedBox(height: defaultSpacing), |
| ElevatedButton( |
| onPressed: () => |
| routerDelegate.navigateHome(clearScreenParam: true), |
| child: const Text('Go to Home screen'), |
| ), |
| ], |
| ], |
| ), |
| ); |
| } |
| } |
| |
| /// Screens to initialize DevTools with. |
| /// |
| /// If the screen depends on a provided controller, the provider should be |
| /// provided here. |
| /// |
| /// Conditional screens can be added to this list, and they will automatically |
| /// be shown or hidden based on the [Screen] conditionalLibrary provided. |
| List<DevToolsScreen> defaultScreens({ |
| List<DevToolsJsonFile> sampleData = const [], |
| }) { |
| return devtoolsScreens ??= <DevToolsScreen>[ |
| DevToolsScreen<DevToolsScreenController>( |
| HomeScreen(sampleData: sampleData), |
| ), |
| // TODO(https://github.com/flutter/devtools/issues/7860): Clean-up after |
| // Inspector V2 has been released. |
| DevToolsScreen<InspectorScreenController>( |
| InspectorScreen(), |
| createController: (_) => InspectorScreenController(), |
| ), |
| DevToolsScreen<PerformanceController>( |
| PerformanceScreen(), |
| createController: (_) => PerformanceController(), |
| ), |
| DevToolsScreen<ProfilerScreenController>( |
| ProfilerScreen(), |
| createController: (_) => ProfilerScreenController(), |
| ), |
| DevToolsScreen<MemoryController>( |
| MemoryScreen(), |
| createController: (_) => MemoryController(), |
| ), |
| DevToolsScreen<DebuggerController>( |
| DebuggerScreen(), |
| createController: (routerDelegate) => |
| DebuggerController(routerDelegate: routerDelegate), |
| ), |
| DevToolsScreen<NetworkController>( |
| NetworkScreen(), |
| createController: (_) => NetworkController(), |
| ), |
| DevToolsScreen<LoggingController>( |
| LoggingScreen(), |
| createController: (_) => LoggingController(), |
| ), |
| DevToolsScreen<DevToolsScreenController>(ProviderScreen()), |
| DevToolsScreen<AppSizeController>( |
| AppSizeScreen(), |
| createController: (_) => AppSizeController(), |
| ), |
| DevToolsScreen<DeepLinksController>( |
| DeepLinksScreen(), |
| createController: (_) => DeepLinksController(), |
| ), |
| DevToolsScreen<VMDeveloperToolsController>( |
| VMDeveloperToolsScreen(), |
| createController: (_) => VMDeveloperToolsController(), |
| ), |
| DevToolsScreen<DTDToolsController>( |
| DTDToolsScreen(), |
| createController: (_) => DTDToolsController(), |
| ), |
| ]; |
| } |
| |
| @visibleForTesting |
| List<DevToolsScreen>? devtoolsScreens; |