Add feature flag scaffolding to support experiments restricted to a specific Flutter channel (#9440)

diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart
index b1c2538..43c72cd 100644
--- a/packages/devtools_app/lib/src/app.dart
+++ b/packages/devtools_app/lib/src/app.dart
@@ -86,7 +86,7 @@
 /// flutter route parameters.
 class DevToolsAppState extends State<DevToolsApp> with AutoDisposeMixin {
   List<Screen> get _screens {
-    if (FeatureFlags.devToolsExtensions) {
+    if (FeatureFlags.devToolsExtensions.isEnabled) {
       // TODO(https://github.com/flutter/devtools/issues/6273): stop special
       // casing the package:provider extension.
       final containsProviderExtension = extensionService
@@ -177,7 +177,7 @@
       });
     }
 
-    if (FeatureFlags.devToolsExtensions) {
+    if (FeatureFlags.devToolsExtensions.isEnabled) {
       addAutoDisposeListener(
         extensionService.currentExtensions,
         clearRoutesAndSetState,
diff --git a/packages/devtools_app/lib/src/framework/framework_core.dart b/packages/devtools_app/lib/src/framework/framework_core.dart
index d7c4ac5..f141b01 100644
--- a/packages/devtools_app/lib/src/framework/framework_core.dart
+++ b/packages/devtools_app/lib/src/framework/framework_core.dart
@@ -84,7 +84,7 @@
     // preferences have been initialized.
     await extensionService.initialize();
 
-    if (FeatureFlags.memoryObserver) {
+    if (FeatureFlags.memoryObserver.isEnabled) {
       _memoryObserver.init();
     }
   }
@@ -99,7 +99,7 @@
     preferences.dispose();
     _themeManager?.dispose();
     unawaited(dtdManager.dispose());
-    if (FeatureFlags.memoryObserver) {
+    if (FeatureFlags.memoryObserver.isEnabled) {
       _memoryObserver.dispose();
     }
   }
diff --git a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
index be2b160..823ad42 100644
--- a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
+++ b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
@@ -58,7 +58,7 @@
 
   static List<Widget> defaultActions({Color? color}) => [
     OpenSettingsAction(color: color),
-    if (FeatureFlags.devToolsExtensions &&
+    if (FeatureFlags.devToolsExtensions.isEnabled &&
         !DevToolsQueryParams.load().hideExtensions)
       ExtensionSettingsAction(color: color),
     ReportFeedbackButton(color: color),
diff --git a/packages/devtools_app/lib/src/framework/scaffold/settings_dialog.dart b/packages/devtools_app/lib/src/framework/scaffold/settings_dialog.dart
index 675641e..b9250f7 100644
--- a/packages/devtools_app/lib/src/framework/scaffold/settings_dialog.dart
+++ b/packages/devtools_app/lib/src/framework/scaffold/settings_dialog.dart
@@ -77,7 +77,7 @@
               gaItem: gac.vmDeveloperMode,
             ),
           ),
-          if (FeatureFlags.wasmOptInSetting) ...[
+          if (FeatureFlags.wasmOptInSetting.isEnabled) ...[
             const SizedBox(height: largeSpacing),
             ...dialogSubHeader(theme, 'Experimental features'),
             Flexible(
diff --git a/packages/devtools_app/lib/src/screens/app_size/app_size_controller.dart b/packages/devtools_app/lib/src/screens/app_size/app_size_controller.dart
index 574a1e9..ecc58a4 100644
--- a/packages/devtools_app/lib/src/screens/app_size/app_size_controller.dart
+++ b/packages/devtools_app/lib/src/screens/app_size/app_size_controller.dart
@@ -772,7 +772,7 @@
 
   @override
   void releaseMemory({bool partial = false}) {
-    if (FeatureFlags.memoryObserver) {
+    if (FeatureFlags.memoryObserver.isEnabled) {
       // This behavior is the same regardless of the value of `partial`. We can
       // implement a partial clearing if it becomes necessary.
       clear(AppSizeScreen.analysisTabKey);
diff --git a/packages/devtools_app/lib/src/screens/debugger/debugger_controller.dart b/packages/devtools_app/lib/src/screens/debugger/debugger_controller.dart
index fceb7d3..aacc0ec 100644
--- a/packages/devtools_app/lib/src/screens/debugger/debugger_controller.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/debugger_controller.dart
@@ -575,7 +575,7 @@
       );
     }
     // Update the variables for the stack frame:
-    if (FeatureFlags.dapDebugging) {
+    if (FeatureFlags.dapDebugging.isEnabled) {
       serviceConnection.appState.setDapVariables(
         frame != null ? await _createDapVariablesForFrame(frame.frame) : [],
       );
diff --git a/packages/devtools_app/lib/src/screens/debugger/variables.dart b/packages/devtools_app/lib/src/screens/debugger/variables.dart
index 3c0dde1..6eda6ad 100644
--- a/packages/devtools_app/lib/src/screens/debugger/variables.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/variables.dart
@@ -24,7 +24,7 @@
   Widget build(BuildContext context) {
     // TODO(kenz): preserve expanded state of tree on switching frames and
     // on stepping.
-    if (FeatureFlags.dapDebugging) {
+    if (FeatureFlags.dapDebugging.isEnabled) {
       return TreeView<DapObjectNode>(
         dataRootsListenable: serviceConnection.appState.dapVariables,
         dataDisplayProvider: (variable, onPressed) {
diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart
index 29d36cd..d1f810a 100644
--- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart
+++ b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart
@@ -30,7 +30,8 @@
     final activeButtonColor = Theme.of(
       context,
     ).colorScheme.activeToggleButtonColor;
-    final isInspectorV2 = controller != null && FeatureFlags.inspectorV2;
+    final isInspectorV2 =
+        controller != null && FeatureFlags.inspectorV2.isEnabled;
     return Row(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart
index f44aa28..b8b7744 100644
--- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart
+++ b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart
@@ -46,7 +46,7 @@
   late InspectorScreenController controller;
 
   bool get shouldShowInspectorV2 =>
-      FeatureFlags.inspectorV2 &&
+      FeatureFlags.inspectorV2.isEnabled &&
       !preferences.inspector.legacyInspectorEnabled.value;
 
   @override
diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart
index 25a91a2..ebc3705 100644
--- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart
+++ b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart
@@ -65,7 +65,7 @@
                 const SizedBox(height: largeSpacing),
                 // TODO(https://github.com/flutter/devtools/issues/7860): Clean-up
                 // after Inspector V2 has been released.
-                if (FeatureFlags.inspectorV2)
+                if (FeatureFlags.inspectorV2.isEnabled)
                   Flexible(
                     child: CheckboxSetting(
                       notifier:
diff --git a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart
index d7c3181..9563c10 100644
--- a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart
+++ b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart
@@ -805,7 +805,7 @@
 
   @override
   void releaseMemory({bool partial = false}) {
-    if (FeatureFlags.memoryObserver) {
+    if (FeatureFlags.memoryObserver.isEnabled) {
       if (partial) {
         // Trim logs from the front so that the oldest logs are removed.
         _updateData(data.sublist(data.length ~/ 2));
diff --git a/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart b/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart
index 9a54884..3f2a8f1 100644
--- a/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart
+++ b/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart
@@ -217,7 +217,7 @@
 
   @override
   FutureOr<void> releaseMemory({bool partial = false}) async {
-    if (FeatureFlags.memoryObserver) {
+    if (FeatureFlags.memoryObserver.isEnabled) {
       diff.clearSnapshots(partial: partial);
       // Clear all allocation traces since the traces form a single tracing
       // profile.
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/secondary_controls.dart b/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/secondary_controls.dart
index 79515d6..af539bd 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/secondary_controls.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/control/widgets/secondary_controls.dart
@@ -52,7 +52,7 @@
           ),
           const SizedBox(width: denseSpacing),
         ],
-        if (FeatureFlags.memorySaveLoad) ...[
+        if (FeatureFlags.memorySaveLoad.isEnabled) ...[
           OpenSaveButtonGroup(
             screenId: ScreenMetaData.memory.id,
             onSave: (_) => onSave(),
diff --git a/packages/devtools_app/lib/src/screens/network/network_controller.dart b/packages/devtools_app/lib/src/screens/network/network_controller.dart
index 309b62e..1547315 100644
--- a/packages/devtools_app/lib/src/screens/network/network_controller.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart
@@ -515,7 +515,7 @@
 
   @override
   FutureOr<void> releaseMemory({bool partial = false}) async {
-    if (FeatureFlags.memoryObserver) {
+    if (FeatureFlags.memoryObserver.isEnabled) {
       await clear(partial: partial);
     }
   }
diff --git a/packages/devtools_app/lib/src/screens/network/network_screen.dart b/packages/devtools_app/lib/src/screens/network/network_screen.dart
index 90fecb0..88f71a9 100644
--- a/packages/devtools_app/lib/src/screens/network/network_screen.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart
@@ -244,7 +244,7 @@
           ),
         ),
         const SizedBox(width: denseSpacing),
-        if (FeatureFlags.networkSaveLoad)
+        if (FeatureFlags.networkSaveLoad.isEnabled)
           OpenSaveButtonGroup(
             screenId: ScreenMetaData.network.id,
             saveFormats: const [SaveFormat.devtools, SaveFormat.har],
diff --git a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart
index cfb4469..85424fe 100644
--- a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart
+++ b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart
@@ -291,7 +291,7 @@
 
   @override
   FutureOr<void> releaseMemory({bool partial = false}) async {
-    if (FeatureFlags.memoryObserver) {
+    if (FeatureFlags.memoryObserver.isEnabled) {
       await clearData(partial: partial, clearVmTimeline: !partial);
     }
   }
diff --git a/packages/devtools_app/lib/src/screens/profiler/profiler_screen_controller.dart b/packages/devtools_app/lib/src/screens/profiler/profiler_screen_controller.dart
index b476800..af58b7a 100644
--- a/packages/devtools_app/lib/src/screens/profiler/profiler_screen_controller.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/profiler_screen_controller.dart
@@ -188,7 +188,7 @@
 
   @override
   FutureOr<void> releaseMemory({bool partial = false}) async {
-    if (FeatureFlags.memoryObserver) {
+    if (FeatureFlags.memoryObserver.isEnabled) {
       // There is no way to partially release memory for this screen.
       await clear();
     }
diff --git a/packages/devtools_app/lib/src/service/vm_service_wrapper.dart b/packages/devtools_app/lib/src/service/vm_service_wrapper.dart
index 312d0a8..a49971f 100644
--- a/packages/devtools_app/lib/src/service/vm_service_wrapper.dart
+++ b/packages/devtools_app/lib/src/service/vm_service_wrapper.dart
@@ -487,7 +487,7 @@
     String command, {
     required Object? args,
   }) async {
-    if (!FeatureFlags.dapDebugging) return null;
+    if (!FeatureFlags.dapDebugging.isEnabled) return null;
 
     // Warn the user if there is no DDS connection.
     if (!_ddsSupported) {
diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart
index 8225c80..d885a3e 100644
--- a/packages/devtools_app/lib/src/shared/feature_flags.dart
+++ b/packages/devtools_app/lib/src/shared/feature_flags.dart
@@ -2,6 +2,7 @@
 // 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.
 
+import 'package:devtools_app_shared/service.dart';
 import 'package:flutter/foundation.dart';
 import 'package:logging/logging.dart';
 
@@ -36,84 +37,192 @@
 @visibleForTesting
 bool get enableBeta => enableExperiments || !isExternalBuild;
 
-const _kNetworkDisconnectExperience = bool.fromEnvironment(
-  'network_disconnect_experience',
-  defaultValue: true,
-);
-
 /// A namespace for feature flags, which set the visibility of features under
 /// active development.
 ///
 /// When adding a new feature flag, the developer is responsible for adding it
-/// to the [_allFlags] map for debugging purposes.
+/// to the [_booleanFlags] or [_flutterChannelFlags] map for debugging
+/// purposes.
 extension FeatureFlags on Never {
   /// Flag to enable the DevTools memory observer, which attempts to help users
   /// avoid OOM crashes.
   ///
   /// https://github.com/flutter/devtools/issues/7002
-  static bool memoryObserver = true;
+  static final memoryObserver = BooleanFeatureFlag(
+    name: 'memoryObserver',
+    enabled: true,
+  );
 
   /// Flag to enable save/load for the Memory screen.
   ///
   /// https://github.com/flutter/devtools/issues/8019
-  static bool memorySaveLoad = enableExperiments;
-
-  /// Flag to enable viewing offline data on the network screen when an app
-  /// disconnects.
-  ///
-  /// https://github.com/flutter/devtools/issues/3806
-  static const networkDisconnectExperience = _kNetworkDisconnectExperience;
+  static final memorySaveLoad = BooleanFeatureFlag(
+    name: 'memorySaveLoad',
+    enabled: enableExperiments,
+  );
 
   /// Flag to enable save/load for the Network screen.
   ///
   /// https://github.com/flutter/devtools/issues/4470
-  static bool networkSaveLoad = true;
+  static final networkSaveLoad = BooleanFeatureFlag(
+    name: 'networkSaveLoad',
+    enabled: true,
+  );
 
   /// Flag to enable DevTools extensions.
   ///
   /// TODO(https://github.com/flutter/devtools/issues/6443): remove this flag
   /// once extension support is added in g3.
-  static bool devToolsExtensions = isExternalBuild;
+  static final devToolsExtensions = BooleanFeatureFlag(
+    name: 'devToolsExtensions',
+    enabled: isExternalBuild,
+  );
 
   /// Flag to enable debugging via DAP.
   ///
   /// https://github.com/flutter/devtools/issues/6056
-  static bool dapDebugging = enableExperiments;
+  static final dapDebugging = BooleanFeatureFlag(
+    name: 'dapDebugging',
+    enabled: enableExperiments,
+  );
 
   /// Flag to enable the new Inspector panel.
   ///
   /// https://github.com/flutter/devtools/issues/7854
-  static bool inspectorV2 = true;
+  static final inspectorV2 = BooleanFeatureFlag(
+    name: 'inspectorV2',
+    enabled: true,
+  );
 
   /// Flag to enable the DevTools setting to opt-in to WASM.
   ///
   /// https://github.com/flutter/devtools/issues/7856
-  static bool wasmOptInSetting = true;
+  static final wasmOptInSetting = BooleanFeatureFlag(
+    name: 'wasmOptInSetting',
+    enabled: true,
+  );
 
   /// Flag to enable refactors in the Flutter Property Editor sidebar.
   ///
   /// https://github.com/flutter/devtools/issues/9214
-  static bool propertyEditorRefactors = true;
+  static final propertyEditorRefactors = BooleanFeatureFlag(
+    name: 'propertyEditorRefactors',
+    enabled: true,
+  );
 
-  /// Stores a map of all the feature flags for debugging purposes.
+  /// A set of all the boolean feature flags for debugging purposes.
   ///
-  /// When adding a new flag, you are responsible for adding it to this map as
-  /// well.
-  static final _allFlags = <String, bool>{
-    'memoryObserver': memoryObserver,
-    'memorySaveLoad': memorySaveLoad,
-    'networkDisconnectExperience': networkDisconnectExperience,
-    'networkSaveLoad': networkSaveLoad,
-    'dapDebugging': dapDebugging,
-    'inspectorV2': inspectorV2,
-    'wasmOptInSetting': wasmOptInSetting,
-    'propertyEditorRefactors': propertyEditorRefactors,
+  /// When adding a new boolean flag, you are responsible for adding it to this
+  /// map as well.
+  static final _booleanFlags = <BooleanFeatureFlag>{
+    memoryObserver,
+    memorySaveLoad,
+    networkSaveLoad,
+    devToolsExtensions,
+    dapDebugging,
+    inspectorV2,
+    wasmOptInSetting,
+    propertyEditorRefactors,
+  };
+
+  /// A set of all the Flutter channel feature flags for debugging purposes.
+  ///
+  /// When adding a new Flutter channel flag, you are responsible for adding it
+  /// to this map as well.
+  static final _flutterChannelFlags = <FlutterChannelFeatureFlag>{
+    // TODO(https://github.com/flutter/devtools/issues/9438): Add wasm flag.
   };
 
   /// A helper to print the status of all the feature flags.
-  static void debugPrintFeatureFlags() {
-    for (final entry in _allFlags.entries) {
-      _log.config('${entry.key}: ${entry.value}');
+  static void debugPrintFeatureFlags({ConnectedApp? connectedApp}) {
+    for (final entry in _booleanFlags) {
+      _log.config(entry.toString());
+    }
+
+    for (final entry in _flutterChannelFlags) {
+      var logLine = entry.toString();
+      if (connectedApp != null) {
+        logLine += '(enabled: ${entry.isEnabled(connectedApp)})';
+      }
+      _log.config(logLine);
     }
   }
 }
+
+/// A simple feature flag that is enabled or disabled by a boolean value.
+class BooleanFeatureFlag {
+  BooleanFeatureFlag({required this.name, required bool enabled})
+    : _enabled = enabled;
+
+  /// The name of the feature.
+  final String name;
+
+  bool _enabled;
+
+  /// Whether the feature is enabled.
+  bool get isEnabled => _enabled;
+
+  @override
+  String toString() => '$name: $isEnabled';
+
+  @visibleForTesting
+  void setEnabledForTests(bool enabled) {
+    _enabled = enabled;
+  }
+}
+
+/// A feature flag that is enabled based on the Flutter channel of the
+/// connected application.
+///
+/// This flag will be enabled if the connected app's Flutter channel is less
+/// than or equal to [flutterChannel]. For example, if [flutterChannel] is
+/// [FlutterChannel.beta], this flag will be enabled for apps on the 'beta' and
+/// 'dev' channels, but not for apps on the 'stable' channel.
+///
+/// TODO(https://github.com/flutter/devtools/issues/9439): Restrict features
+/// based on the user's Dart version instead of Flutter version to allow for
+/// shared experiments across Dart and Flutter.
+class FlutterChannelFeatureFlag {
+  const FlutterChannelFeatureFlag({
+    required this.name,
+    required this.flutterChannel,
+    required bool enabledForDartApps,
+    required bool enabledForFlutterAppsFallback,
+  }) : _enabledForDartApps = enabledForDartApps,
+       _enabledForFlutterAppsFallback = enabledForFlutterAppsFallback;
+
+  /// The name of the feature.
+  final String name;
+
+  /// The maximum Flutter channel that this feature is enabled for.
+  final FlutterChannel flutterChannel;
+
+  /// Whether the feature is enabled when the connected app is a pure Dart app.
+  final bool _enabledForDartApps;
+
+  /// Whether the feature is enabled when the connected app is a Flutter app,
+  /// but we cannot determine the Flutter channel.
+  final bool _enabledForFlutterAppsFallback;
+
+  /// Returns whether the feature is enabled based on the [connectedApp]'s
+  /// Flutter version.
+  bool isEnabled(ConnectedApp connectedApp) {
+    final isFlutterApp = connectedApp.isFlutterAppNow ?? false;
+    if (!isFlutterApp) {
+      return _enabledForDartApps;
+    }
+    final flutterVersion = connectedApp.flutterVersionNow?.version;
+    if (flutterVersion == null) return _enabledForFlutterAppsFallback;
+
+    final currentChannel = FlutterVersion.identifyChannel(
+      flutterVersion,
+      channelStr: connectedApp.flutterVersionNow?.channel,
+    );
+    if (currentChannel == null) return _enabledForFlutterAppsFallback;
+
+    return currentChannel <= flutterChannel;
+  }
+
+  @override
+  String toString() => '$name: <=${flutterChannel.name}';
+}
diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart
index 75344e2..fcacc56 100644
--- a/packages/devtools_app/lib/src/shared/framework/screen.dart
+++ b/packages/devtools_app/lib/src/shared/framework/screen.dart
@@ -17,7 +17,6 @@
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 
-import '../feature_flags.dart';
 import '../globals.dart';
 import '../primitives/listenable.dart';
 import '../ui/icons.dart';
@@ -25,6 +24,11 @@
 
 final _log = Logger('screen.dart');
 
+const _kNetworkDisconnectExperience = bool.fromEnvironment(
+  'network_disconnect_experience',
+  defaultValue: true,
+);
+
 enum ScreenMetaData {
   home(
     'home',
@@ -81,7 +85,7 @@
     tutorialVideoTimestamp: '?t=547',
     requiresConnection: false,
     // ignore: avoid_redundant_argument_values, false positive
-    worksWithOfflineData: FeatureFlags.networkDisconnectExperience,
+    worksWithOfflineData: _kNetworkDisconnectExperience,
   ),
   logging(
     'logging',
diff --git a/packages/devtools_app/lib/src/shared/preferences/preferences.dart b/packages/devtools_app/lib/src/shared/preferences/preferences.dart
index cfc9230..c54e260 100644
--- a/packages/devtools_app/lib/src/shared/preferences/preferences.dart
+++ b/packages/devtools_app/lib/src/shared/preferences/preferences.dart
@@ -118,7 +118,7 @@
     // Get the current values and listen for and write back changes.
     await _initDarkMode();
     await _initAdvancedDeveloperMode();
-    if (FeatureFlags.wasmOptInSetting) {
+    if (FeatureFlags.wasmOptInSetting.isEnabled) {
       await _initWasmEnabled();
     }
     await _initVerboseLogging();
diff --git a/packages/devtools_app/lib/src/shared/ui/utils.dart b/packages/devtools_app/lib/src/shared/ui/utils.dart
index d46894c..bf82e99 100644
--- a/packages/devtools_app/lib/src/shared/ui/utils.dart
+++ b/packages/devtools_app/lib/src/shared/ui/utils.dart
@@ -6,10 +6,10 @@
 import 'dart:math';
 
 import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
 
-import '../primitives/enum_utils.dart';
 import '../primitives/utils.dart';
 
 /// Returns a [TextSpan] that only includes the first [length] characters of
diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart
index f3d1bb5..2be618e 100644
--- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart
+++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_controller.dart
@@ -223,7 +223,8 @@
     CodeActionResult? refactorsResult;
     // TODO(https://github.com/flutter/devtools/issues/8652): Enable refactors
     // in the Property Editor by default.
-    if (editableArgsResult != null && FeatureFlags.propertyEditorRefactors) {
+    if (editableArgsResult != null &&
+        FeatureFlags.propertyEditorRefactors.isEnabled) {
       // Fetch the refactors using the start of the editable arguments' range,
       // which corresponds to the widget constructor name. This ensures that the
       // refactors are always available, even when the cursor is within the
diff --git a/packages/devtools_app/test/framework/home_screen_test.dart b/packages/devtools_app/test/framework/home_screen_test.dart
index 29774fb..0036d28 100644
--- a/packages/devtools_app/test/framework/home_screen_test.dart
+++ b/packages/devtools_app/test/framework/home_screen_test.dart
@@ -83,12 +83,7 @@
         fakeServiceConnection.serviceManager.vm.operatingSystem,
       ).thenReturn('android');
       final app = fakeServiceConnection.serviceManager.connectedApp!;
-      mockConnectedApp(
-        app,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(app);
       setGlobal(ServiceConnectionManager, fakeServiceConnection);
       setGlobal(IdeTheme, IdeTheme());
     }
diff --git a/packages/devtools_app/test/framework/observer/memory_observer_test.dart b/packages/devtools_app/test/framework/observer/memory_observer_test.dart
index 2572d3b..fb2ec91 100644
--- a/packages/devtools_app/test/framework/observer/memory_observer_test.dart
+++ b/packages/devtools_app/test/framework/observer/memory_observer_test.dart
@@ -37,7 +37,7 @@
     }
 
     setUpAll(() {
-      FeatureFlags.memoryObserver = true;
+      FeatureFlags.memoryObserver.setEnabledForTests(true);
     });
 
     setUp(() {
@@ -58,7 +58,7 @@
     });
 
     tearDownAll(() {
-      FeatureFlags.memoryObserver = false;
+      FeatureFlags.memoryObserver.setEnabledForTests(false);
     });
 
     test(
diff --git a/packages/devtools_app/test/framework/scaffold/about_dialog_test.dart b/packages/devtools_app/test/framework/scaffold/about_dialog_test.dart
index 2b32b4c..16a96d6 100644
--- a/packages/devtools_app/test/framework/scaffold/about_dialog_test.dart
+++ b/packages/devtools_app/test/framework/scaffold/about_dialog_test.dart
@@ -30,12 +30,7 @@
         fakeServiceConnection.serviceManager.vm.operatingSystem,
       ).thenReturn('android');
 
-      mockConnectedApp(
-        fakeServiceConnection.serviceManager.connectedApp!,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
       setGlobal(ServiceConnectionManager, fakeServiceConnection);
       setGlobal(
         DevToolsEnvironmentParameters,
diff --git a/packages/devtools_app/test/framework/scaffold/scaffold_debugger_test.dart b/packages/devtools_app/test/framework/scaffold/scaffold_debugger_test.dart
index 8d97604..381e3ab 100644
--- a/packages/devtools_app/test/framework/scaffold/scaffold_debugger_test.dart
+++ b/packages/devtools_app/test/framework/scaffold/scaffold_debugger_test.dart
@@ -53,12 +53,7 @@
     'does not display floating debugger controls when debugger screen is showing',
     (WidgetTester tester) async {
       final connectedApp = MockConnectedApp();
-      mockConnectedApp(
-        connectedApp,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(connectedApp);
       when(mockServiceManager.connectedAppInitialized).thenReturn(true);
       when(mockServiceManager.connectedApp).thenReturn(connectedApp);
       final mockDebuggerController = MockDebuggerController();
diff --git a/packages/devtools_app/test/framework/scaffold/scaffold_debugging_controls_test.dart b/packages/devtools_app/test/framework/scaffold/scaffold_debugging_controls_test.dart
index b0003df..09692bd 100644
--- a/packages/devtools_app/test/framework/scaffold/scaffold_debugging_controls_test.dart
+++ b/packages/devtools_app/test/framework/scaffold/scaffold_debugging_controls_test.dart
@@ -53,12 +53,7 @@
     WidgetTester tester,
   ) async {
     final connectedApp = MockConnectedApp();
-    mockConnectedApp(
-      connectedApp,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(connectedApp);
     when(mockServiceManager.connectedAppInitialized).thenReturn(true);
     when(mockServiceManager.connectedApp).thenReturn(connectedApp);
     when(mockServiceManager.isolateManager).thenReturn(FakeIsolateManager());
diff --git a/packages/devtools_app/test/framework/scaffold/scaffold_profile_test.dart b/packages/devtools_app/test/framework/scaffold/scaffold_profile_test.dart
index e4c41bf..ea4fbc4 100644
--- a/packages/devtools_app/test/framework/scaffold/scaffold_profile_test.dart
+++ b/packages/devtools_app/test/framework/scaffold/scaffold_profile_test.dart
@@ -52,12 +52,7 @@
     WidgetTester tester,
   ) async {
     final connectedApp = MockConnectedApp();
-    mockConnectedApp(
-      connectedApp,
-      isFlutterApp: true,
-      isProfileBuild: true,
-      isWebApp: false,
-    );
+    mockConnectedApp(connectedApp, isProfileBuild: true);
     when(mockServiceManager.connectedAppInitialized).thenReturn(true);
     when(mockServiceManager.connectedApp).thenReturn(connectedApp);
     final mockDebuggerController = MockDebuggerController();
diff --git a/packages/devtools_app/test/screens/cpu_profiler/profiler_screen_controller_test.dart b/packages/devtools_app/test/screens/cpu_profiler/profiler_screen_controller_test.dart
index fe074ab..fca7471 100644
--- a/packages/devtools_app/test/screens/cpu_profiler/profiler_screen_controller_test.dart
+++ b/packages/devtools_app/test/screens/cpu_profiler/profiler_screen_controller_test.dart
@@ -18,7 +18,7 @@
     late ProfilerScreenController controller;
 
     setUp(() {
-      FeatureFlags.memoryObserver = true;
+      FeatureFlags.memoryObserver.setEnabledForTests(true);
       final fakeServiceConnection = FakeServiceConnectionManager(
         service: FakeServiceManager.createFakeService(
           cpuSamples: CpuSamples.parse(goldenCpuSamplesJson),
@@ -63,7 +63,7 @@
     });
 
     test('releaseMemory', () async {
-      FeatureFlags.memoryObserver = true;
+      FeatureFlags.memoryObserver.setEnabledForTests(true);
       await controller.cpuProfilerController.loadAllSamples();
       expect(controller.cpuProfilerController.dataNotifier.value, isNotNull);
       expect(
@@ -75,7 +75,7 @@
         controller.cpuProfilerController.dataNotifier.value,
         CpuProfilerController.baseStateCpuProfileData,
       );
-      FeatureFlags.memoryObserver = false;
+      FeatureFlags.memoryObserver.setEnabledForTests(false);
     });
   });
 }
diff --git a/packages/devtools_app/test/screens/cpu_profiler/profiler_screen_test.dart b/packages/devtools_app/test/screens/cpu_profiler/profiler_screen_test.dart
index 0d6914b..6a7d1ed 100644
--- a/packages/devtools_app/test/screens/cpu_profiler/profiler_screen_test.dart
+++ b/packages/devtools_app/test/screens/cpu_profiler/profiler_screen_test.dart
@@ -77,9 +77,6 @@
       (WidgetTester tester) async {
         mockConnectedApp(
           scene.fakeServiceConnection.serviceManager.connectedApp!,
-          isFlutterApp: true,
-          isProfileBuild: true,
-          isWebApp: false,
         );
         await pumpProfilerScreen(tester);
         verifyBaseState();
diff --git a/packages/devtools_app/test/screens/debugger/debugger_codeview_statistics_test.dart b/packages/devtools_app/test/screens/debugger/debugger_codeview_statistics_test.dart
index 04a05a6..56e199f 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_codeview_statistics_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_codeview_statistics_test.dart
@@ -32,12 +32,7 @@
     scriptsHistory = ScriptsHistory();
 
     final app = fakeServiceConnection.serviceManager.connectedApp!;
-    mockConnectedApp(
-      app,
-      isFlutterApp: false,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(app);
     when(
       fakeServiceConnection.serviceManager.connectedApp!.isProfileBuildNow,
     ).thenReturn(false);
diff --git a/packages/devtools_app/test/screens/debugger/debugger_codeview_test.dart b/packages/devtools_app/test/screens/debugger/debugger_codeview_test.dart
index 4078226..db88c2f 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_codeview_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_codeview_test.dart
@@ -38,12 +38,7 @@
       codeViewController: codeViewController,
     );
     scriptsHistory = ScriptsHistory();
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isProfileBuild: false,
-      isFlutterApp: true,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     setGlobal(ServiceConnectionManager, fakeServiceConnection);
     setGlobal(IdeTheme, IdeTheme());
     setGlobal(ScriptManager, MockScriptManager());
diff --git a/packages/devtools_app/test/screens/debugger/debugger_screen_breakpoints_test.dart b/packages/devtools_app/test/screens/debugger/debugger_screen_breakpoints_test.dart
index b314e61..7470497 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_screen_breakpoints_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_screen_breakpoints_test.dart
@@ -19,12 +19,7 @@
   final mockBreakpointManager = MockBreakpointManager();
   final fakeServiceConnection = FakeServiceConnectionManager();
   final scriptManager = MockScriptManager();
-  mockConnectedApp(
-    fakeServiceConnection.serviceManager.connectedApp!,
-    isFlutterApp: true,
-    isProfileBuild: false,
-    isWebApp: false,
-  );
+  mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
   setGlobal(ServiceConnectionManager, fakeServiceConnection);
   setGlobal(IdeTheme, IdeTheme());
   setGlobal(ScriptManager, scriptManager);
diff --git a/packages/devtools_app/test/screens/debugger/debugger_screen_call_stack_test.dart b/packages/devtools_app/test/screens/debugger/debugger_screen_call_stack_test.dart
index d7379bb..9d697fe 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_screen_call_stack_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_screen_call_stack_test.dart
@@ -24,12 +24,7 @@
   setUp(() {
     fakeServiceConnection = FakeServiceConnectionManager();
     scriptManager = MockScriptManager();
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isProfileBuild: false,
-      isFlutterApp: true,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     setGlobal(ServiceConnectionManager, fakeServiceConnection);
     setGlobal(IdeTheme, IdeTheme());
     setGlobal(NotificationService, NotificationService());
diff --git a/packages/devtools_app/test/screens/debugger/debugger_screen_dap_variables_test.dart b/packages/devtools_app/test/screens/debugger/debugger_screen_dap_variables_test.dart
index f43193e..4bd607d 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_screen_dap_variables_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_screen_dap_variables_test.dart
@@ -23,17 +23,12 @@
   const windowSize = Size(2500, 1500);
 
   setUp(() {
-    FeatureFlags.dapDebugging = true;
+    FeatureFlags.dapDebugging.setEnabledForTests(true);
     vmService = createMockVmServiceWrapperWithDefaults();
     fakeServiceConnection = FakeServiceConnectionManager(service: vmService);
     scriptManager = MockScriptManager();
 
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isProfileBuild: false,
-      isFlutterApp: true,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     setGlobal(ServiceConnectionManager, fakeServiceConnection);
     setGlobal(IdeTheme, IdeTheme());
     setGlobal(ScriptManager, scriptManager);
diff --git a/packages/devtools_app/test/screens/debugger/debugger_screen_explorer_visibility_test.dart b/packages/devtools_app/test/screens/debugger/debugger_screen_explorer_visibility_test.dart
index 4c7a922..29d92c2 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_screen_explorer_visibility_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_screen_explorer_visibility_test.dart
@@ -27,12 +27,7 @@
     setUp(() {
       fakeServiceConnection = FakeServiceConnectionManager();
       scriptManager = MockScriptManager();
-      mockConnectedApp(
-        fakeServiceConnection.serviceManager.connectedApp!,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
       setGlobal(ServiceConnectionManager, fakeServiceConnection);
       setGlobal(IdeTheme, IdeTheme());
       setGlobal(NotificationService, NotificationService());
diff --git a/packages/devtools_app/test/screens/debugger/debugger_screen_paused_test.dart b/packages/devtools_app/test/screens/debugger/debugger_screen_paused_test.dart
index e5d74c4..8507c46 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_screen_paused_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_screen_paused_test.dart
@@ -29,12 +29,7 @@
     fakeServiceConnection = FakeServiceConnectionManager();
     scriptManager = MockScriptManager();
 
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     setGlobal(ServiceConnectionManager, fakeServiceConnection);
     setGlobal(IdeTheme, IdeTheme());
     setGlobal(ScriptManager, scriptManager);
diff --git a/packages/devtools_app/test/screens/debugger/debugger_screen_test.dart b/packages/devtools_app/test/screens/debugger/debugger_screen_test.dart
index 61b3039..63bc094 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_screen_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_screen_test.dart
@@ -36,12 +36,7 @@
     when(
       scriptManager.getScript(any),
     ).thenAnswer((_) => Future<Script>.value(testScript));
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     setGlobal(ServiceConnectionManager, fakeServiceConnection);
     setGlobal(IdeTheme, IdeTheme());
     setGlobal(ScriptManager, scriptManager);
diff --git a/packages/devtools_app/test/screens/debugger/debugger_screen_variables_test.dart b/packages/devtools_app/test/screens/debugger/debugger_screen_variables_test.dart
index 31145cf..2146435 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_screen_variables_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_screen_variables_test.dart
@@ -25,12 +25,7 @@
     fakeServiceConnection = FakeServiceConnectionManager();
     scriptManager = MockScriptManager();
 
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isProfileBuild: false,
-      isFlutterApp: true,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     setGlobal(ServiceConnectionManager, fakeServiceConnection);
     setGlobal(IdeTheme, IdeTheme());
     setGlobal(ScriptManager, scriptManager);
diff --git a/packages/devtools_app/test/screens/debugger/debugger_scripts_test.dart b/packages/devtools_app/test/screens/debugger/debugger_scripts_test.dart
index 548d95c..213c80f 100644
--- a/packages/devtools_app/test/screens/debugger/debugger_scripts_test.dart
+++ b/packages/devtools_app/test/screens/debugger/debugger_scripts_test.dart
@@ -36,12 +36,7 @@
     );
     setGlobal(PreferencesController, PreferencesController());
 
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isProfileBuild: false,
-      isFlutterApp: true,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
   }
 
   Future<void> pumpDebuggerScreen(
diff --git a/packages/devtools_app/test/screens/debugger/program_explorer_test.dart b/packages/devtools_app/test/screens/debugger/program_explorer_test.dart
index eea09eb..189a7f5 100644
--- a/packages/devtools_app/test/screens/debugger/program_explorer_test.dart
+++ b/packages/devtools_app/test/screens/debugger/program_explorer_test.dart
@@ -24,12 +24,7 @@
 
     setUp(() {
       final fakeServiceConnection = FakeServiceConnectionManager();
-      mockConnectedApp(
-        fakeServiceConnection.serviceManager.connectedApp!,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
       mockProgramExplorerController =
           createMockProgramExplorerControllerWithDefaults();
       setGlobal(IdeTheme, IdeTheme());
diff --git a/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart b/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart
index be7223b..7dbc36f 100644
--- a/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart
+++ b/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart
@@ -43,12 +43,7 @@
     fakeServiceConnection = FakeServiceConnectionManager();
     fakeExtensionManager =
         fakeServiceConnection.serviceManager.serviceExtensionManager;
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     when(
       fakeServiceConnection.errorBadgeManager.errorCountNotifier('inspector'),
     ).thenReturn(ValueNotifier<int>(0));
diff --git a/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart b/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart
index 159e87c..17d1b12 100644
--- a/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart
+++ b/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart
@@ -33,12 +33,7 @@
     setGlobal(PreferencesController, PreferencesController());
     setGlobal(NotificationService, NotificationService());
     setGlobal(BreakpointManager, BreakpointManager());
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
 
     inspectorController = InspectorController(
       inspectorTree: InspectorTreeController(),
diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart
index 303e1cf..813cc1e 100644
--- a/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart
+++ b/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart
@@ -54,12 +54,7 @@
     fakeServiceConnection = FakeServiceConnectionManager();
     fakeExtensionManager =
         fakeServiceConnection.serviceManager.serviceExtensionManager;
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     when(
       fakeServiceConnection.errorBadgeManager.errorCountNotifier('inspector'),
     ).thenReturn(ValueNotifier<int>(0));
diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart
index f134241..5a7c896 100644
--- a/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart
+++ b/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart
@@ -41,12 +41,7 @@
     setGlobal(PreferencesController, PreferencesController());
     setGlobal(NotificationService, NotificationService());
     setGlobal(BreakpointManager, BreakpointManager());
-    mockConnectedApp(
-      fakeServiceConnection.serviceManager.connectedApp!,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
 
     inspectorController = InspectorController(
       inspectorTree: InspectorTreeController(),
diff --git a/packages/devtools_app/test/screens/logging/logging_controller_test.dart b/packages/devtools_app/test/screens/logging/logging_controller_test.dart
index 35871ae..5dac954 100644
--- a/packages/devtools_app/test/screens/logging/logging_controller_test.dart
+++ b/packages/devtools_app/test/screens/logging/logging_controller_test.dart
@@ -338,12 +338,12 @@
 
     group('releaseMemory', () {
       setUp(() {
-        FeatureFlags.memoryObserver = true;
+        FeatureFlags.memoryObserver.setEnabledForTests(true);
         prepareTestLogs();
       });
 
       tearDown(() {
-        FeatureFlags.memoryObserver = false;
+        FeatureFlags.memoryObserver.setEnabledForTests(false);
       });
 
       test('releaseMemory - full release', () {
diff --git a/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart b/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart
index 3ae0e03..2fcc090 100644
--- a/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart
+++ b/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart
@@ -72,12 +72,7 @@
   setUp(() async {
     scene = MemoryDefaultScene();
     await scene.setUp(classList: classList);
-    mockConnectedApp(
-      scene.fakeServiceConnection.serviceManager.connectedApp!,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(scene.fakeServiceConnection.serviceManager.connectedApp!);
 
     final mockScriptManager = MockScriptManager();
     when(
@@ -112,11 +107,11 @@
 
   group('release memory', () {
     setUp(() {
-      FeatureFlags.memoryObserver = true;
+      FeatureFlags.memoryObserver.setEnabledForTests(true);
     });
 
     tearDown(() {
-      FeatureFlags.memoryObserver = false;
+      FeatureFlags.memoryObserver.setEnabledForTests(false);
     });
 
     testWidgetsWithWindowSize('full release', _windowSize, (
diff --git a/packages/devtools_app/test/screens/memory/tracing/tracing_view_test.dart b/packages/devtools_app/test/screens/memory/tracing/tracing_view_test.dart
index 3d850ab..d7e8d8e 100644
--- a/packages/devtools_app/test/screens/memory/tracing/tracing_view_test.dart
+++ b/packages/devtools_app/test/screens/memory/tracing/tracing_view_test.dart
@@ -78,9 +78,6 @@
       await scene.setUp(classList: classList);
       mockConnectedApp(
         scene.fakeServiceConnection.serviceManager.connectedApp!,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
       );
 
       final mockScriptManager = MockScriptManager();
diff --git a/packages/devtools_app/test/screens/performance/controls/performance_controls_test.dart b/packages/devtools_app/test/screens/performance/controls/performance_controls_test.dart
index 649f7da..b31e015 100644
--- a/packages/devtools_app/test/screens/performance/controls/performance_controls_test.dart
+++ b/packages/devtools_app/test/screens/performance/controls/performance_controls_test.dart
@@ -40,12 +40,7 @@
         mockServiceManager.serviceExtensionManager,
       ).thenReturn(FakeServiceExtensionManager());
       final connectedApp = MockConnectedApp();
-      mockConnectedApp(
-        connectedApp,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(connectedApp);
       when(mockServiceManager.connectedApp).thenReturn(connectedApp);
       setGlobal(ServiceConnectionManager, mockServiceConnection);
       mockPerformanceController = createMockPerformanceControllerWithDefaults();
@@ -85,12 +80,7 @@
     testWidgetsWithWindowSize('builds for non flutter app', windowSize, (
       WidgetTester tester,
     ) async {
-      mockConnectedApp(
-        mockServiceManager.connectedApp!,
-        isFlutterApp: false,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(mockServiceManager.connectedApp!, isFlutterApp: false);
       await pumpControls(tester);
 
       expect(find.byType(ExitOfflineButton), findsNothing);
diff --git a/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart b/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart
index b0d63ca..0945d83 100644
--- a/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart
+++ b/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart
@@ -40,12 +40,7 @@
   group('FlutterFramesChart', () {
     setUp(() {
       final fakeServiceConnection = FakeServiceConnectionManager();
-      mockConnectedApp(
-        fakeServiceConnection.serviceManager.connectedApp!,
-        isFlutterApp: true,
-        isProfileBuild: true,
-        isWebApp: false,
-      );
+      mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
       setGlobal(ServiceConnectionManager, fakeServiceConnection);
       setGlobal(OfflineDataController, OfflineDataController());
       setGlobal(IdeTheme, IdeTheme());
diff --git a/packages/devtools_app/test/screens/performance/frame_analysis/frame_analysis_test.dart b/packages/devtools_app/test/screens/performance/frame_analysis/frame_analysis_test.dart
index a442e3c..085b5ca 100644
--- a/packages/devtools_app/test/screens/performance/frame_analysis/frame_analysis_test.dart
+++ b/packages/devtools_app/test/screens/performance/frame_analysis/frame_analysis_test.dart
@@ -36,12 +36,7 @@
       final fakeServiceConnection = FakeServiceConnectionManager();
       setGlobal(ServiceConnectionManager, fakeServiceConnection);
       setGlobal(NotificationService, NotificationService());
-      mockConnectedApp(
-        fakeServiceConnection.serviceManager.connectedApp!,
-        isFlutterApp: true,
-        isProfileBuild: true,
-        isWebApp: false,
-      );
+      mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     });
 
     Future<void> pumpAnalysisView(
diff --git a/packages/devtools_app/test/screens/performance/frame_analysis/frame_hints_test.dart b/packages/devtools_app/test/screens/performance/frame_analysis/frame_hints_test.dart
index 8a97003..b8a3a40 100644
--- a/packages/devtools_app/test/screens/performance/frame_analysis/frame_hints_test.dart
+++ b/packages/devtools_app/test/screens/performance/frame_analysis/frame_hints_test.dart
@@ -47,12 +47,7 @@
       final fakeServiceConnection = FakeServiceConnectionManager();
       setGlobal(ServiceConnectionManager, fakeServiceConnection);
       setGlobal(NotificationService, NotificationService());
-      mockConnectedApp(
-        fakeServiceConnection.serviceManager.connectedApp!,
-        isFlutterApp: true,
-        isProfileBuild: true,
-        isWebApp: false,
-      );
+      mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
     });
 
     Future<void> pumpHints(
@@ -359,9 +354,7 @@
         );
         mockConnectedApp(
           serviceConnection.serviceManager.connectedApp!,
-          isFlutterApp: true,
           isProfileBuild: true,
-          isWebApp: false,
           os: 'android',
         );
         await pumpHints(tester, mockFrameAnalysis);
diff --git a/packages/devtools_app/test/screens/performance/performance_controller_test.dart b/packages/devtools_app/test/screens/performance/performance_controller_test.dart
index 16bf1b0..b33f738 100644
--- a/packages/devtools_app/test/screens/performance/performance_controller_test.dart
+++ b/packages/devtools_app/test/screens/performance/performance_controller_test.dart
@@ -29,12 +29,7 @@
       final mockServiceManager =
           mockServiceConnection.serviceManager as MockServiceManager;
       final connectedApp = MockConnectedApp();
-      mockConnectedApp(
-        connectedApp,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(connectedApp);
       when(mockServiceManager.connectedApp).thenReturn(connectedApp);
       when(
         mockServiceManager.connectedState,
diff --git a/packages/devtools_app/test/screens/performance/performance_screen_test.dart b/packages/devtools_app/test/screens/performance/performance_screen_test.dart
index 8c93288..6f23218 100644
--- a/packages/devtools_app/test/screens/performance/performance_screen_test.dart
+++ b/packages/devtools_app/test/screens/performance/performance_screen_test.dart
@@ -138,7 +138,6 @@
         mockConnectedApp(
           fakeServiceConnection.serviceManager.connectedApp!,
           isFlutterApp: false,
-          isProfileBuild: false,
           isWebApp: true,
         );
         await tester.pumpWidget(
@@ -169,8 +168,6 @@
         setEnableExperiments();
         mockConnectedApp(
           fakeServiceConnection.serviceManager.connectedApp!,
-          isFlutterApp: true,
-          isProfileBuild: false,
           isWebApp: true,
         );
         await tester.pumpWidget(
@@ -202,8 +199,6 @@
           mockConnectedApp(
             fakeServiceConnection.serviceManager.connectedApp!,
             isFlutterApp: false,
-            isProfileBuild: false,
-            isWebApp: false,
           );
           await pumpPerformanceScreen(tester, runAsync: true);
           await tester.pumpAndSettle();
diff --git a/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart b/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart
index 66239af..559e0b1 100644
--- a/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart
+++ b/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart
@@ -31,12 +31,7 @@
       ),
     );
     final app = fakeServiceConnection.serviceManager.connectedApp!;
-    mockConnectedApp(
-      app,
-      isFlutterApp: true,
-      isProfileBuild: true,
-      isWebApp: false,
-    );
+    mockConnectedApp(app, isProfileBuild: true);
     when(app.flutterVersionNow).thenReturn(
       FlutterVersion.parse(
         (await fakeServiceConnection.serviceManager.flutterVersion).json!,
@@ -214,12 +209,7 @@
         await tester.runAsync(() async {
           await setUpServiceManagerWithTimeline();
           final app = fakeServiceConnection.serviceManager.connectedApp!;
-          mockConnectedApp(
-            app,
-            isFlutterApp: false,
-            isProfileBuild: false,
-            isWebApp: false,
-          );
+          mockConnectedApp(app, isFlutterApp: false);
           when(app.flutterVersionNow).thenReturn(null);
 
           await pumpView(tester);
diff --git a/packages/devtools_app/test/service/connection_info_test.dart b/packages/devtools_app/test/service/connection_info_test.dart
index a941f72..19c8a33 100644
--- a/packages/devtools_app/test/service/connection_info_test.dart
+++ b/packages/devtools_app/test/service/connection_info_test.dart
@@ -34,12 +34,7 @@
         fakeServiceConnection.serviceManager.vm.operatingSystem,
       ).thenReturn('android');
       final app = fakeServiceConnection.serviceManager.connectedApp!;
-      mockConnectedApp(
-        app,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(app);
       setGlobal(ServiceConnectionManager, fakeServiceConnection);
       setGlobal(IdeTheme, IdeTheme());
     }
@@ -53,12 +48,7 @@
     ) async {
       final app = fakeServiceConnection.serviceManager.connectedApp!;
       mockWebVm(fakeServiceConnection.serviceManager.vm);
-      mockConnectedApp(
-        app,
-        isFlutterApp: false,
-        isProfileBuild: false,
-        isWebApp: true,
-      );
+      mockConnectedApp(app, isFlutterApp: false, isWebApp: true);
 
       await tester.pumpWidget(wrap(const ConnectedAppSummary()));
       expect(find.text('CPU / OS: '), findsOneWidget);
@@ -81,12 +71,7 @@
       when(
         fakeServiceConnection.serviceManager.vm.operatingSystem,
       ).thenReturn('macos');
-      mockConnectedApp(
-        app,
-        isFlutterApp: false,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(app, isFlutterApp: false);
 
       await tester.pumpWidget(wrap(const ConnectedAppSummary()));
       expect(find.text('CPU / OS: '), findsOneWidget);
@@ -107,12 +92,7 @@
       windowSize,
       (WidgetTester tester) async {
         final app = fakeServiceConnection.serviceManager.connectedApp!;
-        mockConnectedApp(
-          app,
-          isFlutterApp: true,
-          isProfileBuild: false,
-          isWebApp: false,
-        );
+        mockConnectedApp(app);
 
         await tester.pumpWidget(wrap(const ConnectedAppSummary()));
         expect(find.text('CPU / OS: '), findsOneWidget);
@@ -139,12 +119,7 @@
       windowSize,
       (WidgetTester tester) async {
         final app = fakeServiceConnection.serviceManager.connectedApp!;
-        mockConnectedApp(
-          app,
-          isFlutterApp: true,
-          isProfileBuild: true,
-          isWebApp: false,
-        );
+        mockConnectedApp(app, isProfileBuild: true);
 
         await tester.pumpWidget(wrap(const ConnectedAppSummary()));
         expect(find.text('CPU / OS: '), findsOneWidget);
@@ -171,12 +146,7 @@
       (WidgetTester tester) async {
         final app = fakeServiceConnection.serviceManager.connectedApp!;
         mockWebVm(fakeServiceConnection.serviceManager.vm);
-        mockConnectedApp(
-          app,
-          isFlutterApp: true,
-          isProfileBuild: false,
-          isWebApp: true,
-        );
+        mockConnectedApp(app, isWebApp: true);
 
         await tester.pumpWidget(wrap(const ConnectedAppSummary()));
         expect(find.text('CPU / OS: '), findsOneWidget);
@@ -204,12 +174,7 @@
       (WidgetTester tester) async {
         final app = fakeServiceConnection.serviceManager.connectedApp!;
         mockWebVm(fakeServiceConnection.serviceManager.vm);
-        mockConnectedApp(
-          app,
-          isFlutterApp: true,
-          isProfileBuild: true,
-          isWebApp: true,
-        );
+        mockConnectedApp(app, isProfileBuild: true, isWebApp: true);
 
         await tester.pumpWidget(wrap(const ConnectedAppSummary()));
         expect(find.text('CPU / OS: '), findsOneWidget);
diff --git a/packages/devtools_app/test/shared/charts/flame_chart_test.dart b/packages/devtools_app/test/shared/charts/flame_chart_test.dart
index 00784b3..e250a77 100644
--- a/packages/devtools_app/test/shared/charts/flame_chart_test.dart
+++ b/packages/devtools_app/test/shared/charts/flame_chart_test.dart
@@ -121,12 +121,7 @@
       setGlobal(PreferencesController, PreferencesController());
 
       final connectedApp = MockConnectedApp();
-      mockConnectedApp(
-        connectedApp,
-        isFlutterApp: true,
-        isProfileBuild: true,
-        isWebApp: false,
-      );
+      mockConnectedApp(connectedApp, isProfileBuild: true);
       when(mockServiceManager.connectedApp).thenReturn(connectedApp);
 
       controller = CpuProfilerController();
diff --git a/packages/devtools_app/test/shared/primitives/enum_utils_test.dart b/packages/devtools_app/test/shared/primitives/enum_utils_test.dart
index b31519f..a1e8d32 100644
--- a/packages/devtools_app/test/shared/primitives/enum_utils_test.dart
+++ b/packages/devtools_app/test/shared/primitives/enum_utils_test.dart
@@ -2,7 +2,7 @@
 // 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.
 
-import 'package:devtools_app/src/shared/primitives/enum_utils.dart';
+import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
diff --git a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart
index d00915e..bc5f69d 100644
--- a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart
+++ b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart
@@ -2,23 +2,147 @@
 // 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.
 
+import 'package:devtools_app/src/service/service_manager.dart';
 import 'package:devtools_app/src/shared/feature_flags.dart';
 import 'package:devtools_app/src/shared/globals.dart';
+import 'package:devtools_app_shared/service.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:devtools_test/devtools_test.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
 
 void main() {
   test('constants have expected values', () {
     expect(enableExperiments, false);
     expect(enableBeta, false);
     expect(isExternalBuild, true);
-    expect(FeatureFlags.memoryObserver, true);
-    expect(FeatureFlags.memorySaveLoad, false);
-    expect(FeatureFlags.networkDisconnectExperience, true);
-    expect(FeatureFlags.networkSaveLoad, true);
-    expect(FeatureFlags.devToolsExtensions, isExternalBuild);
-    expect(FeatureFlags.dapDebugging, false);
-    expect(FeatureFlags.inspectorV2, true);
-    expect(FeatureFlags.wasmOptInSetting, true);
-    expect(FeatureFlags.propertyEditorRefactors, true);
+    expect(FeatureFlags.memoryObserver.isEnabled, true);
+    expect(FeatureFlags.memorySaveLoad.isEnabled, false);
+    expect(FeatureFlags.networkSaveLoad.isEnabled, true);
+    expect(FeatureFlags.devToolsExtensions.isEnabled, isExternalBuild);
+    expect(FeatureFlags.dapDebugging.isEnabled, false);
+    expect(FeatureFlags.inspectorV2.isEnabled, true);
+    expect(FeatureFlags.wasmOptInSetting.isEnabled, true);
+    expect(FeatureFlags.propertyEditorRefactors.isEnabled, true);
+  });
+
+  group('FlutterChannelFeatureFlag', () {
+    final connectedApp = MockConnectedApp();
+
+    late FlutterChannelFeatureFlag flag;
+
+    setUp(() {
+      final mockServiceConnection = createMockServiceConnectionWithDefaults();
+      final mockServiceManager =
+          mockServiceConnection.serviceManager as MockServiceManager;
+      when(
+        mockServiceManager.serviceExtensionManager,
+      ).thenReturn(FakeServiceExtensionManager());
+      when(mockServiceManager.connectedApp).thenReturn(connectedApp);
+      setGlobal(ServiceConnectionManager, mockServiceConnection);
+    });
+
+    test('pure Dart app', () {
+      mockConnectedApp(connectedApp, isFlutterApp: false);
+
+      flag = const FlutterChannelFeatureFlag(
+        name: 'test',
+        flutterChannel: FlutterChannel.dev,
+        enabledForDartApps: true,
+        enabledForFlutterAppsFallback: false,
+      );
+      expect(flag.isEnabled(connectedApp), isTrue);
+
+      flag = const FlutterChannelFeatureFlag(
+        name: 'test',
+        flutterChannel: FlutterChannel.dev,
+        enabledForDartApps: false,
+        enabledForFlutterAppsFallback: true,
+      );
+      expect(flag.isEnabled(connectedApp), isFalse);
+    });
+
+    test('Flutter app on an unknown version', () {
+      mockConnectedApp(connectedApp, flutterVersion: 'unknown-version');
+
+      flag = const FlutterChannelFeatureFlag(
+        name: 'test',
+        flutterChannel: FlutterChannel.dev,
+        enabledForDartApps: false,
+        enabledForFlutterAppsFallback: true,
+      );
+      expect(flag.isEnabled(connectedApp), isTrue);
+
+      flag = const FlutterChannelFeatureFlag(
+        name: 'test',
+        flutterChannel: FlutterChannel.dev,
+        enabledForDartApps: true,
+        enabledForFlutterAppsFallback: false,
+      );
+      expect(flag.isEnabled(connectedApp), isFalse);
+    });
+
+    group('Flutter app with version', () {
+      const stableVersion = '2.3.0';
+      const betaVersion = '2.3.0-17.0.pre';
+      const devVersion = '2.3.0-17.0.pre.355';
+
+      void enableFeatureFlagForChannel(FlutterChannel channel) {
+        flag = FlutterChannelFeatureFlag(
+          name: 'test',
+          flutterChannel: channel,
+          enabledForDartApps: false,
+          enabledForFlutterAppsFallback: false,
+        );
+      }
+
+      void expectEnabledForChannels({
+        required bool enabledOnStable,
+        required bool enabledOnBeta,
+        required bool enabledOnDev,
+      }) {
+        // Flutter app using stable channel.
+        mockConnectedApp(connectedApp, flutterVersion: stableVersion);
+        expect(flag.isEnabled(connectedApp), equals(enabledOnStable));
+
+        // Flutter app using beta channel.
+        mockConnectedApp(connectedApp, flutterVersion: betaVersion);
+        expect(flag.isEnabled(connectedApp), equals(enabledOnBeta));
+
+        // Flutter app using dev channel.
+        mockConnectedApp(connectedApp, flutterVersion: devVersion);
+        expect(flag.isEnabled(connectedApp), equals(enabledOnDev));
+      }
+
+      test('enabled on dev', () {
+        enableFeatureFlagForChannel(FlutterChannel.dev);
+
+        expectEnabledForChannels(
+          enabledOnStable: false,
+          enabledOnBeta: false,
+          enabledOnDev: true,
+        );
+      });
+
+      test('enabled on beta', () {
+        enableFeatureFlagForChannel(FlutterChannel.beta);
+
+        expectEnabledForChannels(
+          enabledOnStable: false,
+          enabledOnBeta: true,
+          enabledOnDev: true,
+        );
+      });
+
+      test('enabled on stable', () {
+        enableFeatureFlagForChannel(FlutterChannel.stable);
+
+        expectEnabledForChannels(
+          enabledOnStable: true,
+          enabledOnBeta: true,
+          enabledOnDev: true,
+        );
+      });
+    });
   });
 }
diff --git a/packages/devtools_app/test/shared/ui/vm_flag_widgets_test.dart b/packages/devtools_app/test/shared/ui/vm_flag_widgets_test.dart
index e35bd05..65ad73d 100644
--- a/packages/devtools_app/test/shared/ui/vm_flag_widgets_test.dart
+++ b/packages/devtools_app/test/shared/ui/vm_flag_widgets_test.dart
@@ -211,12 +211,7 @@
     });
 
     testWidgets('builds dialog', (WidgetTester tester) async {
-      mockConnectedApp(
-        fakeServiceConnection.serviceManager.connectedApp!,
-        isFlutterApp: true,
-        isProfileBuild: false,
-        isWebApp: false,
-      );
+      mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!);
 
       await tester.pumpWidget(wrap(const VMFlagsDialog()));
       expect(find.richText('VM Flags'), findsOneWidget);
diff --git a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart
index 2a91132..adf33b1 100644
--- a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart
+++ b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_test.dart
@@ -927,7 +927,7 @@
     );
 
     setUp(() {
-      FeatureFlags.propertyEditorRefactors = true;
+      FeatureFlags.propertyEditorRefactors.setEnabledForTests(true);
 
       refactorCount = 0;
       when(
diff --git a/packages/devtools_app/test/test_infra/scenes/cpu_profiler/default.dart b/packages/devtools_app/test/test_infra/scenes/cpu_profiler/default.dart
index 3bced37..a4ff98d 100644
--- a/packages/devtools_app/test/test_infra/scenes/cpu_profiler/default.dart
+++ b/packages/devtools_app/test/test_infra/scenes/cpu_profiler/default.dart
@@ -48,12 +48,7 @@
       ),
     );
     final app = fakeServiceConnection.serviceManager.connectedApp!;
-    mockConnectedApp(
-      app,
-      isFlutterApp: false,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(app, isFlutterApp: false);
     when(
       fakeServiceConnection.errorBadgeManager.errorCountNotifier('profiler'),
     ).thenReturn(ValueNotifier<int>(0));
diff --git a/packages/devtools_app/test/test_infra/scenes/memory/default.dart b/packages/devtools_app/test/test_infra/scenes/memory/default.dart
index 96d4a6b..1827a0f 100644
--- a/packages/devtools_app/test/test_infra/scenes/memory/default.dart
+++ b/packages/devtools_app/test/test_infra/scenes/memory/default.dart
@@ -131,12 +131,7 @@
       ),
     );
     final app = fakeServiceConnection.serviceManager.connectedApp!;
-    mockConnectedApp(
-      app,
-      isFlutterApp: true,
-      isProfileBuild: true,
-      isWebApp: false,
-    );
+    mockConnectedApp(app, isProfileBuild: true);
     when(
       fakeServiceConnection.serviceManager.vm.operatingSystem,
     ).thenReturn('ios');
diff --git a/packages/devtools_app/test/test_infra/scenes/memory/diff_snapshot.dart b/packages/devtools_app/test/test_infra/scenes/memory/diff_snapshot.dart
index 3702ba8..aa79a9d 100644
--- a/packages/devtools_app/test/test_infra/scenes/memory/diff_snapshot.dart
+++ b/packages/devtools_app/test/test_infra/scenes/memory/diff_snapshot.dart
@@ -38,9 +38,7 @@
     fakeServiceConnection = FakeServiceConnectionManager();
     mockConnectedApp(
       fakeServiceConnection.serviceManager.connectedApp!,
-      isFlutterApp: true,
       isProfileBuild: true,
-      isWebApp: false,
     );
     setGlobal(ServiceConnectionManager, fakeServiceConnection);
 
diff --git a/packages/devtools_app_shared/lib/src/service/flutter_version.dart b/packages/devtools_app_shared/lib/src/service/flutter_version.dart
index 1ab54e8..f2819bb 100644
--- a/packages/devtools_app_shared/lib/src/service/flutter_version.dart
+++ b/packages/devtools_app_shared/lib/src/service/flutter_version.dart
@@ -4,6 +4,8 @@
 
 import 'package:devtools_shared/devtools_shared.dart';
 
+import '../../utils.dart';
+
 /// Flutter version service registered by Flutter Tools.
 ///
 /// We call this service to get version information about the Flutter framework,
@@ -101,6 +103,54 @@
         dartSdkVersion,
       );
 
+  static final _stableVersionRegex = RegExp(r'^\d+\.\d+\.\d+$');
+  static final _isNumericRegex = RegExp(r'\d');
+
+  /// Identifies the Flutter channel from a version string.
+  ///
+  /// This method will first attempt to use [channelStr] if it is provided.
+  /// Otherwise, it will fall back to parsing the channel from [versionStr].
+  ///
+  /// This method will return `null` if the channel cannot be determined from
+  /// the provided information.
+  ///
+  /// Examples of versions that can be parsed:
+  ///  * '2.3.0' -> [FlutterChannel.stable]
+  ///  * '2.3.0-17.0.pre' -> [FlutterChannel.beta]
+  ///  * '2.3.0-17.0.pre.355' -> [FlutterChannel.dev]
+  static FlutterChannel? identifyChannel(
+    String versionStr, {
+    String? channelStr,
+  }) {
+    // Check if channel string is valid. 
+    if (channelStr != null) {
+      final channel = FlutterChannel.fromName(channelStr);
+      if (channel != null) return channel;
+    }
+
+    // Check if version matches stable release format.
+    final sanitized = SemanticVersion.sanitizeVersionStr(versionStr);
+    if (_stableVersionRegex.hasMatch(sanitized)) return FlutterChannel.stable;
+
+    // Check if version matches pre-release format.
+    const preReleaseIndicator = '.pre';
+    final isValidPreRelease = sanitized.contains(preReleaseIndicator);
+    if (!isValidPreRelease) return null;
+
+    // Check if version matches beta release format.
+    if (sanitized.endsWith(preReleaseIndicator)) return FlutterChannel.beta;
+
+    // Check if version matches dev release format.
+    final versionParts = sanitized.split('$preReleaseIndicator.');
+    final suffix = versionParts.last;
+    if (versionParts.length == 2 && _isNumericRegex.hasMatch(suffix)) {
+      return FlutterChannel.dev;
+    }
+
+    // Matches no known release format, return null.
+    return null;
+  }
+
   static SemanticVersion? _parseDartVersion(String? versionString) {
     if (versionString == null) return null;
 
@@ -121,3 +171,18 @@
     return SemanticVersion.parse(rawVersion);
   }
 }
+
+/// An enum representing the different Flutter channels.
+enum FlutterChannel with EnumIndexOrdering {
+  dev,
+  beta,
+  stable;
+
+  static FlutterChannel? fromName(String? name) {
+    try {
+      return FlutterChannel.values.byName(name ?? '');
+    } catch (_) {
+      return null;
+    }
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/primitives/enum_utils.dart b/packages/devtools_app_shared/lib/src/utils/enum_utils.dart
similarity index 100%
rename from packages/devtools_app/lib/src/shared/primitives/enum_utils.dart
rename to packages/devtools_app_shared/lib/src/utils/enum_utils.dart
diff --git a/packages/devtools_app_shared/lib/utils.dart b/packages/devtools_app_shared/lib/utils.dart
index 78fa6b8..3b9957d 100644
--- a/packages/devtools_app_shared/lib/utils.dart
+++ b/packages/devtools_app_shared/lib/utils.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
 
 export 'src/utils/auto_dispose.dart';
+export 'src/utils/enum_utils.dart';
 export 'src/utils/globals.dart';
 export 'src/utils/list.dart';
 export 'src/utils/url/url.dart';
diff --git a/packages/devtools_app_shared/test/service/flutter_version_test.dart b/packages/devtools_app_shared/test/service/flutter_version_test.dart
index 3e38f08..a1904d6 100644
--- a/packages/devtools_app_shared/test/service/flutter_version_test.dart
+++ b/packages/devtools_app_shared/test/service/flutter_version_test.dart
@@ -69,6 +69,129 @@
       expect(flutterVersion.patch, equals(0));
     });
 
+    group('identifies correct Flutter channel', () {
+      test('uses channel string if it exists', () {
+        expect(
+            FlutterVersion.identifyChannel('ignored-version',
+                channelStr: 'dev'),
+            equals(FlutterChannel.dev));
+
+        expect(
+            FlutterVersion.identifyChannel('ignored-version',
+                channelStr: 'beta'),
+            equals(FlutterChannel.beta));
+
+        expect(
+            FlutterVersion.identifyChannel('ignored-version',
+                channelStr: 'stable'),
+            equals(FlutterChannel.stable));
+      });
+
+      test('identifies channel from version string', () {
+        expect(FlutterVersion.identifyChannel('2.3.0-17.0.pre.355'),
+            equals(FlutterChannel.dev));
+
+        expect(FlutterVersion.identifyChannel('2.3.0-17.0.pre-355'),
+            equals(FlutterChannel.dev));
+
+        expect(FlutterVersion.identifyChannel('2.3.0-17.0.pre'),
+            equals(FlutterChannel.beta));
+
+        expect(FlutterVersion.identifyChannel('2.3.0'),
+            equals(FlutterChannel.stable));
+
+        expect(FlutterVersion.identifyChannel('bad-version'), isNull);
+
+        expect(FlutterVersion.identifyChannel('1.10.11-pre42'), isNull);
+
+        expect(FlutterVersion.identifyChannel('2.3.0-17'), isNull);
+      });
+    });
+
+    group('Flutter channel', () {
+      test('channel comparison', () {
+        // FlutterChannel.dev comparison:
+        // - compare to itself:
+        expect(FlutterChannel.dev == FlutterChannel.dev, isTrue);
+        expect(FlutterChannel.dev != FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.dev < FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.dev <= FlutterChannel.dev, isTrue);
+        expect(FlutterChannel.dev > FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.dev >= FlutterChannel.dev, isTrue);
+        // - compare to beta:
+        expect(FlutterChannel.dev == FlutterChannel.beta, isFalse);
+        expect(FlutterChannel.dev != FlutterChannel.beta, isTrue);
+        expect(FlutterChannel.dev < FlutterChannel.beta, isTrue);
+        expect(FlutterChannel.dev <= FlutterChannel.beta, isTrue);
+        expect(FlutterChannel.dev > FlutterChannel.beta, isFalse);
+        expect(FlutterChannel.dev >= FlutterChannel.beta, isFalse);
+        // - compare to stable:
+        expect(FlutterChannel.dev == FlutterChannel.stable, isFalse);
+        expect(FlutterChannel.dev != FlutterChannel.stable, isTrue);
+        expect(FlutterChannel.dev < FlutterChannel.stable, isTrue);
+        expect(FlutterChannel.dev <= FlutterChannel.stable, isTrue);
+        expect(FlutterChannel.dev > FlutterChannel.stable, isFalse);
+        expect(FlutterChannel.dev >= FlutterChannel.stable, isFalse);
+
+        // FlutterChannel.beta comparison:
+        // - compare to dev:
+        expect(FlutterChannel.beta == FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.beta != FlutterChannel.dev, isTrue);
+        expect(FlutterChannel.beta < FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.beta <= FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.beta > FlutterChannel.dev, isTrue);
+        expect(FlutterChannel.beta >= FlutterChannel.dev, isTrue);
+        // - compare to itself:
+        expect(FlutterChannel.beta == FlutterChannel.beta, isTrue);
+        expect(FlutterChannel.beta != FlutterChannel.beta, isFalse);
+        expect(FlutterChannel.beta < FlutterChannel.beta, isFalse);
+        expect(FlutterChannel.beta <= FlutterChannel.beta, isTrue);
+        expect(FlutterChannel.beta > FlutterChannel.beta, isFalse);
+        expect(FlutterChannel.beta >= FlutterChannel.beta, isTrue);
+        // - compare to stable:
+        expect(FlutterChannel.beta == FlutterChannel.stable, isFalse);
+        expect(FlutterChannel.beta != FlutterChannel.stable, isTrue);
+        expect(FlutterChannel.beta < FlutterChannel.stable, isTrue);
+        expect(FlutterChannel.beta <= FlutterChannel.stable, isTrue);
+        expect(FlutterChannel.beta > FlutterChannel.stable, isFalse);
+        expect(FlutterChannel.beta >= FlutterChannel.stable, isFalse);
+
+        // FlutterChannel.stable comparison:
+        // - compare to dev:
+        expect(FlutterChannel.stable == FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.stable != FlutterChannel.dev, isTrue);
+        expect(FlutterChannel.stable < FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.stable <= FlutterChannel.dev, isFalse);
+        expect(FlutterChannel.stable > FlutterChannel.dev, isTrue);
+        expect(FlutterChannel.stable >= FlutterChannel.dev, isTrue);
+        // - compare to beta:
+        expect(FlutterChannel.stable == FlutterChannel.beta, isFalse);
+        expect(FlutterChannel.stable != FlutterChannel.beta, isTrue);
+        expect(FlutterChannel.stable < FlutterChannel.beta, isFalse);
+        expect(FlutterChannel.stable <= FlutterChannel.beta, isFalse);
+        expect(FlutterChannel.stable > FlutterChannel.beta, isTrue);
+        expect(FlutterChannel.stable >= FlutterChannel.beta, isTrue);
+        // - compare to itself:
+        expect(FlutterChannel.stable == FlutterChannel.stable, isTrue);
+        expect(FlutterChannel.stable != FlutterChannel.stable, isFalse);
+        expect(FlutterChannel.stable < FlutterChannel.stable, isFalse);
+        expect(FlutterChannel.stable <= FlutterChannel.stable, isTrue);
+        expect(FlutterChannel.stable > FlutterChannel.stable, isFalse);
+        expect(FlutterChannel.stable >= FlutterChannel.stable, isTrue);
+      });
+
+      test('fromName factory', () {
+        expect(FlutterChannel.fromName('dev'), FlutterChannel.dev);
+        expect(FlutterChannel.fromName('beta'), FlutterChannel.beta);
+        expect(FlutterChannel.fromName('stable'), FlutterChannel.stable);
+
+        expect(FlutterChannel.fromName('DEV'), isNull);
+        expect(FlutterChannel.fromName('unknown'), isNull);
+        expect(FlutterChannel.fromName(''), isNull);
+        expect(FlutterChannel.fromName(null), isNull);
+      });
+    });
+
     test('parses dart version correctly', () {
       var flutterVersion = FlutterVersion.parse({
         'frameworkVersion': '2.8.0',
diff --git a/packages/devtools_shared/lib/src/utils/semantic_version.dart b/packages/devtools_shared/lib/src/utils/semantic_version.dart
index 98d9644..3e8046f 100644
--- a/packages/devtools_shared/lib/src/utils/semantic_version.dart
+++ b/packages/devtools_shared/lib/src/utils/semantic_version.dart
@@ -18,27 +18,8 @@
   factory SemanticVersion.parse(String? versionString) {
     if (versionString == null) return SemanticVersion();
 
-    // Remove any build metadata, denoted by a '+' character and whatever
-    // follows.
-    final buildMetadataIndex = versionString.indexOf('+');
-    if (buildMetadataIndex != -1) {
-      versionString = versionString.substring(0, buildMetadataIndex);
-    }
+    final version = sanitizeVersionStr(versionString);
 
-    // [versionString] is expected to be of the form for VM.version, Dart, and
-    // Flutter, respectively:
-    // 2.15.0-233.0.dev (dev) (Mon Oct 18 14:06:26 2021 -0700) on "ios_x64"
-    // 2.15.0-178.1.beta
-    // 2.6.0-12.0.pre.443
-    // 2.6.0-12.0.pre-443
-    //
-    // First canonicalize the version string to convert any prerelease suffix
-    // with a "-" to a prerelease suffix with a ".".
-    final canonicalized = _canonicalizeVersion(versionString);
-    // Then split on the spaces to the version, and then on the dash char to
-    // separate the main semantic version from the pre release version.
-    final splitOnSpaces = canonicalized.split(' ');
-    final version = splitOnSpaces.first;
     final splitOnDash = version.split('-');
     assert(splitOnDash.length <= 2, 'version: $version');
 
@@ -119,6 +100,33 @@
 
   int? preReleaseMinor;
 
+  /// Sanitizes a version string so that it can be parsed as a
+  /// [SemanticVersion].
+  ///
+  /// This method will remove build metadata and other trailing text from the
+  /// version string. For example:
+  ///
+  /// * '2.15.0-233.0.dev (dev) (Mon Oct 18 14:06:26 2021 -0700) on "ios_x64"' -> '2.15.0-233.0.dev'
+  /// * '2.15.0-178.1.beta' -> '2.15.0-178.1.beta'
+  /// * '2.6.0-12.0.pre.443' -> '2.6.0-12.0.pre.443'
+  /// * '3.0.0+123' -> '3.0.0'
+  static String sanitizeVersionStr(String versionString) {
+    // Remove any build metadata, denoted by a '+' character and whatever
+    // follows.
+    final buildMetadataIndex = versionString.indexOf('+');
+    if (buildMetadataIndex != -1) {
+      versionString = versionString.substring(0, buildMetadataIndex);
+    }
+    // Canonicalize the version string to convert any prerelease suffix with a
+    // "-" to a prerelease suffix with a ".".
+    final canonicalized = _canonicalizeVersion(versionString);
+    // Then split on the spaces to the version, and then on the dash char to
+    // separate the main semantic version from the pre release version.
+    final splitOnSpaces = canonicalized.split(' ');
+    final version = splitOnSpaces.first;
+    return version;
+  }
+
   static final _nonStandardPreReleaseVersionRegex = RegExp(r'(\.pre)-(\d+)$');
 
   /// Canonicalizes a [semanticVersion] with a prerelease version suffix to use
diff --git a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart
index 206eb16..35efaa8 100644
--- a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart
+++ b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart
@@ -102,12 +102,7 @@
        _isolateManager = FakeIsolateManager(rootLibrary: rootLibrary) {
     this.service = service ?? createFakeService();
     serviceUri = this.service!.wsUri;
-    mockConnectedApp(
-      connectedApp!,
-      isFlutterApp: true,
-      isProfileBuild: false,
-      isWebApp: false,
-    );
+    mockConnectedApp(connectedApp!);
     setConnectedState(hasConnection);
 
     when(vm.operatingSystem).thenReturn('macos');
diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart
index acc9c15..d2f2b10 100644
--- a/packages/devtools_test/lib/src/mocks/mocks.dart
+++ b/packages/devtools_test/lib/src/mocks/mocks.dart
@@ -143,10 +143,11 @@
 
 void mockConnectedApp(
   ConnectedApp connectedApp, {
-  required bool isFlutterApp,
-  required bool isProfileBuild,
-  required bool isWebApp,
+  bool isFlutterApp = true,
+  bool isProfileBuild = false,
+  bool isWebApp = false,
   String os = 'ios',
+  String flutterVersion = '2.10.0',
 }) {
   assert(!(!isFlutterApp && isProfileBuild));
 
@@ -163,7 +164,7 @@
     when(connectedApp.flutterVersionNow).thenReturn(
       FlutterVersion.parse({
         'type': 'Success',
-        'frameworkVersion': '2.10.0',
+        'frameworkVersion': flutterVersion,
         'channel': 'unknown',
         'repositoryUrl': 'unknown source',
         'frameworkRevision': '74432fa91c8ffbc555ffc2701309e8729380a012',