| // Copyright 2019 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 'dart:async'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:provider/provider.dart'; |
| |
| import 'example/conditional_screen.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/release_notes/release_notes.dart'; |
| import 'framework/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/inspector/inspector_controller.dart'; |
| import 'screens/inspector/inspector_screen.dart'; |
| import 'screens/inspector/inspector_tree_controller.dart'; |
| import 'screens/logging/logging_controller.dart'; |
| import 'screens/logging/logging_screen.dart'; |
| import 'screens/memory/framework/connected/memory_controller.dart'; |
| import 'screens/memory/framework/memory_screen.dart'; |
| import 'screens/memory/framework/static/static_screen_body.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.dart' as ga; |
| import 'shared/analytics/analytics_controller.dart'; |
| import 'shared/analytics/metrics.dart'; |
| import 'shared/common_widgets.dart'; |
| import 'shared/console/primitives/simple_items.dart'; |
| import 'shared/feature_flags.dart'; |
| import 'shared/globals.dart'; |
| import 'shared/offline_screen.dart'; |
| import 'shared/primitives/auto_dispose.dart'; |
| import 'shared/primitives/utils.dart'; |
| import 'shared/routing.dart'; |
| import 'shared/screen.dart'; |
| import 'shared/theme.dart'; |
| import 'shared/ui/hover.dart'; |
| import 'standalone_ui/standalone_screen.dart'; |
| |
| // Assign to true to use a sample implementation of a conditional screen. |
| // WARNING: Do not check in this file if debugEnableSampleScreen is true. |
| const debugEnableSampleScreen = false; |
| |
| // Disabled until VM developer mode functionality is added. |
| const showVmDeveloperMode = false; |
| |
| /// Top-level configuration for the app. |
| @immutable |
| class DevToolsApp extends StatefulWidget { |
| const DevToolsApp( |
| this.originalScreens, |
| this.analyticsController, { |
| super.key, |
| this.sampleData = const [], |
| }); |
| |
| final List<DevToolsScreen> originalScreens; |
| final AnalyticsController analyticsController; |
| final List<DevToolsJsonFile> sampleData; |
| |
| @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 => [ |
| ..._originalScreens, |
| if (FeatureFlags.devToolsExtensions) ..._extensionScreens, |
| ]; |
| |
| List<Screen> get _originalScreens => |
| widget.originalScreens.map((s) => s.screen).toList(); |
| |
| /// TODO(kenz): use [extensionService.visibleExtensions] instead of |
| /// [extensionService.availableExtensions] and verify tabs are added / removed |
| /// propertly based on the enabled state of extensions. |
| Iterable<Screen> get _extensionScreens => |
| extensionService.visibleExtensions.value.map( |
| (e) => DevToolsScreen<void>(ExtensionScreen(e)).screen, |
| ); |
| |
| bool get isDarkThemeEnabled => _isDarkThemeEnabled; |
| bool _isDarkThemeEnabled = true; |
| |
| bool get denseModeEnabled => _denseModeEnabled; |
| bool _denseModeEnabled = false; |
| |
| final hoverCardController = HoverCardController(); |
| |
| late ReleaseNotesController releaseNotesController; |
| |
| late final routerDelegate = DevToolsRouterDelegate(_getPage); |
| |
| @override |
| void initState() { |
| super.initState(); |
| |
| // 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()); |
| // } |
| |
| unawaited(ga.setupDimensions()); |
| |
| if (FeatureFlags.devToolsExtensions) { |
| addAutoDisposeListener(extensionService.availableExtensions, () { |
| setState(() { |
| _clearCachedRoutes(); |
| }); |
| }); |
| addAutoDisposeListener(extensionService.visibleExtensions, () { |
| setState(() { |
| _clearCachedRoutes(); |
| }); |
| }); |
| } |
| |
| addAutoDisposeListener(serviceManager.isolateManager.mainIsolate, () { |
| setState(() { |
| _clearCachedRoutes(); |
| }); |
| }); |
| |
| _isDarkThemeEnabled = preferences.darkModeTheme.value; |
| addAutoDisposeListener(preferences.darkModeTheme, () { |
| setState(() { |
| _isDarkThemeEnabled = preferences.darkModeTheme.value; |
| }); |
| }); |
| |
| _denseModeEnabled = preferences.denseModeEnabled.value; |
| addAutoDisposeListener(preferences.denseModeEnabled, () { |
| setState(() { |
| _denseModeEnabled = preferences.denseModeEnabled.value; |
| }); |
| }); |
| |
| releaseNotesController = ReleaseNotesController(); |
| } |
| |
| @override |
| void dispose() { |
| // preferences is initialized in main() to avoid flash of content with |
| // incorrect theme. |
| preferences.dispose(); |
| super.dispose(); |
| } |
| |
| @override |
| void didUpdateWidget(DevToolsApp oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| _clearCachedRoutes(); |
| } |
| |
| /// Gets the page for a given page/path and args. |
| Page _getPage( |
| BuildContext context, |
| String? page, |
| Map<String, String?> args, |
| DevToolsNavigationState? state, |
| ) { |
| // Provide the appropriate page route. |
| if (pages.containsKey(page)) { |
| Widget widget = pages[page!]!( |
| context, |
| page, |
| args, |
| state, |
| ); |
| assert( |
| () { |
| widget = _AlternateCheckedModeBanner( |
| builder: (context) => pages[page]!( |
| context, |
| page, |
| args, |
| state, |
| ), |
| ); |
| return true; |
| }(), |
| ); |
| return MaterialPage(child: widget); |
| } |
| |
| // Return a page not found. |
| return MaterialPage( |
| child: DevToolsScaffold.withChild( |
| key: const Key('not-found'), |
| embed: isEmbedded(args), |
| child: Center( |
| child: Column( |
| mainAxisSize: MainAxisSize.min, |
| children: [ |
| Text("'$page' not found."), |
| const SizedBox(height: defaultSpacing), |
| ElevatedButton( |
| onPressed: () => |
| routerDelegate.navigateHome(clearScreenParam: true), |
| child: const Text('Go to Home screen'), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Widget _buildTabbedPage( |
| BuildContext _, |
| String? page, |
| Map<String, String?> params, |
| DevToolsNavigationState? __, |
| ) { |
| final vmServiceUri = params['uri']; |
| final embed = isEmbedded(params); |
| final hide = {...?params['hide']?.split(',')}; |
| |
| // 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 = params['page']; |
| } |
| |
| final connectedToVmService = |
| vmServiceUri != null && vmServiceUri.isNotEmpty; |
| |
| Widget scaffoldBuilder() { |
| // Force regeneration of visible screens when VM developer mode is |
| // enabled and when the list of available extensions change. |
| return MultiValueListenableBuilder( |
| listenables: [ |
| preferences.vmDeveloperModeEnabled, |
| extensionService.availableExtensions, |
| extensionService.visibleExtensions, |
| ], |
| builder: (_, __, child) { |
| final screens = _visibleScreens() |
| .where((p) => embed && page != null ? p.screenId == page : true) |
| .where((p) => !hide.contains(p.screenId)) |
| .toList(); |
| return MultiProvider( |
| providers: _providedControllers(), |
| child: DevToolsScaffold( |
| embed: embed, |
| page: page, |
| screens: screens, |
| actions: [ |
| if (connectedToVmService) |
| // TODO(https://github.com/flutter/devtools/issues/1941) |
| if (serviceManager.connectedApp?.isFlutterAppNow ?? |
| false) ...[ |
| const HotReloadButton(), |
| const HotRestartButton(), |
| ], |
| ...DevToolsScaffold.defaultActions(isEmbedded: embed), |
| ], |
| ), |
| ); |
| }, |
| ); |
| } |
| |
| return connectedToVmService |
| ? Initializer( |
| url: vmServiceUri, |
| allowConnectionScreenOnDisconnect: !embed, |
| 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: (_, __, args, ___) { |
| final snapshotArgs = OfflineDataArguments.fromArgs(args); |
| final embed = isEmbedded(args); |
| return DevToolsScaffold.withChild( |
| key: UniqueKey(), |
| embed: embed, |
| child: MultiProvider( |
| providers: _providedControllers(offline: true), |
| child: OfflineScreenBody(snapshotArgs, _screens), |
| ), |
| ); |
| }, |
| if (FeatureFlags.memoryAnalysis) |
| memoryAnalysisScreenId: (_, __, args, ____) { |
| final embed = isEmbedded(args); |
| return DevToolsScaffold.withChild( |
| key: const Key('memoryanalysis'), |
| embed: embed, |
| child: MultiProvider( |
| providers: _providedControllers(), |
| child: const StaticMemoryBody(), |
| ), |
| ); |
| }, |
| if (FeatureFlags.vsCodeSidebarTooling) ..._standaloneScreens, |
| }; |
| } |
| |
| Map<String, UrlParametersBuilder> get _standaloneScreens { |
| return { |
| for (final type in StandaloneScreenType.values) |
| type.name: (_, __, args, ___) => type.screen, |
| }; |
| } |
| |
| bool isEmbedded(Map<String, String?> args) => args['embed'] == 'true'; |
| |
| Map<String, UrlParametersBuilder>? _routes; |
| |
| void _clearCachedRoutes() { |
| _routes = null; |
| } |
| |
| List<Screen> _visibleScreens() => _screens.where(shouldShowScreen).toList(); |
| |
| List<Provider> _providedControllers({bool offline = false}) { |
| // 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), |
| ) |
| .map((s) => s.controllerProvider(routerDelegate)) |
| .toList(); |
| } |
| |
| @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) { |
| 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: 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), |
| ); |
| } |
| } |
| |
| /// 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] or provided by [controllerProvider]. |
| class DevToolsScreen<C> { |
| const DevToolsScreen( |
| this.screen, { |
| this.createController, |
| this.controller, |
| this.supportsOffline = false, |
| }) : assert(createController == null || controller == null); |
| |
| final Screen screen; |
| |
| /// Responsible for creating the controller for this screen, if non-null. |
| /// |
| /// The controller will then be provided via [controllerProvider], and |
| /// widgets depending on this controller can access it by calling |
| /// `Provider<C>.of(context)`. |
| /// |
| /// If [createController] and [controller] are both null, [screen] will be |
| /// responsible for creating and maintaining its own controller. |
| final C Function(DevToolsRouterDelegate)? createController; |
| |
| /// A provided controller for this screen, if non-null. |
| /// |
| /// The controller will then be provided via [controllerProvider], and |
| /// widgets depending on this controller can access it by calling |
| /// `Provider<C>.of(context)`. |
| /// |
| /// If [createController] and [controller] are both null, [screen] will be |
| /// responsible for creating and maintaining its own controller. |
| final C? controller; |
| |
| /// 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 || controller != null; |
| |
| /// Whether this screen has implemented offline support. |
| /// |
| /// Defaults to false. |
| final bool supportsOffline; |
| |
| Provider<C> controllerProvider(DevToolsRouterDelegate routerDelegate) { |
| assert( |
| (createController != null && controller == null) || |
| (createController == null && controller != null), |
| ); |
| final controllerLocal = controller; |
| if (controllerLocal != null) { |
| return Provider<C>.value(value: controllerLocal); |
| } |
| return Provider<C>(create: (_) => createController!(routerDelegate)); |
| } |
| } |
| |
| /// 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?, |
| Map<String, String?>, |
| 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({Key? key, required this.builder}) |
| : super(key: key); |
| final WidgetBuilder builder; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Banner( |
| message: 'DEBUG', |
| textDirection: TextDirection.ltr, |
| location: BannerLocation.topStart, |
| child: Builder( |
| builder: builder, |
| ), |
| ); |
| } |
| } |
| |
| /// 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<void>(HomeScreen(sampleData: sampleData)), |
| DevToolsScreen<InspectorController>( |
| InspectorScreen(), |
| createController: (_) => InspectorController( |
| inspectorTree: InspectorTreeController( |
| gaId: InspectorScreenMetrics.summaryTreeGaId, |
| ), |
| detailsTree: InspectorTreeController( |
| gaId: InspectorScreenMetrics.detailsTreeGaId, |
| ), |
| treeType: FlutterTreeType.widget, |
| ), |
| ), |
| DevToolsScreen<PerformanceController>( |
| PerformanceScreen(), |
| createController: (_) => PerformanceController(), |
| supportsOffline: true, |
| ), |
| DevToolsScreen<ProfilerScreenController>( |
| ProfilerScreen(), |
| createController: (_) => ProfilerScreenController(), |
| supportsOffline: true, |
| ), |
| DevToolsScreen<MemoryController>( |
| MemoryScreen(), |
| createController: (_) => MemoryController(), |
| ), |
| DevToolsScreen<DebuggerController>( |
| DebuggerScreen(), |
| createController: (routerDelegate) => DebuggerController( |
| routerDelegate: routerDelegate, |
| ), |
| ), |
| DevToolsScreen<NetworkController>( |
| NetworkScreen(), |
| createController: (_) => NetworkController(), |
| ), |
| DevToolsScreen<LoggingController>( |
| LoggingScreen(), |
| createController: (_) => LoggingController(), |
| ), |
| DevToolsScreen<void>(ProviderScreen()), |
| DevToolsScreen<AppSizeController>( |
| AppSizeScreen(), |
| createController: (_) => AppSizeController(), |
| ), |
| if (FeatureFlags.deepLinkValidation) |
| DevToolsScreen<DeepLinksController>( |
| DeepLinksScreen(), |
| createController: (_) => DeepLinksController(), |
| ), |
| DevToolsScreen<VMDeveloperToolsController>( |
| VMDeveloperToolsScreen(), |
| createController: (_) => VMDeveloperToolsController(), |
| ), |
| // Show the sample DevTools screen. |
| if (debugEnableSampleScreen && (kDebugMode || kProfileMode)) |
| DevToolsScreen<ExampleController>( |
| const ExampleConditionalScreen(), |
| createController: (_) => ExampleController(), |
| supportsOffline: true, |
| ), |
| ]; |
| } |
| |
| @visibleForTesting |
| List<DevToolsScreen>? devtoolsScreens; |