Merge branch 'master' into 2.31.1
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index a0df3a4..5b1db18 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -65,7 +65,7 @@
           wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
           echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
           sudo apt-get update
-          sudo apt-get install dcm=1.11.0-1
+          sudo apt-get install dcm=1.13.4-1 # To avoid errors add `-1` (build number) to the version
           sudo chmod +x /usr/bin/dcm
           echo "$(dcm --version)"
       - name: Setup Dart SDK
diff --git a/.github/workflows/daily-dev-bump.yaml b/.github/workflows/daily-dev-bump.yaml
index 7da3763..86e52f2 100644
--- a/.github/workflows/daily-dev-bump.yaml
+++ b/.github/workflows/daily-dev-bump.yaml
@@ -66,7 +66,7 @@
           # Ensure the devtools_tool command is available
           export PATH="$PATH":`pwd`/tool/bin
 
-          ORIGINAL_VERSION=$(devtools_tool update-version current-version)
+          ORIGINAL_VERSION=$(devtools_tool update-version current-version | tail -1)
 
           if [ -z "$UPDATE_TYPE" ]; then
             # If $UPDATE_TYPE is not set, then assume it is dev
@@ -100,7 +100,7 @@
             exit 1
           fi
 
-          NEW_VERSION=$(devtools_tool update-version current-version)
+          NEW_VERSION=$(devtools_tool update-version current-version | tail -1)
 
           echo "COMMIT_MESSAGE=Updating from $ORIGINAL_VERSION to $NEW_VERSION" >> $GITHUB_OUTPUT
         env:
diff --git a/BETA_TESTING.md b/BETA_TESTING.md
index d0270de..57df2d1 100644
--- a/BETA_TESTING.md
+++ b/BETA_TESTING.md
@@ -2,63 +2,92 @@
 
 This page describes the fastest way to build DevTools with the goal to use it. Do not mix this setup with development environment. If you want to make code changes, follow [contributing guidance](https://github.com/flutter/devtools/blob/master/CONTRIBUTING.md).
 
-You may want to build DevTools to:
+You may want to build DevTools locally to:
 
 1. Try experimental features
 
-2. Run desktop version, instead of Web version, to get get rid of browser memory limit. For example,
-to be able to analyze heap snapshots of more complicated applications.
+2. Run DevTools on Flutter Desktop instead of Flutter Web. This will eliminate issues like the browser memory limit, for example, to be able to analyze heap snapshots of large applications.
 
-These steps were tested for Mac and may require adjustments for other platforms. Contributions
-that make the steps more platform agnostic are welcome.
+These steps are tested for Mac and may require adjustments for other platforms. Contributions
+to make these instructions more platform-agnostic are welcome.
 
-## Prerequisites
+## Prerequisites (first time only)
 
-1. [Configure](https://docs.flutter.dev/get-started/install) Dart or Flutter.
+### Set up Dart & Flutter
 
-## Setup DevTools (first time only)
+[Configure](https://docs.flutter.dev/get-started/install) Dart & Flutter on your local machine.
 
-1. In your terminal `cd` to a folder where you want to clone devtools, and that does not have subfolder `devtools` yet.
+After doing so, typing `which flutter` and `which dart` (or `where.exe flutter` and `where.exe dart` for Windows)
+into your terminal should print the path to your Flutter and Dart executables.
 
-2. Clone the repo and update the Flutter SDK that DevTools will be built with:
+### Set up your DevTools environment
 
-```bash
-git clone https://github.com/flutter/devtools.git
+1. Ensure you have a clone of the DevTools repository on your machine. This can be a clone of
+`flutter/devtools` or a clone of a DevTools
+[fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) from your own Github
+account. You may want to fork Devtools to your own Github account if you plan to contribute
+to the project.
 
-bash devtools/tool/update_flutter_sdk.sh
-cd devtools
-```
+    In your terminal, navigate to a directory where you want to clone DevTools: `cd some/directory`.
+    This folder must not already contain a folder named 'devtools'.
+ 
+    **To clone flutter/devtools**:
+    - Clone the DevTools repo: `git clone git@github.com:flutter/devtools.git` 
+        - If you haven't already, you may need to
+    [generate a new SSH key](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh)
+    to connect to Github with SSH.
 
-## Or refresh DevTools
+    **To clone your fork of flutter/devtools**:
+    - [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the DevTools repo to your
+    own Github account.
+    - Clone your fork of the DevTools repo: `git clone git@github.com:your_github_account/devtools.git` 
+        - If you haven't already, you may need to
+    [generate a new SSH key](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh)
+    to connect to Github with SSH.
+    - Make sure to [configure Git to keep your fork in sync](https://docs.github.com/en/get-started/quickstart/fork-a-repo#configuring-git-to-sync-your-fork-with-the-upstream-repository)
+    with the upstream DevTools repo.
 
-If you have already configured the DevTools environment and need to refresh to get the latest DevTools code, follow these instructions:
+2. Ensure that you have access to the `devtools_tool` executable by:
+	- Running `flutter pub get` on the `devtools/tool` directory
+	- Adding the `devtools/tool/bin` folder to your `PATH` environment variable:
+	  - **MacOS Users**
+	    - add the following to your `~/.zshrc` file (or `~/.bashrc`, `~/.bash_profile` if you use Bash),
+		replacing `<DEVTOOLS_DIR>` with the local path to your DevTools repo:
 
-1. `cd` to the `devtools` directory created in the [Setup and start](#setup-and-start) section.
+			```
+			export PATH=$PATH:<DEVTOOLS_DIR>/tool/bin
+			```
+	  - **Windows Users**
+		- Open "Edit environment variables for your account" from Control Panel
+		- Locate the `Path` variable and click **Edit**
+		- Click the **New** button and paste in `<DEVTOOLS_DIR>/tool/bin`, replacing `<DEVTOOLS_DIR>`
+		with the local path to your DevTools repo.
+	
+	Explore the commands and helpers that the `devtools_tool` provides by running `devtools_tool -h`. 
 
-2. Refresh DevTools (it will delete all your local changes!):
+## Prepare to build DevTools
 
+To ensure your DevTools repository is up to date and ready to build, run the following from the
+`devtools` directory (this will delete any local changes you have made to your DevTools clone):
 ```bash
 git checkout master
 git reset --hard origin/master
 
-bash tool/update_flutter_sdk.sh
+devtools_tool update-flutter-sdk
 devtools_tool pub-get --only-main --upgrade
 ```
 
-If some steps failed, remove the directory and redo to [Setup](#setup).
-
 ## Start DevTools and connect to an app
 
-1. From the main devtools directory, run `cd packages/devtools_app``
+1. From the main `devtools/packages/devtools_app` directory, run the following, where
+`<platform>` is one of `chrome`, `macos`, or `windows` depending on which platform you
+are targeting:
+    ```bash
+    ../../tool/flutter-sdk/bin/flutter run --release -d <platform>
+    ```
 
-2. Start DevTools
+    - Add `--dart-define=enable_experiments=true` to enable experimental features.
 
-- On Chrome: `../../tool/flutter-sdk/bin/flutter run --release -d chrome`
-- On Mac: `../../tool/flutter-sdk/bin/flutter run --release -d macos`
-- On Windows: `../../tool/flutter-sdk/bin/flutter run --release -d windows`
-
-Add `--dart-define=enable_experiments=true` to enable experimental features.
-
-3. Paste the URL of your application
-(for example [Gallery](https://github.com/flutter/devtools/blob/master/CONTRIBUTING.md#connect-to-application))
-to the connection textbox.
+2. Run the application that you want to debug or profile with DevTools. 
+3. Paste the VM Service URL of your application into the DevTools connect dialog. See this
+[example](https://github.com/flutter/devtools/blob/master/CONTRIBUTING.md#connect-devtools-to-a-test-application).
diff --git a/CODEOWNERS b/CODEOWNERS
index 7b4c121..34bd959 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -27,9 +27,11 @@
 # Tooling files
 /tool/              @CoderDake
 
+packages/devtools_extensions/    @kenzieschmoll
+
+
 # Version bump files.
 # No owners to prevent version update spam
 packages/devtools_app/lib/devtools.dart
 packages/devtools_app/pubspec.yaml
-packages/devtools_shared/pubspec.yaml
 packages/devtools_test/pubspec.yaml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 68e5a42..8989185 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -59,8 +59,8 @@
 2. Make sure to [configure Git to keep your fork in sync](https://docs.github.com/en/get-started/quickstart/fork-a-repo#configuring-git-to-sync-your-fork-with-the-upstream-repository)
 with the upstream DevTools repo.
 3. Ensure that you have access to the `devtools_tool` executable by:
-	- Running `flutter pub get` on the `tool` directory
-	- Adding the `tool/bin` folder to your `PATH` environment variable:
+	- Running `flutter pub get` on the `devtools/tool` directory
+	- Adding the `devtools/tool/bin` folder to your `PATH` environment variable:
 	  - **MacOS Users**
 	    - add the following to your `~/.zshrc` file (or `~/.bashrc`, `~/.bash_profile` if you use Bash),
 		replacing `<DEVTOOLS_DIR>` with the local path to your DevTools repo:
diff --git a/STYLE.md b/STYLE.md
index d793b6c..eb66ef3 100644
--- a/STYLE.md
+++ b/STYLE.md
@@ -23,3 +23,10 @@
 ## Overriding equality
 
 Use [boilerplaite](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#common-boilerplates-for-operator--and-hashcode).
+
+## Text styles
+
+The default text style for DevTools is `Theme.of(context).regularTextStyle`. The default
+value for `Theme.of(context).bodyMedium` is equivalent to `Theme.of(context).regularTextStyle`.
+
+When creating a `Text` widget, this is the default style that will be applied.
\ No newline at end of file
diff --git a/bin/repo_tool b/bin/repo_tool
deleted file mode 100755
index 8be0904..0000000
--- a/bin/repo_tool
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-
-set -e
-
-REPO_DIR=$(dirname $(dirname $BASH_SOURCE))
-
-# Potentially run flutter pub get
-if [[ "$REPO_DIR/tool/pubspec.yaml" -nt "$REPO_DIR/tool/.packages" ]]; then
-  echo Running flutter pub get...
-  (cd $REPO_DIR/tool; flutter pub get)
-fi
-
-$REPO_DIR/flutter-sdk/bin/dart $REPO_DIR/tool/bin/repo_tool.dart "$@"
diff --git a/case_study/code_size/optimized/code_size_images/README.md b/case_study/code_size/optimized/code_size_images/README.md
index 9e27825..6b0ed8d 100644
--- a/case_study/code_size/optimized/code_size_images/README.md
+++ b/case_study/code_size/optimized/code_size_images/README.md
@@ -1,16 +1,3 @@
 # code_size_images
 
-A flutter project demonstrating code size issues with images
-
-## Getting Started
-
-This project is a starting point for a Flutter application.
-
-A few resources to get you started if this is your first Flutter project:
-
-- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
-
-For help getting started with Flutter, view our
-[online documentation](https://flutter.dev/docs), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+A Flutter project demonstrating code size issues with images
diff --git a/case_study/code_size/optimized/code_size_images/lib/main.dart b/case_study/code_size/optimized/code_size_images/lib/main.dart
index 5ab430e..77a7e66 100644
--- a/case_study/code_size/optimized/code_size_images/lib/main.dart
+++ b/case_study/code_size/optimized/code_size_images/lib/main.dart
@@ -25,7 +25,7 @@
   const MyHomePage({Key? key}) : super(key: key);
 
   @override
-  _MyHomePageState createState() => _MyHomePageState();
+  State<MyHomePage> createState() => _MyHomePageState();
 }
 
 class _MyHomePageState extends State<MyHomePage> {
diff --git a/case_study/code_size/optimized/code_size_images/pubspec.yaml b/case_study/code_size/optimized/code_size_images/pubspec.yaml
index acc8555..5d0b4a5 100644
--- a/case_study/code_size/optimized/code_size_images/pubspec.yaml
+++ b/case_study/code_size/optimized/code_size_images/pubspec.yaml
@@ -1,24 +1,9 @@
 name: code_size_images
-description: A flutter project demonstrating code size issues with images
-
-# The following line prevents the package from being accidentally published to
-# pub.dev using `pub publish`. This is preferred for private packages.
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
-
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 1.0.0+1
+description: A Flutter project demonstrating code size issues with images.
+publish_to: none
 
 environment:
-  sdk: '>=2.12.0 <4.0.0'
+  sdk: ^3.0.0
 
 dependencies:
   flutter:
@@ -28,38 +13,7 @@
   flutter_test:
     sdk: flutter
 
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter.
 flutter:
   uses-material-design: true
-  # The assets are compressed in this version of code_size_images.
   assets:
   - assets/
-
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.dev/assets-and-images/#resolution-aware.
-
-  # For details regarding adding assets from package dependencies, see
-  # https://flutter.dev/assets-and-images/#from-packages
-
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
-  #
-  # For details regarding fonts from package dependencies,
-  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/case_study/code_size/optimized/code_size_package/README.md b/case_study/code_size/optimized/code_size_package/README.md
index 19062af..f99ad9c 100644
--- a/case_study/code_size/optimized/code_size_package/README.md
+++ b/case_study/code_size/optimized/code_size_package/README.md
@@ -1,16 +1,3 @@
 # code_size_package
 
-A flutter project demonstrating code size issues with packages.
-
-## Getting Started
-
-This project is a starting point for a Flutter application.
-
-A few resources to get you started if this is your first Flutter project:
-
-- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
-
-For help getting started with Flutter, view our
-[online documentation](https://flutter.dev/docs), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+A Flutter project demonstrating code size issues with packages.
diff --git a/case_study/code_size/optimized/code_size_package/lib/main.dart b/case_study/code_size/optimized/code_size_package/lib/main.dart
index e3e9ad3..0be9c94 100644
--- a/case_study/code_size/optimized/code_size_package/lib/main.dart
+++ b/case_study/code_size/optimized/code_size_package/lib/main.dart
@@ -27,7 +27,7 @@
   final String title;
 
   @override
-  _MyHomePageState createState() => _MyHomePageState();
+  State<MyHomePage> createState() => _MyHomePageState();
 }
 
 class _MyHomePageState extends State<MyHomePage> {
diff --git a/case_study/code_size/optimized/code_size_package/pubspec.yaml b/case_study/code_size/optimized/code_size_package/pubspec.yaml
index a24ef0b..e9d00b8 100644
--- a/case_study/code_size/optimized/code_size_package/pubspec.yaml
+++ b/case_study/code_size/optimized/code_size_package/pubspec.yaml
@@ -1,12 +1,9 @@
 name: code_size_package
-description: A new Flutter project.
-
-publish_to: "none"
-
-version: 1.0.0+1
+description: A Flutter project demonstrating code size issues with packages.
+publish_to: none
 
 environment:
-  sdk: ">=2.12.0 <3.0.0"
+  sdk: ^3.0.0
 
 dependencies:
   flutter:
diff --git a/case_study/code_size/unoptimized/code_size_images/README.md b/case_study/code_size/unoptimized/code_size_images/README.md
index 9e27825..6b0ed8d 100644
--- a/case_study/code_size/unoptimized/code_size_images/README.md
+++ b/case_study/code_size/unoptimized/code_size_images/README.md
@@ -1,16 +1,3 @@
 # code_size_images
 
-A flutter project demonstrating code size issues with images
-
-## Getting Started
-
-This project is a starting point for a Flutter application.
-
-A few resources to get you started if this is your first Flutter project:
-
-- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
-
-For help getting started with Flutter, view our
-[online documentation](https://flutter.dev/docs), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+A Flutter project demonstrating code size issues with images
diff --git a/case_study/code_size/unoptimized/code_size_images/lib/main.dart b/case_study/code_size/unoptimized/code_size_images/lib/main.dart
index 5ab430e..77a7e66 100644
--- a/case_study/code_size/unoptimized/code_size_images/lib/main.dart
+++ b/case_study/code_size/unoptimized/code_size_images/lib/main.dart
@@ -25,7 +25,7 @@
   const MyHomePage({Key? key}) : super(key: key);
 
   @override
-  _MyHomePageState createState() => _MyHomePageState();
+  State<MyHomePage> createState() => _MyHomePageState();
 }
 
 class _MyHomePageState extends State<MyHomePage> {
diff --git a/case_study/code_size/unoptimized/code_size_images/pubspec.yaml b/case_study/code_size/unoptimized/code_size_images/pubspec.yaml
index 66af136..90b024f 100644
--- a/case_study/code_size/unoptimized/code_size_images/pubspec.yaml
+++ b/case_study/code_size/unoptimized/code_size_images/pubspec.yaml
@@ -1,24 +1,9 @@
 name: code_size_images
-description: A flutter project demonstrating code size issues with images
-
-# The following line prevents the package from being accidentally published to
-# pub.dev using `pub publish`. This is preferred for private packages.
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
-
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 1.0.0+1
+description: A Flutter project demonstrating code size issues with images.
+publish_to: none
 
 environment:
-  sdk: '>=2.12.0 <4.0.0'
+  sdk: ^3.0.0
 
 dependencies:
   flutter:
@@ -28,38 +13,8 @@
   flutter_test:
     sdk: flutter
 
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter.
 flutter:
   uses-material-design: true
   # The assets are not compressed in this version of code_size_images.
   assets:
-  - assets/
-
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.dev/assets-and-images/#resolution-aware.
-
-  # For details regarding adding assets from package dependencies, see
-  # https://flutter.dev/assets-and-images/#from-packages
-
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
-  #
-  # For details regarding fonts from package dependencies,
-  # see https://flutter.dev/custom-fonts/#from-packages
+    - assets/
diff --git a/case_study/code_size/unoptimized/code_size_package/README.md b/case_study/code_size/unoptimized/code_size_package/README.md
index 19062af..f99ad9c 100644
--- a/case_study/code_size/unoptimized/code_size_package/README.md
+++ b/case_study/code_size/unoptimized/code_size_package/README.md
@@ -1,16 +1,3 @@
 # code_size_package
 
-A flutter project demonstrating code size issues with packages.
-
-## Getting Started
-
-This project is a starting point for a Flutter application.
-
-A few resources to get you started if this is your first Flutter project:
-
-- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
-
-For help getting started with Flutter, view our
-[online documentation](https://flutter.dev/docs), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+A Flutter project demonstrating code size issues with packages.
diff --git a/case_study/code_size/unoptimized/code_size_package/lib/main.dart b/case_study/code_size/unoptimized/code_size_package/lib/main.dart
index cd500f5..b2bda32 100644
--- a/case_study/code_size/unoptimized/code_size_package/lib/main.dart
+++ b/case_study/code_size/unoptimized/code_size_package/lib/main.dart
@@ -24,7 +24,7 @@
   final String title;
 
   @override
-  _MyHomePageState createState() => _MyHomePageState();
+  State<MyHomePage> createState() => _MyHomePageState();
 }
 
 class _MyHomePageState extends State<MyHomePage> {
diff --git a/case_study/code_size/unoptimized/code_size_package/pubspec.yaml b/case_study/code_size/unoptimized/code_size_package/pubspec.yaml
index a02d9af..dc50089 100644
--- a/case_study/code_size/unoptimized/code_size_package/pubspec.yaml
+++ b/case_study/code_size/unoptimized/code_size_package/pubspec.yaml
@@ -1,12 +1,9 @@
 name: code_size_package
-description: A new Flutter project.
-
-publish_to: "none"
-
-version: 1.0.0+1
+description: A Flutter project demonstrating code size issues with packages.
+publish_to: none
 
 environment:
-  sdk: '>=2.12.0 <4.0.0'
+  sdk: ^3.0.0
 
 dependencies:
   flutter:
diff --git a/case_study/memory_leaks/images_1/README.md b/case_study/memory_leaks/images_1/README.md
index 7eeaf53..8e0f0b2 100644
--- a/case_study/memory_leaks/images_1/README.md
+++ b/case_study/memory_leaks/images_1/README.md
@@ -32,7 +32,7 @@
 
 <img src="readme_images/snapshot.png" />
 
-When complete a Heat Map will appear.  Turn off the Heat Map swtich:
+When complete a Heat Map will appear.  Turn off the Heat Map switch:
 
 <img src="readme_images/heatmap_off.png" />
 
@@ -44,7 +44,7 @@
 
 <img src="readme_images/analyze.png" />
 
-After the snashot analysis a child row inside of > Analysis will be added titled "Snapshot ..." with the timetime stamp of the snapshot e.g., "Snapshot Jun 09 12:23:44".
+After the snapshot analysis a child row inside of > Analysis will be added titled "Snapshot ..." with the timestamp of the snapshot e.g., "Snapshot Jun 09 12:23:44".
 
 <img src="readme_images/analysis_1.png" />
 
@@ -153,7 +153,3 @@
 Read [[Image.network Documentation](https://api.flutter.dev/flutter/widgets/Image/Image.network.html)].
 
 <img src="readme_images/leak_app.png" height="600em" />
-
-## Getting Started
-
-For help getting started with Flutter, view online [documentation](http://flutter.io/).
diff --git a/case_study/memory_leaks/images_1/analysis_options.yaml b/case_study/memory_leaks/images_1/analysis_options.yaml
index 61b6c4d..f9b3034 100644
--- a/case_study/memory_leaks/images_1/analysis_options.yaml
+++ b/case_study/memory_leaks/images_1/analysis_options.yaml
@@ -1,29 +1 @@
-# This file configures the analyzer, which statically analyzes Dart code to
-# check for errors, warnings, and lints.
-#
-# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
-# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
-# invoked from the command line by running `flutter analyze`.
-
-# The following line activates a set of recommended lints for Flutter apps,
-# packages, and plugins designed to encourage good coding practices.
 include: package:flutter_lints/flutter.yaml
-
-linter:
-  # The lint rules applied to this project can be customized in the
-  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
-  # included above or to enable additional rules. A list of all available lints
-  # and their documentation is published at
-  # https://dart-lang.github.io/linter/lints/index.html.
-  #
-  # Instead of disabling a lint rule for the entire project in the
-  # section below, it can also be suppressed for a single line of code
-  # or a specific dart file by using the `// ignore: name_of_lint` and
-  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
-  # producing the lint.
-  rules:
-    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
-    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/case_study/memory_leaks/images_1/lib/main.dart b/case_study/memory_leaks/images_1/lib/main.dart
index 2854ad2..80c64fe 100644
--- a/case_study/memory_leaks/images_1/lib/main.dart
+++ b/case_study/memory_leaks/images_1/lib/main.dart
@@ -10,7 +10,7 @@
 }
 
 class MyApp extends StatelessWidget {
-  const MyApp({Key? key}) : super(key: key);
+  const MyApp({super.key});
   // This widget is the root of your application.
 
   @override
@@ -29,12 +29,12 @@
 }
 
 class MyHomePage extends StatefulWidget {
-  const MyHomePage({Key? key, this.title = ""}) : super(key: key);
+  const MyHomePage({super.key, this.title = ""});
 
   final String title;
 
   @override
-  MyHomePageState createState() => MyHomePageState();
+  State<MyHomePage> createState() => MyHomePageState();
 }
 
 int globalObjectId = 0;
@@ -97,7 +97,7 @@
 
     developer.log('$loadingState, $imageUrl');
 
-    // Progress spinnger.
+    // Progress spinner.
     return Center(
       child: CircularProgressIndicator(
         value: expectedTotalBytes != null
@@ -176,7 +176,7 @@
 }
 
 class SecondScreen extends MyHomePage {
-  const SecondScreen({Key? key}) : super(key: key);
+  const SecondScreen({super.key});
 }
 
 // Loaded images. key is ImageUrl and value is the ImageChunkEvent (total and cumulative bytes loaded).
diff --git a/case_study/memory_leaks/images_1/pubspec.yaml b/case_study/memory_leaks/images_1/pubspec.yaml
index aff28fc..bcd5928 100644
--- a/case_study/memory_leaks/images_1/pubspec.yaml
+++ b/case_study/memory_leaks/images_1/pubspec.yaml
@@ -1,31 +1,11 @@
 name: memory_leaks_images
-description: A new Flutter project.
+description: Flutter image memory leak example for a DevTools case study.
 
-# The following line prevents the package from being accidentally published to
-# pub.dev using `flutter pub publish`. This is preferred for private packages.
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
-
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 1.0.0+1
+publish_to: none
 
 environment:
-  sdk: ">=2.17.0-182.2.beta <3.0.0"
+  sdk: ^3.0.0
 
-# Dependencies specify other packages that your package needs in order to work.
-# To automatically upgrade your package dependencies to the latest versions
-# consider running `flutter pub upgrade --major-versions`. Alternatively,
-# dependencies can be manually updated by changing the version numbers below to
-# the latest version available on pub.dev. To see which dependencies have newer
-# versions available, run `flutter pub outdated`.
 dependencies:
   flutter:
     sdk: flutter
@@ -33,52 +13,7 @@
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  flutter_lints: ^3.0.1
 
-  # The "flutter_lints" package below contains a set of recommended lints to
-  # encourage good coding practices. The lint set provided by the package is
-  # activated in the `analysis_options.yaml` file located at the root of your
-  # package. See that file for information about deactivating specific lint
-  # rules and activating additional ones.
-  flutter_lints: ^1.0.0
-
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter packages.
 flutter:
-
-  # The following line ensures that the Material Icons font is
-  # included with your application, so that you can use the icons in
-  # the material Icons class.
   uses-material-design: true
-
-  # To add assets to your application, add an assets section, like this:
-  # assets:
-  #   - images/a_dot_burr.jpeg
-  #   - images/a_dot_ham.jpeg
-
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.dev/assets-and-images/#resolution-aware.
-
-  # For details regarding adding assets from package dependencies, see
-  # https://flutter.dev/assets-and-images/#from-packages
-
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
-  #
-  # For details regarding fonts from package dependencies,
-  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/case_study/memory_leaks/images_1/web/README.md b/case_study/memory_leaks/images_1/web/README.md
index 7eeaf53..05bf1b6 100644
--- a/case_study/memory_leaks/images_1/web/README.md
+++ b/case_study/memory_leaks/images_1/web/README.md
@@ -32,7 +32,7 @@
 
 <img src="readme_images/snapshot.png" />
 
-When complete a Heat Map will appear.  Turn off the Heat Map swtich:
+When complete a Heat Map will appear.  Turn off the Heat Map switch:
 
 <img src="readme_images/heatmap_off.png" />
 
@@ -44,7 +44,7 @@
 
 <img src="readme_images/analyze.png" />
 
-After the snashot analysis a child row inside of > Analysis will be added titled "Snapshot ..." with the timetime stamp of the snapshot e.g., "Snapshot Jun 09 12:23:44".
+After the snapshot analysis a child row inside of > Analysis will be added titled "Snapshot ..." with the timestamp of the snapshot e.g., "Snapshot Jun 09 12:23:44".
 
 <img src="readme_images/analysis_1.png" />
 
diff --git a/case_study/memory_leaks/leaking_counter_1/analysis_options.yaml b/case_study/memory_leaks/leaking_counter_1/analysis_options.yaml
index 61b6c4d..f9b3034 100644
--- a/case_study/memory_leaks/leaking_counter_1/analysis_options.yaml
+++ b/case_study/memory_leaks/leaking_counter_1/analysis_options.yaml
@@ -1,29 +1 @@
-# This file configures the analyzer, which statically analyzes Dart code to
-# check for errors, warnings, and lints.
-#
-# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
-# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
-# invoked from the command line by running `flutter analyze`.
-
-# The following line activates a set of recommended lints for Flutter apps,
-# packages, and plugins designed to encourage good coding practices.
 include: package:flutter_lints/flutter.yaml
-
-linter:
-  # The lint rules applied to this project can be customized in the
-  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
-  # included above or to enable additional rules. A list of all available lints
-  # and their documentation is published at
-  # https://dart-lang.github.io/linter/lints/index.html.
-  #
-  # Instead of disabling a lint rule for the entire project in the
-  # section below, it can also be suppressed for a single line of code
-  # or a specific dart file by using the `// ignore: name_of_lint` and
-  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
-  # producing the lint.
-  rules:
-    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
-    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/case_study/memory_leaks/leaking_counter_1/pubspec.yaml b/case_study/memory_leaks/leaking_counter_1/pubspec.yaml
index 1d99c05..796dbc2 100644
--- a/case_study/memory_leaks/leaking_counter_1/pubspec.yaml
+++ b/case_study/memory_leaks/leaking_counter_1/pubspec.yaml
@@ -1,33 +1,11 @@
 name: leaking_counter_1
 description: A new Flutter project.
 
-# The following line prevents the package from being accidentally published to
-# pub.dev using `flutter pub publish`. This is preferred for private packages.
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
-
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-# In Windows, build-name is used as the major, minor, and patch parts
-# of the product and file versions while build-number is used as the build suffix.
-version: 1.0.0+1
+publish_to: none
 
 environment:
-  sdk: '>=2.18.0-271.7.beta <4.0.0'
+  sdk: ^3.0.0
 
-# Dependencies specify other packages that your package needs in order to work.
-# To automatically upgrade your package dependencies to the latest versions
-# consider running `flutter pub upgrade --major-versions`. Alternatively,
-# dependencies can be manually updated by changing the version numbers below to
-# the latest version available on pub.dev. To see which dependencies have newer
-# versions available, run `flutter pub outdated`.
 dependencies:
   flutter:
     sdk: flutter
@@ -35,52 +13,7 @@
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  flutter_lints: ^3.0.1
 
-  # The "flutter_lints" package below contains a set of recommended lints to
-  # encourage good coding practices. The lint set provided by the package is
-  # activated in the `analysis_options.yaml` file located at the root of your
-  # package. See that file for information about deactivating specific lint
-  # rules and activating additional ones.
-  flutter_lints: ^2.0.0
-
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter packages.
 flutter:
-
-  # The following line ensures that the Material Icons font is
-  # included with your application, so that you can use the icons in
-  # the material Icons class.
   uses-material-design: true
-
-  # To add assets to your application, add an assets section, like this:
-  # assets:
-  #   - images/a_dot_burr.jpeg
-  #   - images/a_dot_ham.jpeg
-
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.dev/assets-and-images/#resolution-aware
-
-  # For details regarding adding assets from package dependencies, see
-  # https://flutter.dev/assets-and-images/#from-packages
-
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
-  #
-  # For details regarding fonts from package dependencies,
-  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/case_study/memory_leaks/memory_leak_app/README.md b/case_study/memory_leaks/memory_leak_app/README.md
index acaef7c..78cdfa2 100644
--- a/case_study/memory_leaks/memory_leak_app/README.md
+++ b/case_study/memory_leaks/memory_leak_app/README.md
@@ -2,11 +2,6 @@
 
 A material design widget that displays a horizontal row of tabs.
 
-Read [[Documentation](https://docs.flutter.io/flutter/material/TabBar-class.html)] [[Material Design Spec](https://material.io/guidelines/components/tabs.html)]
+Read ([Documentation](https://api.flutter.dev/flutter/material/TabBar-class.html)) ([Material Design Spec](https://m2.material.io/components/tabs))
 
 <img src="demo_img.gif" height="600em" />
-
-
-## Getting Started
-
-For help getting started with Flutter, view online [documentation](http://flutter.io/).
\ No newline at end of file
diff --git a/case_study/memory_leaks/memory_leak_app/lib/about.dart b/case_study/memory_leaks/memory_leak_app/lib/about.dart
index 14a83d0..113ab2b 100644
--- a/case_study/memory_leaks/memory_leak_app/lib/about.dart
+++ b/case_study/memory_leaks/memory_leak_app/lib/about.dart
@@ -8,7 +8,7 @@
 
 class About extends StatefulWidget {
   @override
-  AboutState createState() => AboutState();
+  State<About> createState() => AboutState();
 }
 
 class AboutState extends State<About> {
diff --git a/case_study/memory_leaks/memory_leak_app/lib/logging.dart b/case_study/memory_leaks/memory_leak_app/lib/logging.dart
index a9fe62f..7444fd6 100644
--- a/case_study/memory_leaks/memory_leak_app/lib/logging.dart
+++ b/case_study/memory_leaks/memory_leak_app/lib/logging.dart
@@ -110,11 +110,11 @@
 
   void _updateLog() {
     now = DateTime.now();
-    final _year = now.year;
+    final year = now.year;
 
     /// Due to a bug, need to verify the date has the current year before
     /// returning a date and time.
-    if (_year < 2019) {
+    if (year < 2019) {
       return;
     }
 
diff --git a/case_study/memory_leaks/memory_leak_app/lib/main.dart b/case_study/memory_leaks/memory_leak_app/lib/main.dart
index 6115b75..84d58a2 100644
--- a/case_study/memory_leaks/memory_leak_app/lib/main.dart
+++ b/case_study/memory_leaks/memory_leak_app/lib/main.dart
@@ -27,7 +27,7 @@
 
 class MyHome extends StatefulWidget {
   @override
-  MyHomeState createState() => MyHomeState();
+  State<MyHome> createState() => MyHomeState();
 }
 
 /// Setup Tabs
diff --git a/case_study/memory_leaks/memory_leak_app/lib/restful_servers.dart b/case_study/memory_leaks/memory_leak_app/lib/restful_servers.dart
index f3ed728..c4bcce7 100644
--- a/case_study/memory_leaks/memory_leak_app/lib/restful_servers.dart
+++ b/case_study/memory_leaks/memory_leak_app/lib/restful_servers.dart
@@ -2,7 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-/// All Restful Servers are defined here.
+/// The RESTful APIs used by the app.
+library;
 
 /// All servers with RestfulAPI implement this base.
 abstract class RestfulAPI {
@@ -21,7 +22,7 @@
 class StarWars extends RestfulAPI {
   StarWars([String name = starWarsPeople]) {
     _defaultUri = _friendlyNames[name]!;
-    _activefriendlyName = name;
+    _activeFriendlyName = name;
   }
 
   static const String starWarsFilms = 'StarWars Films';
@@ -40,13 +41,13 @@
     '$starWarsVehicles': 'https://swapi.co/api/vehicles',
   };
 
-  late final String _activefriendlyName;
+  late final String _activeFriendlyName;
   late final String _defaultUri;
 
   static List<String> get friendlyNames => _friendlyNames.keys.toList();
 
   @override
-  String get activeFriendlyName => _activefriendlyName;
+  String get activeFriendlyName => _activeFriendlyName;
 
   @override
   String uri() => _defaultUri;
@@ -93,7 +94,7 @@
   String state = '???';
 }
 
-/// openewathermap APIs
+/// OpenWeatherMap APIs
 ///
 ///   Docs on Restful APIs https://openweathermap.org/current#data
 ///
diff --git a/case_study/memory_leaks/memory_leak_app/lib/tabs/http_data.dart b/case_study/memory_leaks/memory_leak_app/lib/tabs/http_data.dart
index 53988af..fbecb2a 100644
--- a/case_study/memory_leaks/memory_leak_app/lib/tabs/http_data.dart
+++ b/case_study/memory_leaks/memory_leak_app/lib/tabs/http_data.dart
@@ -16,7 +16,7 @@
 /// Create a stateful widget
 class MyGetHttpData extends StatefulWidget {
   @override
-  MyGetHttpDataState createState() => MyGetHttpDataState();
+  State<MyGetHttpData> createState() => MyGetHttpDataState();
 }
 
 // Create the state for our stateful widget
diff --git a/case_study/memory_leaks/memory_leak_app/lib/tabs/logger.dart b/case_study/memory_leaks/memory_leak_app/lib/tabs/logger.dart
index f311695..1c13d5e 100644
--- a/case_study/memory_leaks/memory_leak_app/lib/tabs/logger.dart
+++ b/case_study/memory_leaks/memory_leak_app/lib/tabs/logger.dart
@@ -14,7 +14,7 @@
   final Logging _logging = Logging.logging;
 
   @override
-  LoggerState createState() => LoggerState(_logging);
+  State<Logger> createState() => LoggerState(_logging);
 }
 
 class LoggerState extends State<Logger> {
diff --git a/case_study/memory_leaks/memory_leak_app/lib/tabs/settings.dart b/case_study/memory_leaks/memory_leak_app/lib/tabs/settings.dart
index d286620..3b27adb 100644
--- a/case_study/memory_leaks/memory_leak_app/lib/tabs/settings.dart
+++ b/case_study/memory_leaks/memory_leak_app/lib/tabs/settings.dart
@@ -20,7 +20,7 @@
   final RestfulAPI restfulRoot;
 
   @override
-  SettingsState createState() {
+  State<Settings> createState() {
     state = SettingsState();
     return state;
   }
diff --git a/case_study/memory_leaks/memory_leak_app/pubspec.yaml b/case_study/memory_leaks/memory_leak_app/pubspec.yaml
index 7dbc3fd..18055fa 100644
--- a/case_study/memory_leaks/memory_leak_app/pubspec.yaml
+++ b/case_study/memory_leaks/memory_leak_app/pubspec.yaml
@@ -1,62 +1,18 @@
 name: memory_leak
-description: A new Flutter project.
+description: Flutter memory leak example app to serve as a DevTools case study.
 
 environment:
-  sdk: '>=2.15.0 <4.0.0'
+  sdk: ^3.0.0
 
 dependencies:
   flutter:
     sdk: flutter
+  http: ^1.1.0
+  intl: any
 
 dev_dependencies:
   flutter_test:
     sdk: flutter
-  intl: ^0.18.0
-  http: ^0.13.5
 
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter.
 flutter:
-
-  # The following line ensures that the Material Icons font is
-  # included with your application, so that you can use the icons in
-  # the Icons class.
   uses-material-design: true
-
-  # To add assets to your application, add an assets section, like this:
-  # assets:
-  #  - images/a_dot_burr.jpeg
-  #  - images/a_dot_ham.jpeg
-
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.io/assets-and-images/.
-
-  # To add assets from package dependencies, first ensure the asset
-  # is in the lib/ directory of the dependency. Then,
-  # refer to the asset with a path prefixed with
-  # `packages/PACKAGE_NAME/`. The `lib/` is implied, do not
-  # include `lib/` in the asset path.
-  #
-  # Here is an example:
-  #
-  # assets:
-  #  - packages/PACKAGE_NAME/path/to/asset
-
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
diff --git a/case_study/platform_channel/README.md b/case_study/platform_channel/README.md
index 52cbe80..e234f35 100644
--- a/case_study/platform_channel/README.md
+++ b/case_study/platform_channel/README.md
@@ -1,16 +1,3 @@
 # platform_channel
 
 A Flutter example that uses a platform channel
-
-## Getting Started
-
-This project is a starting point for a Flutter application.
-
-A few resources to get you started if this is your first Flutter project:
-
-- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
-
-For help getting started with Flutter, view our
-[online documentation](https://flutter.dev/docs), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
diff --git a/case_study/platform_channel/lib/channel_demo.dart b/case_study/platform_channel/lib/channel_demo.dart
index 1aa9cce..2312175 100644
--- a/case_study/platform_channel/lib/channel_demo.dart
+++ b/case_study/platform_channel/lib/channel_demo.dart
@@ -5,7 +5,7 @@
 
 class ChannelDemo extends StatefulWidget {
   @override
-  _ChannelDemoState createState() => _ChannelDemoState();
+  State<ChannelDemo> createState() => _ChannelDemoState();
 }
 
 class _ChannelDemoState extends State<ChannelDemo> {
diff --git a/case_study/platform_channel/pubspec.yaml b/case_study/platform_channel/pubspec.yaml
index 389d5d5..cc5d459 100644
--- a/case_study/platform_channel/pubspec.yaml
+++ b/case_study/platform_channel/pubspec.yaml
@@ -1,20 +1,10 @@
 name: platform_channel
-description: A Flutter example that uses a platform channel
+description: A Flutter example that uses a platform channel.
 
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 version: 1.0.0+1
 
 environment:
-  sdk: '>=2.15.0 <4.0.0'
+  sdk: ^3.0.0
 
 dependencies:
   flutter:
@@ -24,45 +14,5 @@
   flutter_test:
     sdk: flutter
 
-
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter.
 flutter:
-
-  # The following line ensures that the Material Icons font is
-  # included with your application, so that you can use the icons in
-  # the material Icons class.
   uses-material-design: true
-
-  # To add assets to your application, add an assets section, like this:
-  # assets:
-  #  - images/a_dot_burr.jpeg
-  #  - images/a_dot_ham.jpeg
-
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.dev/assets-and-images/#resolution-aware.
-
-  # For details regarding adding assets from package dependencies, see
-  # https://flutter.dev/assets-and-images/#from-packages
-
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
-  #
-  # For details regarding fonts from package dependencies,
-  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/flutter-candidate.txt b/flutter-candidate.txt
index b9c121b..6998c44 100644
--- a/flutter-candidate.txt
+++ b/flutter-candidate.txt
@@ -1 +1 @@
-3.18.0-17.0.pre
+3.20.0-1.0.pre
diff --git a/packages/.vscode/extensions.json b/packages/.vscode/extensions.json
new file mode 100644
index 0000000..8481f95
--- /dev/null
+++ b/packages/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+    "recommendations": [
+        "streetsidesoftware.code-spell-checker"
+    ]
+}
diff --git a/packages/analysis_options.yaml b/packages/analysis_options.yaml
index 07c7bbd..9682889 100644
--- a/packages/analysis_options.yaml
+++ b/packages/analysis_options.yaml
@@ -7,8 +7,6 @@
   errors:
     # treat missing required parameters as a warning (not a hint)
     missing_required_param: warning
-    # treat missing returns as a warning (not a hint)
-    missing_return: warning
   exclude:
     - build/**
     - '**.freezed.dart'
@@ -220,12 +218,11 @@
     #    allowed-duplicated-chains: 2
     # - prefer-static-class
     - prefer-trailing-comma
-    - tag-name
     - always-remove-listener
     # - avoid-border-all Micro-optimization to avoid a const constructor.
     # - avoid-returning-widgets  This one is nice but has a lot of false positives.
     - avoid-shrink-wrap-in-lists
-    - avoid-unnecessary-setstate
+    # - avoid-unnecessary-setstate It is unclear why to "Avoid calling sync methods that call 'setState'."
     - avoid-expanded-as-spacer
     - avoid-wrapping-in-padding
     - check-for-equals-in-render-object-setters
diff --git a/packages/devtools_app/assets/img/doc/upload_dark.png b/packages/devtools_app/assets/img/doc/upload_dark.png
new file mode 100644
index 0000000..6b628e5
--- /dev/null
+++ b/packages/devtools_app/assets/img/doc/upload_dark.png
Binary files differ
diff --git a/packages/devtools_app/assets/img/doc/upload_light.png b/packages/devtools_app/assets/img/doc/upload_light.png
new file mode 100644
index 0000000..60e291d
--- /dev/null
+++ b/packages/devtools_app/assets/img/doc/upload_light.png
Binary files differ
diff --git a/packages/devtools_app/integration_test/test/live_connection/eval_and_browse_test.dart b/packages/devtools_app/integration_test/test/live_connection/eval_and_browse_test.dart
index f4f2ca3..b8a4c79 100644
--- a/packages/devtools_app/integration_test/test/live_connection/eval_and_browse_test.dart
+++ b/packages/devtools_app/integration_test/test/live_connection/eval_and_browse_test.dart
@@ -5,18 +5,13 @@
 // Do not delete these arguments. They are parsed by test runner.
 // test-argument:appPath="test/test_infra/fixtures/memory_app"
 
-import 'package:devtools_app/devtools_app.dart';
-import 'package:devtools_app/src/screens/memory/panes/control/primary_controls.dart';
-import 'package:devtools_app/src/screens/memory/panes/diff/widgets/snapshot_list.dart';
-import 'package:devtools_app/src/screens/memory/shared/primitives/instance_context_menu.dart';
-import 'package:devtools_app/src/shared/console/widgets/console_pane.dart';
 import 'package:devtools_test/helpers.dart';
 import 'package:devtools_test/integration_test.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';
 
+import 'eval_utils.dart';
+
 // To run:
 // dart run integration_test/run_tests.dart --target=integration_test/test/live_connection/eval_and_browse_test.dart
 
@@ -37,14 +32,14 @@
   testWidgets('memory eval and browse', (tester) async {
     await pumpAndConnectDevTools(tester, testApp);
 
-    final evalTester = _EvalAndBrowseTester(tester);
+    final evalTester = EvalTester(tester);
     await evalTester.prepareMemoryUI();
 
     logStatus('test basic evaluation');
-    await _testBasicEval(evalTester);
+    await testBasicEval(evalTester);
 
     logStatus('test variable assignment');
-    await _testAssignment(evalTester);
+    await testAssignment(evalTester);
 
     logStatus('test dump one instance to console');
     await _profileOneInstance(evalTester);
@@ -60,23 +55,7 @@
   });
 }
 
-Future<void> _testBasicEval(_EvalAndBrowseTester tester) async {
-  await tester.testEval('21 + 34', find.text('55'));
-}
-
-Future<void> _testAssignment(_EvalAndBrowseTester tester) async {
-  await tester.testEval('DateTime(2023)', find.text('DateTime'));
-  await tester.testEval(
-    r'var x = $0',
-    find.textContaining('Variable x is created '),
-  );
-  await tester.testEval(
-    'x.toString()',
-    find.text("'${DateTime(2023).toString()}'"),
-  );
-}
-
-Future<void> _profileOneInstance(_EvalAndBrowseTester tester) async {
+Future<void> _profileOneInstance(EvalTester tester) async {
   await tester.openContextMenuForClass('MyHomePage');
   await tester.tapAndPump(
     find.textContaining('one instance'),
@@ -85,7 +64,7 @@
   expect(find.text('MyHomePage'), findsNWidgets(2));
 }
 
-Future<void> _profileAllInstances(_EvalAndBrowseTester tester) async {
+Future<void> _profileAllInstances(EvalTester tester) async {
   await tester.openContextMenuForClass('_MyHomePageState');
   await tester.tapAndPump(find.textContaining('all class instances'));
   await tester.tapAndPump(
@@ -96,7 +75,7 @@
   expect(find.text('List (1 item)'), findsOneWidget);
 }
 
-Future<void> _inboundReferencesAreListed(_EvalAndBrowseTester tester) async {
+Future<void> _inboundReferencesAreListed(EvalTester tester) async {
   await tester.openContextMenuForClass('MyApp');
 
   await tester.tapAndPump(find.textContaining('one instance'));
@@ -120,128 +99,3 @@
     next: find.text('View'),
   );
 }
-
-class _EvalAndBrowseTester {
-  _EvalAndBrowseTester(this.tester);
-
-  final WidgetTester tester;
-
-  /// Tests if eval returns expected response by searching for response text.
-  Future<void> testEval(String expression, Finder expectedResponse) async {
-    await tapAndPump(find.byType(AutoCompleteSearchField));
-    await tester.enterText(find.byType(AutoCompleteSearchField), expression);
-    await tester.pump(safePumpDuration);
-    await _pressEnter();
-
-    expect(expectedResponse, findsOneWidget);
-  }
-
-  Future<void> _pressEnter() async {
-    // TODO(polina-c): Figure out why one time sometimes is not enough.
-    // https://github.com/flutter/devtools/issues/5436
-    await simulateKeyDownEvent(LogicalKeyboardKey.enter);
-    await simulateKeyUpEvent(LogicalKeyboardKey.enter);
-    await tester.pumpAndSettle();
-    await simulateKeyDownEvent(LogicalKeyboardKey.enter);
-    await simulateKeyUpEvent(LogicalKeyboardKey.enter);
-    await tester.pump(longPumpDuration);
-  }
-
-  /// Prepares the UI of the memory screen so that the eval-related elements are
-  /// visible on the screen for testing.
-  Future<void> prepareMemoryUI() async {
-    // Open memory screen.
-    await switchToScreen(
-      tester,
-      tabIcon: ScreenMetaData.memory.icon!,
-      screenId: ScreenMetaData.memory.id,
-    );
-
-    // Close warning and chart to get screen space.
-    await tapAndPump(
-      find.descendant(
-        of: find.byType(BannerWarning),
-        matching: find.byIcon(Icons.close),
-      ),
-    );
-    await tapAndPump(find.text(PrimaryControls.memoryChartText));
-
-    // Make console wider.
-    // The distance is big enough to see more items in console,
-    // but not too big to make classes in snapshot hidden.
-    const dragDistance = -320.0;
-    await tester.drag(
-      find.byType(ConsolePaneHeader),
-      const Offset(0, dragDistance),
-    );
-    await tester.pumpAndSettle();
-  }
-
-  Future<void> switchToSnapshotsAndTakeOne() async {
-    // Switch to diff tab.
-    await tapAndPump(find.text('Diff Snapshots'));
-
-    logStatus('Started taking snapshot.');
-    // Take snapshot.
-    const snapshotDuration = Duration(seconds: 20);
-    await tapAndPump(
-      find.byIcon(iconToTakeSnapshot),
-      duration: snapshotDuration,
-    );
-    logStatus('Finished taking snapshot.');
-
-    // Sort by class.
-    await tapAndPump(find.text('Class'));
-
-    // Select class.
-    await tapAndPump(find.text('MyApp'));
-  }
-
-  /// Taps and settles.
-  ///
-  /// If [next] is provided, will repeat the tap untill [next] returns results.
-  /// Returns [next].
-  Future<Finder?> tapAndPump(
-    Finder finder, {
-    Duration? duration,
-    Finder? next,
-    String? description,
-  }) async {
-    Future<void> action(int tryNumber) async {
-      logStatus('\nattempt #$tryNumber, tapping');
-      logStatus(description ?? finder.toString());
-      tryNumber++;
-      await tester.tap(finder);
-      await tester.pump(duration);
-      await tester.pumpAndSettle();
-    }
-
-    await action(0);
-
-    if (next == null) return null;
-
-    // These tries are needed because tap in console is flaky.
-    for (var tryNumber = 1; tryNumber < 10; tryNumber++) {
-      try {
-        final items = tester.widgetList(next);
-        if (items.isNotEmpty) return next;
-        await action(tryNumber);
-      } on StateError {
-        // tester.widgetList throws StateError if no widgets found.
-        await action(tryNumber);
-      }
-    }
-
-    throw StateError('Could not find $next');
-  }
-
-  Future<void> openContextMenuForClass(String className) async {
-    await tapAndPump(find.text(className));
-    await tapAndPump(
-      find.descendant(
-        of: find.byType(InstanceViewWithContextMenu),
-        matching: find.byType(ContextMenuButton),
-      ),
-    );
-  }
-}
diff --git a/packages/devtools_app/integration_test/test/live_connection/eval_and_inspect_test.dart b/packages/devtools_app/integration_test/test/live_connection/eval_and_inspect_test.dart
new file mode 100644
index 0000000..bd0d588
--- /dev/null
+++ b/packages/devtools_app/integration_test/test/live_connection/eval_and_inspect_test.dart
@@ -0,0 +1,61 @@
+// Copyright 2024 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.
+
+// Do not delete these arguments. They are parsed by test runner.
+// test-argument:appPath="test/test_infra/fixtures/memory_app"
+// test-argument:experimentsOn=true
+
+// ignore_for_file: avoid_print
+
+import 'package:devtools_test/helpers.dart';
+import 'package:devtools_test/integration_test.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'eval_utils.dart';
+
+// To run the test while connected to a flutter-tester device:
+// dart run integration_test/run_tests.dart --target=integration_test/test/live_connection/eval_and_inspect_test.dart
+
+// To run the test while connected to a chrome device:
+// dart run integration_test/run_tests.dart --target=integration_test/test/live_connection/eval_and_inspect_test.dart --test-app-device=chrome
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  late TestApp testApp;
+
+  setUpAll(() {
+    testApp = TestApp.fromEnvironment();
+    expect(testApp.vmServiceUri, isNotNull);
+  });
+
+  tearDown(() async {
+    await resetHistory();
+  });
+
+  testWidgets('eval with scope in inspector window', (tester) async {
+    await pumpAndConnectDevTools(tester, testApp);
+
+    final evalTester = EvalTester(tester);
+    await evalTester.prepareInspectorUI();
+
+    logStatus('testing basic evaluation');
+    await testBasicEval(evalTester);
+
+    logStatus('testing variable assignment');
+    await testAssignment(evalTester);
+
+    logStatus('testing eval on widget tree node');
+    await _testEvalOnWidgetTreeNode(evalTester);
+  });
+}
+
+Future<void> _testEvalOnWidgetTreeNode(EvalTester tester) async {
+  await tester.selectWidgetTreeNode(find.richText('FloatingActionButton'));
+  await tester.testEval(
+    r'var button = $0',
+    find.textContaining('Variable button is created '),
+  );
+}
diff --git a/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart b/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart
new file mode 100644
index 0000000..963a4dd
--- /dev/null
+++ b/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart
@@ -0,0 +1,177 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/screens/memory/panes/control/primary_controls.dart';
+import 'package:devtools_app/src/screens/memory/panes/diff/widgets/snapshot_list.dart';
+import 'package:devtools_app/src/screens/memory/shared/primitives/instance_context_menu.dart';
+import 'package:devtools_app/src/shared/console/widgets/console_pane.dart';
+import 'package:devtools_test/helpers.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+class EvalTester {
+  EvalTester(this.tester);
+
+  final WidgetTester tester;
+
+  /// Tests if eval returns expected response by searching for response text.
+  Future<void> testEval(String expression, Finder expectedResponse) async {
+    await tapAndPump(find.byType(AutoCompleteSearchField));
+    await tester.enterText(find.byType(AutoCompleteSearchField), expression);
+    await tester.pump(safePumpDuration);
+    await _pressEnter();
+
+    expect(expectedResponse, findsOneWidget);
+  }
+
+  Future<void> _pressEnter() async {
+    // TODO(polina-c): Figure out why one time sometimes is not enough.
+    // https://github.com/flutter/devtools/issues/5436
+    await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+    await simulateKeyUpEvent(LogicalKeyboardKey.enter);
+    await tester.pumpAndSettle();
+    await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+    await simulateKeyUpEvent(LogicalKeyboardKey.enter);
+    await tester.pump(longPumpDuration);
+  }
+
+  /// Prepares the UI of the memory screen so that the eval-related elements are
+  /// visible on the screen for testing.
+  Future<void> prepareMemoryUI() async {
+    // Open memory screen.
+    await switchToScreen(
+      tester,
+      tabIcon: ScreenMetaData.memory.icon!,
+      screenId: ScreenMetaData.memory.id,
+    );
+
+    // Close warning and chart to get screen space.
+    await tapAndPump(
+      find.descendant(
+        of: find.byType(BannerWarning),
+        matching: find.byIcon(Icons.close),
+      ),
+    );
+    await tapAndPump(find.text(PrimaryControls.memoryChartText));
+
+    // Make console wider.
+    // The distance is big enough to see more items in console,
+    // but not too big to make classes in snapshot hidden.
+    const dragDistance = -320.0;
+    await tester.drag(
+      find.byType(ConsolePaneHeader),
+      const Offset(0, dragDistance),
+    );
+    await tester.pumpAndSettle();
+  }
+
+  /// Prepares the UI of the inspector screen so that the eval-related
+  /// elements are visible on the screen for testing.
+  Future<void> prepareInspectorUI() async {
+    // Open the inspector screen.
+    await switchToScreen(
+      tester,
+      tabIcon: ScreenMetaData.inspector.icon!,
+      screenId: ScreenMetaData.inspector.id,
+    );
+    await tester.pumpAndSettle();
+  }
+
+  /// Selects a widget to run evaluation on.
+  Future<void> selectWidgetTreeNode(Finder finder) async {
+    await tapAndPump(
+      find.descendant(
+        of: find.byKey(InspectorScreenBodyState.summaryTreeKey),
+        matching: finder,
+      ),
+    );
+    await tester.pumpAndSettle();
+  }
+
+  Future<void> switchToSnapshotsAndTakeOne() async {
+    // Switch to diff tab.
+    await tapAndPump(find.text('Diff Snapshots'));
+
+    logStatus('Started taking snapshot.');
+    // Take snapshot.
+    const snapshotDuration = Duration(seconds: 20);
+    await tapAndPump(
+      find.byIcon(iconToTakeSnapshot),
+      duration: snapshotDuration,
+    );
+    logStatus('Finished taking snapshot.');
+
+    // Sort by class.
+    await tapAndPump(find.text('Class'));
+
+    // Select class.
+    await tapAndPump(find.text('MyApp'));
+  }
+
+  /// Taps and settles.
+  ///
+  /// If [next] is provided, will repeat the tap untill [next] returns results.
+  /// Returns [next].
+  Future<Finder?> tapAndPump(
+    Finder finder, {
+    Duration? duration,
+    Finder? next,
+    String? description,
+  }) async {
+    Future<void> action(int tryNumber) async {
+      logStatus('\nattempt #$tryNumber, tapping');
+      logStatus(description ?? finder.toString());
+      tryNumber++;
+      await tester.tap(finder);
+      await tester.pump(duration);
+      await tester.pumpAndSettle();
+    }
+
+    await action(0);
+
+    if (next == null) return null;
+
+    // These tries are needed because tap in console is flaky.
+    for (var tryNumber = 1; tryNumber < 10; tryNumber++) {
+      try {
+        final items = tester.widgetList(next);
+        if (items.isNotEmpty) return next;
+        await action(tryNumber);
+      } on StateError {
+        // tester.widgetList throws StateError if no widgets found.
+        await action(tryNumber);
+      }
+    }
+
+    throw StateError('Could not find $next');
+  }
+
+  Future<void> openContextMenuForClass(String className) async {
+    await tapAndPump(find.text(className));
+    await tapAndPump(
+      find.descendant(
+        of: find.byType(InstanceViewWithContextMenu),
+        matching: find.byType(ContextMenuButton),
+      ),
+    );
+  }
+}
+
+Future<void> testBasicEval(EvalTester tester) async {
+  await tester.testEval('21 + 34', find.text('55'));
+}
+
+Future<void> testAssignment(EvalTester tester) async {
+  await tester.testEval('DateTime(2023)', find.text('DateTime'));
+  await tester.testEval(
+    r'var x = $0',
+    find.textContaining('Variable x is created '),
+  );
+  await tester.testEval(
+    'x.toString()',
+    find.text("'${DateTime(2023).toString()}'"),
+  );
+}
diff --git a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection.png b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection.png
index 86304e8..815bd79 100644
--- a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection.png
+++ b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection.png
Binary files differ
diff --git a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection_2.png b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection_2.png
index 4499e84..26c4933 100644
--- a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection_2.png
+++ b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection_2.png
Binary files differ
diff --git a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_initial_load.png b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_initial_load.png
index d517320..9e91454 100644
--- a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_initial_load.png
+++ b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_initial_load.png
Binary files differ
diff --git a/packages/devtools_app/integration_test/test_infra/run/_in_file_args.dart b/packages/devtools_app/integration_test/test_infra/run/_in_file_args.dart
index aa8f435..dd6685e 100644
--- a/packages/devtools_app/integration_test/test_infra/run/_in_file_args.dart
+++ b/packages/devtools_app/integration_test/test_infra/run/_in_file_args.dart
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// ignore_for_file: avoid-dynamic
-
 import 'dart:convert';
 import 'dart:io';
 
diff --git a/packages/devtools_app/integration_test/test_infra/run/_test_app_driver.dart b/packages/devtools_app/integration_test/test_infra/run/_test_app_driver.dart
index ac5ebe4..f4cfd0d 100644
--- a/packages/devtools_app/integration_test/test_infra/run/_test_app_driver.dart
+++ b/packages/devtools_app/integration_test/test_infra/run/_test_app_driver.dart
@@ -188,10 +188,10 @@
   Map<String, Object?>? _parseFlutterResponse(String line) {
     if (line.startsWith('[') && line.endsWith(']')) {
       try {
-        final Map<String, Object?>? resp = json.decode(line)[0];
-        return resp;
+        return (json.decode(line) as List)[0];
       } catch (e) {
-        // Not valid JSON, so likely some other output that was surrounded by [brackets]
+        // Not valid JSON, so likely some other output that was surrounded by
+        // [brackets].
         return null;
       }
     }
@@ -360,7 +360,7 @@
         logMessage('<timed out>');
         throw '$message';
       },
-    ).catchError((error) {
+    ).catchError((Object? error) {
       throw '$error\nReceived:\n${messages.toString()}';
     }).whenComplete(() => sub.cancel());
   }
@@ -430,8 +430,10 @@
   static final _unsupportedTestsForDevice = <TestAppDevice, List<String>>{
     TestAppDevice.flutterTester: [],
     TestAppDevice.flutterChrome: [
-      // TODO(https://github.com/flutter/devtools/issues/5874): Remove once supported on web.
       'eval_and_browse_test.dart',
+      // TODO(https://github.com/flutter/devtools/issues/7145): Figure out why
+      // this fails on bots but passes locally and enable.
+      'eval_and_inspect_test.dart',
       'perfetto_test.dart',
       'performance_screen_event_recording_test.dart',
       'service_connection_test.dart',
@@ -439,6 +441,7 @@
     TestAppDevice.cli: [
       'debugger_panel_test.dart',
       'eval_and_browse_test.dart',
+      'eval_and_inspect_test.dart',
       'perfetto_test.dart',
       'performance_screen_event_recording_test.dart',
       'service_connection_test.dart',
diff --git a/packages/devtools_app/lib/devtools.dart b/packages/devtools_app/lib/devtools.dart
index 2dbcbb2..02f0156 100644
--- a/packages/devtools_app/lib/devtools.dart
+++ b/packages/devtools_app/lib/devtools.dart
@@ -9,4 +9,4 @@
 // the constant declaration `const String version =`.
 // If you change the declaration you must also modify the regex in
 // tools/update_version.dart.
-const String version = '2.31.1';
+const String version = '2.33.0-dev.3';
diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart
index 9ee93ac..bdd613d 100644
--- a/packages/devtools_app/lib/devtools_app.dart
+++ b/packages/devtools_app/lib/devtools_app.dart
@@ -98,3 +98,4 @@
 export 'src/shared/ui/icons.dart';
 export 'src/shared/ui/search.dart';
 export 'src/shared/utils.dart';
+export 'src/standalone_ui/api/vs_code_api.dart';
diff --git a/packages/devtools_app/lib/initialization.dart b/packages/devtools_app/lib/initialization.dart
index 6b8823a..35cefff 100644
--- a/packages/devtools_app/lib/initialization.dart
+++ b/packages/devtools_app/lib/initialization.dart
@@ -6,6 +6,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_web_plugins/url_strategy.dart';
+import 'package:logging/logging.dart';
 
 import 'src/app.dart';
 import 'src/framework/app_error_handling.dart';
@@ -19,6 +20,9 @@
 import 'src/shared/preferences.dart';
 import 'src/shared/primitives/url_utils.dart';
 import 'src/shared/primitives/utils.dart';
+import 'src/shared/server/server.dart' as server;
+
+final _log = Logger('initializtion');
 
 /// Handles necessary initialization then runs DevTools.
 ///
@@ -76,7 +80,6 @@
   // Initialize the framework before we do anything else, otherwise the
   // StorageController won't be initialized and preferences won't be loaded.
   await initializeFramework();
-
   await _initDTDConnection();
 
   final preferences = PreferencesController();
@@ -103,10 +106,36 @@
 }
 
 Future<void> _initDTDConnection() async {
-  final queryParams = loadQueryParams();
-  final dtdUri = queryParams['dtdUri'];
-  if (dtdUri != null) {
-    await dtdManager.connect(Uri.parse(dtdUri));
+  try {
+    // Get the dtdUri from the devtools server
+    final dtdUri = await server.getDtdUri();
+
+    if (dtdUri != null) {
+      await dtdManager.connect(
+        dtdUri,
+        onError: (e, st) {
+          notificationService.pushError(
+            'Failed to connect to the Dart Tooling Daemon',
+            isReportable: false,
+          );
+          reportError(
+            e,
+            errorType: 'Dart Tooling Daemon connection failed.',
+            stack: st,
+          );
+        },
+      );
+    } else {
+      _log.info('No DTD uri provided from the server during initialization.');
+    }
+  } catch (e, st) {
+    // Dtd failing to connect does not interfere with devtools starting up so
+    // catch any errors and report them.
+    reportError(
+      e,
+      errorType: 'Failed to initialize the DTD connection.',
+      stack: st,
+    );
   }
 }
 
diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart
index bc22d96..444e653 100644
--- a/packages/devtools_app/lib/src/app.dart
+++ b/packages/devtools_app/lib/src/app.dart
@@ -119,9 +119,6 @@
 
   bool _isDarkThemeEnabledPreference = true;
 
-  bool get denseModeEnabled => _denseModeEnabled;
-  bool _denseModeEnabled = false;
-
   final hoverCardController = HoverCardController();
 
   late ReleaseNotesController releaseNotesController;
@@ -171,13 +168,6 @@
       });
     });
 
-    _denseModeEnabled = preferences.denseModeEnabled.value;
-    addAutoDisposeListener(preferences.denseModeEnabled, () {
-      setState(() {
-        _denseModeEnabled = preferences.denseModeEnabled.value;
-      });
-    });
-
     releaseNotesController = ReleaseNotesController();
   }
 
diff --git a/packages/devtools_app/lib/src/extensions/embedded/_controller_web.dart b/packages/devtools_app/lib/src/extensions/embedded/_controller_web.dart
index 66e1638..789c941 100644
--- a/packages/devtools_app/lib/src/extensions/embedded/_controller_web.dart
+++ b/packages/devtools_app/lib/src/extensions/embedded/_controller_web.dart
@@ -8,7 +8,7 @@
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_extensions/api.dart';
 import 'package:path/path.dart' as path;
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import '../../shared/development_helpers.dart';
 import '../../shared/globals.dart';
@@ -57,6 +57,7 @@
       ExtensionEventParameters.theme: preferences.darkModeTheme.value
           ? ExtensionEventParameters.themeValueDark
           : ExtensionEventParameters.themeValueLight,
+      if (dtdManager.uri != null) 'dtdUri': dtdManager.uri.toString(),
     };
     return Uri.parse(baseUri).copyWith(queryParameters: queryParams).toString();
   }
diff --git a/packages/devtools_app/lib/src/extensions/embedded/_view_web.dart b/packages/devtools_app/lib/src/extensions/embedded/_view_web.dart
index ce401d4..4509b3a 100644
--- a/packages/devtools_app/lib/src/extensions/embedded/_view_web.dart
+++ b/packages/devtools_app/lib/src/extensions/embedded/_view_web.dart
@@ -10,7 +10,7 @@
 import 'package:devtools_extensions/api.dart';
 import 'package:devtools_extensions/utils.dart';
 import 'package:flutter/material.dart';
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import '../../shared/banner_messages.dart';
 import '../../shared/common_widgets.dart';
diff --git a/packages/devtools_app/lib/src/extensions/extension_screen.dart b/packages/devtools_app/lib/src/extensions/extension_screen.dart
index b0b01d0..b819daf 100644
--- a/packages/devtools_app/lib/src/extensions/extension_screen.dart
+++ b/packages/devtools_app/lib/src/extensions/extension_screen.dart
@@ -20,7 +20,7 @@
   ExtensionScreen(this.extensionConfig)
       : super.conditional(
           // TODO(kenz): we may need to ensure this is a unique id.
-          id: '${extensionConfig.name}_ext',
+          id: extensionConfig.screenId,
           title: extensionConfig.name,
           icon: extensionConfig.icon,
           // TODO(kenz): support static DevTools extensions.
@@ -134,4 +134,6 @@
         materialIconCodePoint,
         fontFamily: 'MaterialIcons',
       );
+
+  String get screenId => '${name}_ext';
 }
diff --git a/packages/devtools_app/lib/src/extensions/extension_service.dart b/packages/devtools_app/lib/src/extensions/extension_service.dart
index 1ef7323..2de7f3d 100644
--- a/packages/devtools_app/lib/src/extensions/extension_service.dart
+++ b/packages/devtools_app/lib/src/extensions/extension_service.dart
@@ -11,6 +11,19 @@
 
 class ExtensionService extends DisposableController
     with AutoDisposeControllerMixin {
+  ExtensionService({this.fixedAppRoot});
+
+  /// The fixed (unchanging) root file:// URI for the application this
+  /// [ExtensionService] will manage DevTools extensions for.
+  ///
+  /// When null, the root will be calculated from the [serviceManager]'s
+  /// currently connected app. See [_initAppRoot].
+  final Uri? fixedAppRoot;
+
+  /// The root file:// URI for the Dart / Flutter application this
+  /// [ExtensionService] will manage DevTools extensions for.
+  Uri? _appRoot;
+
   /// All the DevTools extensions that are available for the connected
   /// application, regardless of whether they have been enabled or disabled
   /// by the user.
@@ -45,31 +58,43 @@
       <String, ValueNotifier<ExtensionEnabledState>>{};
 
   Future<void> initialize() async {
+    await _initAppRoot();
     await _maybeRefreshExtensions();
-    addAutoDisposeListener(
-      serviceConnection.serviceManager.connectedState,
-      () async {
-        if (serviceConnection.serviceManager.connectedState.value.connected) {
-          await _maybeRefreshExtensions();
-        } else {
-          _reset();
-        }
-      },
-    );
 
-    // TODO(https://github.com/flutter/flutter/issues/134470): refresh on
-    // hot reload and hot restart events instead.
-    addAutoDisposeListener(
-      serviceConnection.serviceManager.isolateManager.mainIsolate,
-      () async {
-        if (serviceConnection.serviceManager.isolateManager.mainIsolate.value !=
-            null) {
-          await _maybeRefreshExtensions();
-        } else {
-          _reset();
-        }
-      },
-    );
+    cancelListeners();
+
+    // We only need to add VM service manager related listeners when we are
+    // interacting with the currently connected app (i.e. when
+    // [fixedAppRootUri] is null).
+    if (fixedAppRoot == null) {
+      addAutoDisposeListener(
+        serviceConnection.serviceManager.connectedState,
+        () async {
+          if (serviceConnection.serviceManager.connectedState.value.connected) {
+            await _initAppRoot();
+            await _maybeRefreshExtensions();
+          } else {
+            _reset();
+          }
+        },
+      );
+
+      // TODO(https://github.com/flutter/flutter/issues/134470): refresh on
+      // hot reload and hot restart events instead.
+      addAutoDisposeListener(
+        serviceConnection.serviceManager.isolateManager.mainIsolate,
+        () async {
+          if (serviceConnection
+                  .serviceManager.isolateManager.mainIsolate.value !=
+              null) {
+            await _initAppRoot();
+            await _maybeRefreshExtensions();
+          } else {
+            _reset();
+          }
+        },
+      );
+    }
 
     addAutoDisposeListener(
       preferences.devToolsExtensions.showOnlyEnabledExtensions,
@@ -83,21 +108,23 @@
     // .dart_tool/package_config.json file for changes.
   }
 
+  Future<void> _initAppRoot() async {
+    _appRoot = fixedAppRoot ?? await _connectedAppRoot();
+  }
+
   Future<void> _maybeRefreshExtensions() async {
-    final appRootPath = await _connectedAppRootPath();
-    if (appRootPath == null) return;
+    if (_appRoot == null) return;
 
     _refreshInProgress.value = true;
     _availableExtensions.value =
-        await server.refreshAvailableExtensions(appRootPath)
+        await server.refreshAvailableExtensions(_appRoot!)
           ..sort();
     await _refreshExtensionEnabledStates();
     _refreshInProgress.value = false;
   }
 
   Future<void> _refreshExtensionEnabledStates() async {
-    final appRootPath = await _connectedAppRootPath();
-    if (appRootPath == null) return;
+    if (_appRoot == null) return;
 
     final onlyIncludeEnabled =
         preferences.devToolsExtensions.showOnlyEnabledExtensions.value;
@@ -105,7 +132,7 @@
     final visible = <DevToolsExtensionConfig>[];
     for (final extension in _availableExtensions.value) {
       final stateFromOptionsFile = await server.extensionEnabledState(
-        rootPath: appRootPath,
+        appRoot: _appRoot!,
         extensionName: extension.name,
       );
       final stateNotifier = _extensionEnabledStates.putIfAbsent(
@@ -133,18 +160,18 @@
     DevToolsExtensionConfig extension, {
     required bool enable,
   }) async {
-    final appRootPath = await _connectedAppRootPath();
-    if (appRootPath != null) {
-      await server.extensionEnabledState(
-        rootPath: appRootPath,
-        extensionName: extension.name,
-        enable: enable,
-      );
-      await _refreshExtensionEnabledStates();
-    }
+    if (_appRoot == null) return;
+
+    await server.extensionEnabledState(
+      appRoot: _appRoot!,
+      extensionName: extension.name,
+      enable: enable,
+    );
+    await _refreshExtensionEnabledStates();
   }
 
   void _reset() {
+    _appRoot = null;
     _availableExtensions.value = [];
     _visibleExtensions.value = [];
     _extensionEnabledStates.clear();
@@ -154,9 +181,9 @@
 
 // TODO(kenz): consider caching this for the duration of the VM service
 // connection.
-Future<String?> _connectedAppRootPath() async {
-  var fileUri = await serviceConnection.rootLibraryForMainIsolate();
-  if (fileUri == null) return null;
+Future<Uri?> _connectedAppRoot() async {
+  var fileUriString = await serviceConnection.rootLibraryForMainIsolate();
+  if (fileUriString == null) return null;
 
   // TODO(kenz): for robustness, consider sending the root library uri to the
   // server and having the server look for the package folder that contains the
@@ -164,10 +191,10 @@
 
   final directoryRegExp =
       RegExp(r'\/(lib|integration_test|test|bin)\/[^\/.]*.dart');
-  final directoryIndex = fileUri.indexOf(directoryRegExp);
+  final directoryIndex = fileUriString.indexOf(directoryRegExp);
   if (directoryIndex != -1) {
-    fileUri = fileUri.substring(0, directoryIndex);
+    fileUriString = fileUriString.substring(0, directoryIndex);
   }
 
-  return fileUri;
+  return Uri.parse(fileUriString);
 }
diff --git a/packages/devtools_app/lib/src/extensions/extension_settings.dart b/packages/devtools_app/lib/src/extensions/extension_settings.dart
index 036ba04..cc917f0 100644
--- a/packages/devtools_app/lib/src/extensions/extension_settings.dart
+++ b/packages/devtools_app/lib/src/extensions/extension_settings.dart
@@ -63,15 +63,13 @@
                   'enabled\n(i.e. do not show tabs for extensions that have no '
                   'preference set).',
             ),
-            const SizedBox(height: defaultSpacing),
+            const PaddedDivider(),
             Expanded(
               child: availableExtensions.isEmpty
                   ? Center(
                       child: Text(
                         'No extensions available.',
-                        style: theme.textTheme.bodyLarge!.copyWith(
-                          color: theme.colorScheme.subtleTextColor,
-                        ),
+                        style: theme.subtleTextStyle,
                       ),
                     )
                   : _ExtensionsList(extensions: availableExtensions),
diff --git a/packages/devtools_app/lib/src/framework/app_bar.dart b/packages/devtools_app/lib/src/framework/app_bar.dart
index b7df309..6ff4485 100644
--- a/packages/devtools_app/lib/src/framework/app_bar.dart
+++ b/packages/devtools_app/lib/src/framework/app_bar.dart
@@ -159,7 +159,7 @@
 
   bool get overflowTabSelected => selectedIndex >= 0;
 
-  final Function(int) onItemSelected;
+  final void Function(int) onItemSelected;
 
   @override
   Widget build(BuildContext context) {
diff --git a/packages/devtools_app/lib/src/framework/framework_core.dart b/packages/devtools_app/lib/src/framework/framework_core.dart
index 999dbee..f8792e1 100644
--- a/packages/devtools_app/lib/src/framework/framework_core.dart
+++ b/packages/devtools_app/lib/src/framework/framework_core.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:devtools_app_shared/service.dart';
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_shared/devtools_shared.dart';
@@ -14,7 +15,6 @@
 import '../../devtools.dart' as devtools show version;
 import '../extensions/extension_service.dart';
 import '../screens/debugger/breakpoint_manager.dart';
-import '../service/dtd_manager.dart';
 import '../service/service_manager.dart';
 import '../service/vm_service_wrapper.dart';
 import '../shared/banner_messages.dart';
diff --git a/packages/devtools_app/lib/src/framework/home_screen.dart b/packages/devtools_app/lib/src/framework/home_screen.dart
index ba1fdc2..052dc5d 100644
--- a/packages/devtools_app/lib/src/framework/home_screen.dart
+++ b/packages/devtools_app/lib/src/framework/home_screen.dart
@@ -116,7 +116,7 @@
         child: const ConnectedAppSummary(narrowView: false),
       );
     }
-    return const ConnectDialog();
+    return const ConnectInput();
   }
 }
 
@@ -145,7 +145,7 @@
             Expanded(
               child: Text(
                 title,
-                style: textTheme.titleLarge,
+                style: textTheme.titleMedium,
               ),
             ),
             ...actions,
@@ -159,15 +159,14 @@
   }
 }
 
-class ConnectDialog extends StatefulWidget {
-  const ConnectDialog({Key? key}) : super(key: key);
+class ConnectInput extends StatefulWidget {
+  const ConnectInput({Key? key}) : super(key: key);
 
   @override
-  State<ConnectDialog> createState() => _ConnectDialogState();
+  State<ConnectInput> createState() => _ConnectInputState();
 }
 
-class _ConnectDialogState extends State<ConnectDialog>
-    with BlockingActionMixin {
+class _ConnectInputState extends State<ConnectInput> with BlockingActionMixin {
   late final TextEditingController connectDialogController;
 
   SharedPreferences? _debugSharedPreferences;
@@ -211,32 +210,26 @@
         Row(
           children: [
             SizedBox(
+              height: defaultTextFieldHeight,
               width: scaleByFontFactor(350.0),
-              child: TextField(
+              child: DevToolsClearableTextField(
+                labelText: 'VM service URL',
                 onSubmitted:
                     actionInProgress ? null : (str) => unawaited(_connect()),
                 autofocus: true,
-                decoration: const InputDecoration(
-                  isDense: true,
-                  border: OutlineInputBorder(),
-                  enabledBorder: OutlineInputBorder(
-                    // TODO(jacobr): we need to use themed colors everywhere instead
-                    // of hard coding material colors.
-                    borderSide: BorderSide(width: 0.5, color: Colors.grey),
-                  ),
-                ),
                 controller: connectDialogController,
               ),
             ),
             const SizedBox(width: defaultSpacing),
-            ElevatedButton(
+            DevToolsButton(
               onPressed: actionInProgress ? null : () => unawaited(_connect()),
-              child: const Text('Connect'),
+              elevated: true,
+              label: 'Connect',
             ),
           ],
         ),
         Padding(
-          padding: const EdgeInsets.symmetric(vertical: 8.0),
+          padding: const EdgeInsets.symmetric(vertical: densePadding),
           child: Text(
             '(e.g., http://127.0.0.1:12345/auth_code=...)',
             textAlign: TextAlign.start,
@@ -253,14 +246,14 @@
         children: [
           Text(
             'Connect to a Running App',
-            style: textTheme.titleMedium,
+            style: textTheme.titleSmall,
           ),
           const SizedBox(height: denseRowSpacing),
           Text(
             'Enter a URL to a running Dart or Flutter application',
             style: textTheme.bodySmall,
           ),
-          const Padding(padding: EdgeInsets.only(top: 20.0)),
+          const SizedBox(height: denseSpacing),
           connectorInput,
         ],
       ),
diff --git a/packages/devtools_app/lib/src/framework/initializer.dart b/packages/devtools_app/lib/src/framework/initializer.dart
index f41d82d..0f81921 100644
--- a/packages/devtools_app/lib/src/framework/initializer.dart
+++ b/packages/devtools_app/lib/src/framework/initializer.dart
@@ -216,10 +216,7 @@
                   child: const Text(connectToNewAppText),
                 )
               else
-                Text(
-                  'Run a new debug session to reconnect',
-                  style: theme.textTheme.bodyMedium,
-                ),
+                const Text('Run a new debug session to reconnect'),
               const Spacer(),
               if (offlineController.offlineDataJson.isNotEmpty)
                 ElevatedButton(
diff --git a/packages/devtools_app/lib/src/framework/notifications_view.dart b/packages/devtools_app/lib/src/framework/notifications_view.dart
index 447ce55..f39af18 100644
--- a/packages/devtools_app/lib/src/framework/notifications_view.dart
+++ b/packages/devtools_app/lib/src/framework/notifications_view.dart
@@ -318,7 +318,7 @@
   @override
   Widget build(BuildContext context) {
     final theme = Theme.of(context);
-    final textStyle = theme.textTheme.bodyMedium;
+    final textStyle = theme.regularTextStyle;
     return Padding(
       padding: const EdgeInsets.fromLTRB(
         denseSpacing,
@@ -329,7 +329,7 @@
       child: Text(
         widget.message.text,
         style: widget.message.isError
-            ? textStyle?.copyWith(color: theme.colorScheme.error)
+            ? textStyle.copyWith(color: theme.colorScheme.error)
             : textStyle,
         overflow: TextOverflow.visible,
         maxLines: 10,
diff --git a/packages/devtools_app/lib/src/framework/release_notes/release_notes.dart b/packages/devtools_app/lib/src/framework/release_notes/release_notes.dart
index 3c82497..04bd9f5 100644
--- a/packages/devtools_app/lib/src/framework/release_notes/release_notes.dart
+++ b/packages/devtools_app/lib/src/framework/release_notes/release_notes.dart
@@ -4,14 +4,18 @@
 
 import 'dart:async';
 
+import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_shared/devtools_shared.dart';
 import 'package:flutter/material.dart';
 import 'package:http/http.dart' as http;
 import 'package:logging/logging.dart';
 
 import '../../../devtools.dart' as devtools;
+import '../../shared/primitives/url_utils.dart';
 import '../../shared/server/server.dart' as server;
 import '../../shared/side_panel.dart';
+import '../../shared/utils.dart';
+import '../../standalone_ui/standalone_screen.dart';
 
 final _log = Logger('release_notes');
 
@@ -57,6 +61,15 @@
   }
 
   void _maybeShowReleaseNotes() async {
+    final currentUrl = getWebUrl();
+    final currentPage =
+        currentUrl != null ? extractCurrentPageFromUrl(currentUrl) : null;
+    if (isEmbedded() &&
+        currentPage == StandaloneScreenType.vsCodeFlutterPanel.name) {
+      // Do not show release notes in the Flutter sidebar.
+      return;
+    }
+
     SemanticVersion previousVersion = SemanticVersion();
     if (server.isDevToolsServerAvailable) {
       final lastReleaseNotesShownVersion =
diff --git a/packages/devtools_app/lib/src/framework/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold.dart
index 8c794c1..5d9bb81 100644
--- a/packages/devtools_app/lib/src/framework/scaffold.dart
+++ b/packages/devtools_app/lib/src/framework/scaffold.dart
@@ -302,7 +302,6 @@
           ),
       ],
     );
-    final theme = Theme.of(context);
 
     return Provider<ImportController>.value(
       value: _importController,
@@ -341,9 +340,7 @@
                       ? Split(
                           axis: Axis.vertical,
                           splitters: [
-                            ConsolePaneHeader(
-                              backgroundColor: theme.colorScheme.surface,
-                            ),
+                            ConsolePaneHeader(),
                           ],
                           initialFractions: const [0.8, 0.2],
                           children: [
diff --git a/packages/devtools_app/lib/src/framework/settings_dialog.dart b/packages/devtools_app/lib/src/framework/settings_dialog.dart
index 65ef41b..ff03b5d 100644
--- a/packages/devtools_app/lib/src/framework/settings_dialog.dart
+++ b/packages/devtools_app/lib/src/framework/settings_dialog.dart
@@ -53,14 +53,6 @@
               gaItem: gac.darkTheme,
             ),
           ),
-          Flexible(
-            child: CheckboxSetting(
-              title: 'Use dense mode',
-              notifier: preferences.denseModeEnabled,
-              onChanged: preferences.toggleDenseMode,
-              gaItem: gac.denseMode,
-            ),
-          ),
           if (isExternalBuild && isDevToolsServerAvailable)
             Flexible(
               child: CheckboxSetting(
@@ -94,6 +86,8 @@
 class _VerboseLoggingSetting extends StatelessWidget {
   const _VerboseLoggingSetting();
 
+  static const _minScreenWidthForTextBeforeScaling = 500.0;
+
   @override
   Widget build(BuildContext context) {
     return Column(
@@ -114,6 +108,8 @@
               icon: Icons.copy_outlined,
               gaScreen: gac.settingsDialog,
               gaSelection: gac.copyLogs,
+              minScreenWidthForTextBeforeScaling:
+                  _minScreenWidthForTextBeforeScaling,
               onPressed: () async => await copyToClipboard(
                 LogStorage.root.toString(),
                 'Successfully copied logs',
@@ -124,6 +120,8 @@
               label: 'Clear logs',
               gaScreen: gac.settingsDialog,
               gaSelection: gac.clearLogs,
+              minScreenWidthForTextBeforeScaling:
+                  _minScreenWidthForTextBeforeScaling,
               onPressed: LogStorage.root.clear,
             ),
           ],
@@ -132,12 +130,13 @@
         const Row(
           mainAxisSize: MainAxisSize.min,
           children: [
-            Spacer(),
             Icon(Icons.warning),
             SizedBox(width: defaultSpacing),
-            Text(
-              'Logs may contain sensitive information.\n'
-              'Always check their contents before sharing.',
+            Flexible(
+              child: Text(
+                'Logs may contain sensitive information.\n'
+                'Always check their contents before sharing.',
+              ),
             ),
           ],
         ),
diff --git a/packages/devtools_app/lib/src/framework/status_line.dart b/packages/devtools_app/lib/src/framework/status_line.dart
index e0330c1..c84949e 100644
--- a/packages/devtools_app/lib/src/framework/status_line.dart
+++ b/packages/devtools_app/lib/src/framework/status_line.dart
@@ -121,7 +121,6 @@
 
   Widget buildConnectionStatus(BuildContext context, MediaSize screenWidth) {
     final theme = Theme.of(context);
-    final textTheme = theme.textTheme;
     const noConnectionMsg = 'No client connection';
     return ValueListenableBuilder<ConnectedState>(
       valueListenable: serviceConnection.serviceManager.connectedState,
@@ -139,7 +138,7 @@
 
           final color = isConnected
               ? theme.colorScheme.onPrimary
-              : textTheme.bodyMedium!.color;
+              : theme.regularTextStyle.color;
 
           return Row(
             mainAxisAlignment: MainAxisAlignment.end,
@@ -164,9 +163,9 @@
                 child: Text(
                   description,
                   style: isConnected
-                      ? textTheme.bodyMedium!
+                      ? theme.regularTextStyle
                           .copyWith(color: theme.colorScheme.onPrimary)
-                      : textTheme.bodyMedium,
+                      : theme.regularTextStyle,
                   overflow: TextOverflow.clip,
                 ),
               ),
@@ -186,7 +185,7 @@
             )
           : Text(
               noConnectionMsg,
-              style: textTheme.bodyMedium,
+              style: theme.regularTextStyle,
             ),
     );
   }
@@ -316,7 +315,6 @@
 
   @override
   Widget build(BuildContext context) {
-    final textTheme = Theme.of(context).textTheme;
     return Row(
       children: [
         Icon(
@@ -328,7 +326,7 @@
         const SizedBox(width: denseSpacing),
         Text(
           ref == null ? 'isolate' : _isolateName(ref!),
-          style: textTheme.bodyMedium!.copyWith(color: color),
+          style: Theme.of(context).regularTextStyle.copyWith(color: color),
         ),
       ],
     );
diff --git a/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart b/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart
index 0665c9f..a359cac 100644
--- a/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart
+++ b/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart
@@ -535,7 +535,7 @@
 
   final TreemapNode treemapRoot;
 
-  final Function(TreemapNode?) onRootChangedCallback;
+  final void Function(TreemapNode?) onRootChangedCallback;
 
   final Widget analysisTable;
 
diff --git a/packages/devtools_app/lib/src/screens/app_size/app_size_table.dart b/packages/devtools_app/lib/src/screens/app_size/app_size_table.dart
index e07f795..e0f752e 100644
--- a/packages/devtools_app/lib/src/screens/app_size/app_size_table.dart
+++ b/packages/devtools_app/lib/src/screens/app_size/app_size_table.dart
@@ -90,8 +90,7 @@
   bool get supportsSorting => true;
 
   @override
-  String getTooltip(TreemapNode dataObject) =>
-      dataObject.displayText().toPlainText();
+  String getTooltip(TreemapNode dataObject) => dataObject.displayText();
 
   @override
   double getNodeIndentPx(TreemapNode dataObject) {
diff --git a/packages/devtools_app/lib/src/screens/app_size/code_size_attribution.dart b/packages/devtools_app/lib/src/screens/app_size/code_size_attribution.dart
index 9ae7b43..afb6e0c 100644
--- a/packages/devtools_app/lib/src/screens/app_size/code_size_attribution.dart
+++ b/packages/devtools_app/lib/src/screens/app_size/code_size_attribution.dart
@@ -311,8 +311,9 @@
 
 extension CallGraphNodeDisplay on CallGraphNode {
   String get display {
-    final displayText =
-        data is ProgramInfoNode ? data.qualifiedName : data.toString();
+    final displayText = data is ProgramInfoNode
+        ? (data as ProgramInfoNode).qualifiedName
+        : data.toString();
     if (displayText == '@shared') {
       // Special case '@shared' because this is the name of the call graph root,
       // and '@root' has a more intuitive meaning.
diff --git a/packages/devtools_app/lib/src/screens/debugger/codeview.dart b/packages/devtools_app/lib/src/screens/debugger/codeview.dart
index 36aa898..dd95f4f 100644
--- a/packages/devtools_app/lib/src/screens/debugger/codeview.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/codeview.dart
@@ -63,8 +63,7 @@
   static const debuggerCodeViewVerticalScrollbarKey =
       Key('debuggerCodeViewVerticalScrollbarKey');
 
-  static double get rowHeight =>
-      isDense() ? scaleByFontFactor(16.0) : scaleByFontFactor(20.0);
+  static double get rowHeight => scaleByFontFactor(16.0);
 
   final CodeViewController codeViewController;
   final DebuggerController? debuggerController;
@@ -863,7 +862,7 @@
 
   @override
   Widget build(BuildContext context) {
-    final bpLineSet = Set.from(breakpoints.map((bp) => bp.line));
+    final bpLineSet = Set.of(breakpoints.map((bp) => bp.line));
     final theme = Theme.of(context);
     final coverageLines =
         sourceReport.coverageHitLines.union(sourceReport.coverageMissedLines);
diff --git a/packages/devtools_app/lib/src/screens/debugger/common.dart b/packages/devtools_app/lib/src/screens/debugger/common.dart
index 22c2f04..1ff8793 100644
--- a/packages/devtools_app/lib/src/screens/debugger/common.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/common.dart
@@ -5,8 +5,6 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
 
-import '../../shared/utils.dart';
-
 /// Create a header area for a debugger component.
 ///
 /// Either one of [text] or [child] must be supplied.
@@ -15,14 +13,11 @@
   assert(text == null || child == null);
 
   return OutlineDecoration.onlyBottom(
-    child: SizedBox(
-      height: defaultHeaderHeight(isDense: isDense()),
-      child: Container(
-        padding: const EdgeInsets.only(left: defaultSpacing),
-        alignment: Alignment.centerLeft,
-        height: areaPaneHeaderHeight,
-        child: child ?? Text(text!, style: theme.textTheme.titleSmall),
-      ),
+    child: Container(
+      padding: const EdgeInsets.only(left: defaultSpacing),
+      alignment: Alignment.centerLeft,
+      height: defaultHeaderHeight,
+      child: child ?? Text(text!, style: theme.textTheme.titleSmall),
     ),
   );
 }
diff --git a/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart b/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart
index 90cc02b..df35a2e 100644
--- a/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart
@@ -17,7 +17,6 @@
 import '../../shared/analytics/constants.dart' as gac;
 import '../../shared/common_widgets.dart';
 import '../../shared/diagnostics/primitives/source_location.dart';
-import '../../shared/flex_split_column.dart';
 import '../../shared/globals.dart';
 import '../../shared/primitives/listenable.dart';
 import '../../shared/primitives/utils.dart';
@@ -239,6 +238,7 @@
               message: 'Remove all breakpoints',
               child: ToolbarAction(
                 icon: Icons.delete,
+                size: defaultIconSize,
                 onPressed: breakpoints.isNotEmpty
                     ? () => unawaited(breakpointManager.clearBreakpoints())
                     : null,
diff --git a/packages/devtools_app/lib/src/screens/debugger/file_search.dart b/packages/devtools_app/lib/src/screens/debugger/file_search.dart
index 87ee92f..6a4db82 100644
--- a/packages/devtools_app/lib/src/screens/debugger/file_search.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/file_search.dart
@@ -396,7 +396,7 @@
       return copyWith();
     }
 
-    final topMatches = [];
+    final topMatches = <List<ScriptRef>>[];
     int matchesLeft = numOfMatchesToShow;
     for (final matches in [
       _exactFileNameMatches,
diff --git a/packages/devtools_app/lib/src/screens/debugger/program_explorer.dart b/packages/devtools_app/lib/src/screens/debugger/program_explorer.dart
index 5d5578f..9c5bc77 100644
--- a/packages/devtools_app/lib/src/screens/debugger/program_explorer.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/program_explorer.dart
@@ -11,7 +11,6 @@
 import 'package:vm_service/vm_service.dart' hide Stack;
 
 import '../../shared/common_widgets.dart';
-import '../../shared/flex_split_column.dart';
 import '../../shared/globals.dart';
 import '../../shared/primitives/utils.dart';
 import '../../shared/tree.dart';
@@ -299,8 +298,8 @@
   });
 
   final ProgramExplorerController controller;
-  final Function(VMServiceObjectNode) onItemSelected;
-  final Function(VMServiceObjectNode) onItemExpanded;
+  final void Function(VMServiceObjectNode) onItemSelected;
+  final void Function(VMServiceObjectNode) onItemExpanded;
 
   @override
   State<_FileExplorer> createState() => _FileExplorerState();
@@ -378,8 +377,8 @@
   });
 
   final ProgramExplorerController controller;
-  final Function(VMServiceObjectNode) onItemSelected;
-  final Function(VMServiceObjectNode) onItemExpanded;
+  final void Function(VMServiceObjectNode) onItemSelected;
+  final void Function(VMServiceObjectNode) onItemExpanded;
 
   @override
   Widget build(BuildContext context) {
diff --git a/packages/devtools_app/lib/src/screens/debugger/syntax_highlighter.dart b/packages/devtools_app/lib/src/screens/debugger/syntax_highlighter.dart
index c90094a..b1f92e8 100644
--- a/packages/devtools_app/lib/src/screens/debugger/syntax_highlighter.dart
+++ b/packages/devtools_app/lib/src/screens/debugger/syntax_highlighter.dart
@@ -16,11 +16,11 @@
 final _log = Logger('syntax_highlighter');
 
 class SyntaxHighlighter {
-  SyntaxHighlighter({source}) : source = source ?? '';
+  SyntaxHighlighter({String? source}) : source = source ?? '';
 
   SyntaxHighlighter.withGrammar({
     Grammar? grammar,
-    source,
+    String? source,
   }) : source = source ?? '' {
     _grammar = grammar;
   }
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart
index 9803848..2835ca9 100644
--- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart
@@ -485,8 +485,8 @@
 
   @override
   Widget build(BuildContext context) {
-    final colorScheme = Theme.of(context).colorScheme;
-    final textTheme = Theme.of(context).textTheme;
+    final theme = Theme.of(context);
+    final colorScheme = theme.colorScheme;
     return SizedBox.fromSize(
       size: _kNotificationCardSize,
       child: Card(
@@ -507,14 +507,10 @@
                 child: Column(
                   crossAxisAlignment: CrossAxisAlignment.start,
                   children: [
-                    Text(
-                      title,
-                      style: textTheme.bodyMedium!
-                          .copyWith(color: colorScheme.onSurface),
-                    ),
+                    Text(title),
                     Text(
                       description,
-                      style: Theme.of(context).subtleTextStyle,
+                      style: theme.subtleTextStyle,
                     ),
                     Expanded(
                       child: Align(
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart
index 13f4518..1ef1f3c 100644
--- a/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/validation_details_view.dart
@@ -11,7 +11,6 @@
 
 import '../../shared/common_widgets.dart';
 import '../../shared/config_specific/launch_url/launch_url.dart';
-import '../../shared/table/table.dart';
 import '../../shared/ui/colors.dart';
 import 'deep_link_list_view.dart';
 import 'deep_links_controller.dart';
@@ -37,7 +36,7 @@
         ValidationDetailHeader(viewType: viewType, controller: controller),
         Padding(
           padding: const EdgeInsets.symmetric(
-            horizontal: largeSpacing,
+            horizontal: extraLargeSpacing,
             vertical: defaultSpacing,
           ),
           child: Column(
@@ -58,7 +57,7 @@
               if (viewType == TableViewType.pathView ||
                   viewType == TableViewType.singleUrlView)
                 _PathCheckTable(),
-              const SizedBox(height: largeSpacing),
+              const SizedBox(height: extraLargeSpacing),
               if (linkData.domainErrors.isNotEmpty)
                 Align(
                   alignment: Alignment.bottomRight,
@@ -144,7 +143,7 @@
             DataColumn(label: Text('Issue type')),
             DataColumn(label: Text('Status')),
           ],
-          headingRowHeight: areaPaneHeaderHeight,
+          headingRowHeight: defaultHeaderHeight,
           dataRowMinHeight: defaultRowHeight,
           dataRowMaxHeight: defaultRowHeight,
           rows: [
@@ -473,7 +472,7 @@
         Opacity(
           opacity: 0.5,
           child: DataTable(
-            headingRowHeight: areaPaneHeaderHeight,
+            headingRowHeight: defaultHeaderHeight,
             dataRowMinHeight: defaultRowHeight,
             dataRowMaxHeight: defaultRowHeight,
             headingRowColor: MaterialStateProperty.all(
diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart
index b5b0947..2489339 100644
--- a/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart
@@ -5,10 +5,10 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
 
+import '../../shared/common_widgets.dart';
 import '../../shared/console/eval/inspector_tree.dart';
 import '../../shared/diagnostics_text_styles.dart';
 import '../../shared/primitives/utils.dart';
-import '../../shared/utils.dart';
 
 class InspectorBreadcrumbNavigator extends StatelessWidget {
   const InspectorBreadcrumbNavigator({
@@ -23,7 +23,7 @@
   static const _maxNumberOfBreadcrumbs = 5;
 
   final List<InspectorTreeNode> items;
-  final Function(InspectorTreeNode?) onTap;
+  final void Function(InspectorTreeNode?) onTap;
 
   @override
   Widget build(BuildContext context) {
@@ -33,7 +33,7 @@
 
     final breadcrumbs = _generateBreadcrumbs(items);
     return SizedBox(
-      height: isDense() ? 24 : 28,
+      height: Breadcrumb.height,
       child: Padding(
         padding: const EdgeInsets.symmetric(horizontal: 6),
         child: Row(
diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart
index 8564620..2935a05 100644
--- a/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart
@@ -357,16 +357,14 @@
             ),
             const SizedBox(height: denseSpacing),
             ...dialogSubHeader(theme, 'Package Directories'),
-            Text(
-              'Widgets in these directories will show up in your summary tree.',
-              style: theme.subtleTextStyle,
-            ),
             Row(
               mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
-                Text(
-                  '(e.g. /absolute/path/to/myPackage)',
-                  style: theme.subtleTextStyle,
+                Expanded(
+                  child: Text(
+                    'Widgets in these directories will show up in your summary tree.',
+                    style: theme.subtleTextStyle,
+                  ),
                 ),
                 MoreInfoLink(
                   url: DocLinks.inspectorPackageDirectories.value,
@@ -376,6 +374,10 @@
                 ),
               ],
             ),
+            Text(
+              '(e.g. /absolute/path/to/myPackage)',
+              style: theme.subtleTextStyle,
+            ),
             const SizedBox(height: denseSpacing),
             const Expanded(
               child: PubRootDirectorySection(),
@@ -416,9 +418,12 @@
           context,
           Row(
             children: <Widget>[
-              const Padding(
-                padding: EdgeInsets.symmetric(horizontal: denseSpacing),
-                child: Text('Widget Tree'),
+              Padding(
+                padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
+                child: Text(
+                  'Widget Tree',
+                  style: Theme.of(context).textTheme.titleSmall,
+                ),
               ),
               ...!isSearchVisible
                   ? [
@@ -453,7 +458,7 @@
 
   Container _controlsContainer(BuildContext context, Widget child) {
     return Container(
-      height: defaultHeaderHeight(isDense: isDense()),
+      height: defaultHeaderHeight,
       decoration: BoxDecoration(
         border: Border(
           bottom: defaultBorderSide(Theme.of(context)),
@@ -485,7 +490,7 @@
 
   final int? errorIndex;
 
-  final Function(int) onSelectError;
+  final void Function(int) onSelectError;
 
   @override
   Widget build(BuildContext context) {
diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart
index 0e8f7dd..8d8593c 100644
--- a/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart
@@ -68,7 +68,7 @@
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: rowHeight,
+      height: inspectorRowHeight,
       child: InspectorRowContent(
         row: widget.row,
         error: widget.error,
@@ -258,7 +258,7 @@
   }
 
   double getRowOffset(int index) {
-    return (getCachedRow(index)?.depth ?? 0) * columnWidth;
+    return (getCachedRow(index)?.depth ?? 0) * inspectorColumnWidth;
   }
 
   List<InspectorTreeNode> getPathFromSelectedRowToRoot() {
@@ -350,11 +350,11 @@
   double get horizontalPadding => 10.0;
 
   double getDepthIndent(int depth) {
-    return (depth + 1) * columnWidth + horizontalPadding;
+    return (depth + 1) * inspectorColumnWidth + horizontalPadding;
   }
 
   double rowYTop(int index) {
-    return rowHeight * index;
+    return inspectorRowHeight * index;
   }
 
   void nodeChanged(InspectorTreeNode node) {
@@ -405,7 +405,7 @@
 
   int get numRows => root?.subtreeSize ?? 0;
 
-  int getRowIndex(double y) => max(0, y ~/ rowHeight);
+  int getRowIndex(double y) => max(0, y ~/ inspectorRowHeight);
 
   InspectorTreeRow? getRowForNode(InspectorTreeNode node) {
     final rootLocal = root;
@@ -463,7 +463,7 @@
       getDepthIndent(row.depth),
       rowYTop(row.index),
       approximateNodeWidth,
-      rowHeight,
+      inspectorRowHeight,
     );
   }
 
@@ -947,7 +947,7 @@
     required double initialX,
     int padCount = _scrollPadCount,
   }) {
-    return initialX - columnWidth * padCount;
+    return initialX - inspectorColumnWidth * padCount;
   }
 
   /// Pad [initialY] so that a row would be placed in the vertical center of
@@ -955,7 +955,7 @@
   double _padTargetY({
     required double initialY,
   }) {
-    return initialY - (safeViewportHeight / 2) + rowHeight / 2;
+    return initialY - (safeViewportHeight / 2) + inspectorRowHeight / 2;
   }
 
   /// Handle arrow keys for the InspectorTree. Ignore other key events so that
@@ -1049,11 +1049,11 @@
                     offsetController: _scrollControllerX,
                     offsetControllerViewportDimension: viewportWidth,
                     child: ListView.custom(
-                      itemExtent: rowHeight,
+                      itemExtent: inspectorRowHeight,
                       childrenDelegate: SliverChildBuilderDelegate(
                         (context, index) {
                           if (index == treeControllerLocal.numRows) {
-                            return SizedBox(height: rowHeight);
+                            return SizedBox(height: inspectorRowHeight);
                           }
                           final InspectorTreeRow row =
                               treeControllerLocal.getCachedRow(index)!;
@@ -1135,28 +1135,31 @@
     final InspectorTreeNode node = row.node;
     final bool showExpandCollapse = node.showExpandCollapse;
     for (int tick in row.ticks) {
-      currentX = _controller.getDepthIndent(tick) - columnWidth * 0.5;
+      currentX = _controller.getDepthIndent(tick) - inspectorColumnWidth * 0.5;
       // Draw a vertical line for each tick identifying a connection between
       // an ancestor of this node and some other node in the tree.
       canvas.drawLine(
         Offset(currentX, 0.0),
-        Offset(currentX, rowHeight),
+        Offset(currentX, inspectorRowHeight),
         paint,
       );
     }
     // If this row is itself connected to a parent then draw the L shaped line
     // to make that connection.
     if (row.lineToParent) {
-      currentX = _controller.getDepthIndent(row.depth - 1) - columnWidth * 0.5;
-      final double width = showExpandCollapse ? columnWidth * 0.5 : columnWidth;
+      currentX = _controller.getDepthIndent(row.depth - 1) -
+          inspectorColumnWidth * 0.5;
+      final double width = showExpandCollapse
+          ? inspectorColumnWidth * 0.5
+          : inspectorColumnWidth;
       canvas.drawLine(
         Offset(currentX, 0.0),
-        Offset(currentX, rowHeight * 0.5),
+        Offset(currentX, inspectorRowHeight * 0.5),
         paint,
       );
       canvas.drawLine(
-        Offset(currentX, rowHeight * 0.5),
-        Offset(currentX + width, rowHeight * 0.5),
+        Offset(currentX, inspectorRowHeight * 0.5),
+        Offset(currentX + width, inspectorRowHeight * 0.5),
         paint,
       );
     }
@@ -1210,7 +1213,8 @@
 
   @override
   Widget build(BuildContext context) {
-    final double currentX = controller.getDepthIndent(row.depth) - columnWidth;
+    final double currentX =
+        controller.getDepthIndent(row.depth) - inspectorColumnWidth;
     final theme = Theme.of(context);
     final colorScheme = theme.colorScheme;
 
@@ -1260,7 +1264,7 @@
                         controller.requestFocus();
                       },
                       child: SizedBox(
-                        height: rowHeight,
+                        height: inspectorRowHeight,
                         child: DiagnosticsNodeDescription(
                           node.diagnostic,
                           isSelected: row.isSelected,
@@ -1294,7 +1298,7 @@
 
     return CustomPaint(
       painter: _RowPainter(row, controller, colorScheme),
-      size: Size(currentX, rowHeight),
+      size: Size(currentX, inspectorRowHeight),
       child: Align(
         alignment: Alignment.topLeft,
         child: AnimatedBuilder(
diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart
index 12900d2..194cdc9 100644
--- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart
@@ -98,8 +98,7 @@
       );
     }
     return Container(
-      margin: const EdgeInsets.all(margin),
-      padding: const EdgeInsets.only(bottom: margin, right: margin),
+      margin: const EdgeInsets.all(denseSpacing),
       child: AnimatedBuilder(
         animation: changeController,
         builder: (context, _) {
diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart
index c5a9b1b..171b409 100644
--- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart
@@ -8,6 +8,7 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
 
+import '../../../../shared/common_widgets.dart';
 import '../../../../shared/diagnostics/diagnostics_node.dart';
 import '../../../../shared/diagnostics/inspector_service.dart';
 import '../../../../shared/primitives/math_utils.dart';
@@ -21,6 +22,13 @@
 import '../ui/widget_constraints.dart';
 import 'utils.dart';
 
+// TODO(kenz): clean up this file so that we use helper widgets instead of
+// methods that pass around build context.
+
+// TODO(kenz): densify the layout explorer visualization for flex widgets.
+
+double get alignmentDropdownMaxSize => scaleByFontFactor(140.0);
+
 class FlexLayoutExplorerWidget extends LayoutExplorerWidget {
   const FlexLayoutExplorerWidget(
     InspectorController inspectorController, {
@@ -111,7 +119,8 @@
     });
   }
 
-  Widget _buildAxisAlignmentDropdown(Axis axis, ColorScheme colorScheme) {
+  Widget _buildAxisAlignmentDropdown(Axis axis, ThemeData theme) {
+    final colorScheme = theme.colorScheme;
     final color = axis == direction
         ? colorScheme.mainAxisTextColor
         : colorScheme.crossAxisTextColor;
@@ -133,11 +142,12 @@
       quarterTurns: axis == Axis.vertical ? 3 : 0,
       child: Container(
         constraints: BoxConstraints(
-          maxWidth: dropdownMaxSize,
-          maxHeight: dropdownMaxSize,
+          maxWidth: alignmentDropdownMaxSize,
+          maxHeight: defaultButtonHeight,
         ),
         child: DropdownButton(
           value: selected,
+          isDense: true,
           isExpanded: true,
           // Avoid showing an underline for the main axis and cross-axis drop downs.
           underline: const SizedBox(),
@@ -151,15 +161,16 @@
                   mainAxisAlignment: MainAxisAlignment.center,
                   children: [
                     Expanded(
-                      flex: 2,
                       child: Text(
                         alignment.name,
-                        style: TextStyle(color: color),
+                        style: theme.regularTextStyleWithColor(color),
                         textAlign: TextAlign.center,
                         overflow: TextOverflow.ellipsis,
                       ),
                     ),
-                    Flexible(
+                    SizedBox(
+                      height: actionsIconSize,
+                      width: actionsIconSize,
                       child: Image.asset(
                         (axis == direction)
                             ? mainAxisAssetImageUrl(
@@ -191,12 +202,14 @@
                       Expanded(
                         child: Text(
                           alignment.name,
-                          style: TextStyle(color: color),
+                          style: theme.regularTextStyleWithColor(color),
                           textAlign: TextAlign.center,
                           overflow: TextOverflow.ellipsis,
                         ),
                       ),
-                      Flexible(
+                      SizedBox(
+                        height: actionsIconSize,
+                        width: actionsIconSize,
                         child: Image.asset(
                           (axis == direction)
                               ? mainAxisAssetImageUrl(
@@ -258,7 +271,8 @@
   }
 
   Widget _buildLayout(BuildContext context, BoxConstraints constraints) {
-    final colorScheme = Theme.of(context).colorScheme;
+    final theme = Theme.of(context);
+    final colorScheme = theme.colorScheme;
     final maxHeight = constraints.maxHeight;
     final maxWidth = constraints.maxWidth;
     final propertiesLocal = properties!;
@@ -280,11 +294,7 @@
               padding: const EdgeInsets.all(4.0),
               child: Text(
                 'Total Flex Factor: ${propertiesLocal.totalFlex.toInt()}',
-                textScaler: const TextScaler.linear(largeTextScaleFactor),
-                style: const TextStyle(
-                  color: emphasizedTextColor,
-                  fontWeight: FontWeight.bold,
-                ),
+                style: theme.regularTextStyleWithColor(emphasizedTextColor),
                 overflow: TextOverflow.ellipsis,
               ),
             ),
@@ -320,9 +330,8 @@
                       propertiesLocal.verticalDirectionDescription,
                       overflow: TextOverflow.ellipsis,
                       textAlign: TextAlign.center,
-                      textScaler: const TextScaler.linear(largeTextScaleFactor),
-                      style: TextStyle(
-                        color: verticalTextColor(colorScheme),
+                      style: theme.regularTextStyleWithColor(
+                        verticalTextColor(colorScheme),
                       ),
                     ),
                   ),
@@ -331,7 +340,7 @@
             ),
             Truncateable(
               truncate: maxHeight <= minHeightToAllowTruncating,
-              child: _buildAxisAlignmentDropdown(Axis.vertical, colorScheme),
+              child: _buildAxisAlignmentDropdown(Axis.vertical, theme),
             ),
           ],
         ),
@@ -355,15 +364,16 @@
                     propertiesLocal.horizontalDirectionDescription,
                     overflow: TextOverflow.ellipsis,
                     textAlign: TextAlign.center,
-                    textScaler: const TextScaler.linear(largeTextScaleFactor),
-                    style: TextStyle(color: horizontalTextColor(colorScheme)),
+                    style: theme.regularTextStyleWithColor(
+                      horizontalTextColor(colorScheme),
+                    ),
                   ),
                 ),
               ),
             ),
             Truncateable(
               truncate: maxWidth <= minWidthToAllowTruncating,
-              child: _buildAxisAlignmentDropdown(Axis.horizontal, colorScheme),
+              child: _buildAxisAlignmentDropdown(Axis.horizontal, theme),
             ),
           ],
         ),
@@ -432,7 +442,7 @@
     }
 
     if (!widget.properties.hasChildren) {
-      return const Center(child: Text('No Children'));
+      return const CenteredMessage('No Children');
     }
 
     final theme = Theme.of(context);
@@ -593,18 +603,18 @@
     );
   }
 
-  Widget _buildFlexFactorChangerDropdown(int maximumFlexFactor) {
+  Widget _buildFlexFactorChangerDropdown(
+    int maximumFlexFactor,
+    ThemeData theme,
+  ) {
     final propertiesLocal = properties;
 
     Widget buildMenuitemChild(int? flexFactor) {
       return Text(
         'flex: $flexFactor',
         style: flexFactor == propertiesLocal.flexFactor
-            ? const TextStyle(
-                fontWeight: FontWeight.bold,
-                color: emphasizedTextColor,
-              )
-            : const TextStyle(color: emphasizedTextColor),
+            ? theme.boldTextStyle.copyWith(color: emphasizedTextColor)
+            : theme.regularTextStyleWithColor(emphasizedTextColor),
       );
     }
 
@@ -627,10 +637,10 @@
     );
   }
 
-  Widget _buildFlexFitChangerDropdown() {
+  Widget _buildFlexFitChangerDropdown(ThemeData theme) {
     Widget flexFitDescription(FlexFit flexFit) => Text(
           'fit: ${flexFit.name}',
-          style: const TextStyle(color: emphasizedTextColor),
+          style: theme.regularTextStyleWithColor(emphasizedTextColor),
         );
 
     final propertiesLocal = properties;
@@ -660,7 +670,7 @@
     );
   }
 
-  Widget _buildContent(ColorScheme colorScheme) {
+  Widget _buildContent(ThemeData theme) {
     // TODO(https://github.com/flutter/devtools/issues/4058) allow more dynamic
     // flex factor input
     final currentFlexFactor = properties.flexFactor?.toInt() ?? 0;
@@ -676,13 +686,13 @@
         crossAxisAlignment: CrossAxisAlignment.end,
         children: [
           Flexible(
-            child: _buildFlexFactorChangerDropdown(currentMaxFlexFactor),
+            child: _buildFlexFactorChangerDropdown(currentMaxFlexFactor, theme),
           ),
           if (!properties.hasFlexFactor)
             Text(
               'unconstrained ${root.isMainAxisHorizontal ? 'horizontal' : 'vertical'}',
-              style: TextStyle(
-                color: colorScheme.unconstrainedColor,
+              style: theme.regularTextStyle.copyWith(
+                color: theme.colorScheme.unconstrainedColor,
                 fontStyle: FontStyle.italic,
               ),
               maxLines: 2,
@@ -691,7 +701,7 @@
               textScaler: const TextScaler.linear(smallTextScaleFactor),
               textAlign: TextAlign.center,
             ),
-          _buildFlexFitChangerDropdown(),
+          _buildFlexFitChangerDropdown(theme),
         ],
       ),
     );
@@ -731,8 +741,6 @@
       );
     }
 
-    final colorScheme = Theme.of(context).colorScheme;
-
     return Positioned(
       top: renderOffset.dy,
       left: renderOffset.dx,
@@ -756,7 +764,7 @@
                 properties: propertiesLocal,
                 child: Align(
                   alignment: Alignment.topRight,
-                  child: _buildContent(colorScheme),
+                  child: _buildContent(Theme.of(context)),
                 ),
               ),
             ),
diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart
index 15b277c..0ebf49b 100644
--- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart
@@ -5,7 +5,7 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
 
-const margin = 8.0;
+const margin = 6.0;
 
 double get arrowHeadSize => scaleByFontFactor(8.0);
 double get arrowMargin => scaleByFontFactor(4.0);
@@ -38,14 +38,11 @@
 double get minWidthToDisplayWidthInsideArrow => scaleByFontFactor(200.0);
 double get minHeightToDisplayHeightInsideArrow => scaleByFontFactor(200.0);
 
-const largeTextScaleFactor = 1.2;
 const smallTextScaleFactor = 0.8;
 
 /// Height for limiting asset image (selected one in the drop down).
 double get axisAlignmentAssetImageHeight => scaleByFontFactor(24.0);
 
-double get dropdownMaxSize => scaleByFontFactor(220.0);
-
 double get minHeightToAllowTruncating => scaleByFontFactor(375.0);
 double get minWidthToAllowTruncating => scaleByFontFactor(375.0);
 
@@ -117,10 +114,11 @@
 const negativeSpaceLightAssetName =
     'assets/img/layout_explorer/negative_space_light.png';
 
-const dimensionIndicatorTextStyle = TextStyle(
+final dimensionIndicatorTextStyle = TextStyle(
   height: 1.0,
   letterSpacing: 1.1,
   color: emphasizedTextColor,
+  fontSize: defaultFontSize,
 );
 
 TextStyle overflowingDimensionIndicatorTextStyle(ColorScheme colorScheme) =>
diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart
index 257b42c..315aa75 100644
--- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart
@@ -217,8 +217,8 @@
                                 child: Center(
                                   child: Text(
                                     title,
-                                    style: TextStyle(
-                                      color: colorScheme.widgetNameColor,
+                                    style: theme.regularTextStyleWithColor(
+                                      colorScheme.widgetNameColor,
                                     ),
                                     overflow: TextOverflow.ellipsis,
                                   ),
diff --git a/packages/devtools_app/lib/src/screens/logging/_kind_column.dart b/packages/devtools_app/lib/src/screens/logging/_kind_column.dart
index 7da0263..4ace708 100644
--- a/packages/devtools_app/lib/src/screens/logging/_kind_column.dart
+++ b/packages/devtools_app/lib/src/screens/logging/_kind_column.dart
@@ -46,9 +46,7 @@
     }
 
     // Use a font color that contrasts with the colored backgrounds.
-    final textStyle = Theme.of(context).fixedFontStyle.copyWith(
-          color: Colors.white,
-        );
+    final textStyle = Theme.of(context).regularTextStyleWithColor(Colors.white);
 
     return Container(
       padding: const EdgeInsets.symmetric(horizontal: 3.0),
diff --git a/packages/devtools_app/lib/src/screens/logging/_log_details.dart b/packages/devtools_app/lib/src/screens/logging/_log_details.dart
index 9c818b9..f629457 100644
--- a/packages/devtools_app/lib/src/screens/logging/_log_details.dart
+++ b/packages/devtools_app/lib/src/screens/logging/_log_details.dart
@@ -52,23 +52,18 @@
 
   @override
   Widget build(BuildContext context) {
-    return Container(
-      child: _buildContent(context, widget.log),
-    );
-  }
-
-  Widget _buildContent(BuildContext context, LogData? log) {
+    final log = widget.log;
     // TODO(#1370): Handle showing flutter errors in a structured manner.
     return Stack(
       children: [
-        _buildSimpleLog(context, log),
+        _buildSimpleLog(log),
         if (log != null && log.needsComputing)
           const CenteredCircularProgressIndicator(),
       ],
     );
   }
 
-  Widget _buildSimpleLog(BuildContext context, LogData? log) {
+  Widget _buildSimpleLog(LogData? log) {
     final details = log?.details;
     if (details != _lastDetails) {
       if (scrollController.hasClients) {
@@ -78,7 +73,6 @@
       _lastDetails = details;
     }
 
-    final theme = Theme.of(context);
     return RoundedOutlinedBorder(
       clip: true,
       child: ConsoleFrame(
@@ -91,7 +85,6 @@
               child: SelectableText(
                 log?.prettyPrinted() ?? '',
                 textAlign: TextAlign.left,
-                style: theme.fixedFontStyle,
               ),
             ),
           ),
diff --git a/packages/devtools_app/lib/src/screens/logging/_message_column.dart b/packages/devtools_app/lib/src/screens/logging/_message_column.dart
index 4f4f1fa..c73fb68 100644
--- a/packages/devtools_app/lib/src/screens/logging/_message_column.dart
+++ b/packages/devtools_app/lib/src/screens/logging/_message_column.dart
@@ -52,18 +52,16 @@
     bool isRowHovered = false,
     VoidCallback? onPressed,
   }) {
-    final textStyle = Theme.of(context).fixedFontStyle;
     if (data.kind == 'flutter.frame') {
       const Color color = Color.fromARGB(0xff, 0x00, 0x91, 0xea);
       final Text text = Text(
         getDisplayValue(data),
         overflow: TextOverflow.ellipsis,
-        style: textStyle,
       );
 
       double frameLength = 0.0;
       try {
-        final int micros = jsonDecode(data.details!)['elapsed'];
+        final int micros = (jsonDecode(data.details!) as Map)['elapsed'];
         frameLength = micros * 3.0 / 1000.0;
       } catch (e) {
         // ignore
@@ -88,7 +86,7 @@
             // TODO(helin24): Recompute summary length considering ansi codes.
             //  The current summary is generally the first 200 chars of details.
             getDisplayValue(data),
-            textStyle,
+            Theme.of(context).regularTextStyle,
           ),
         ),
         overflow: TextOverflow.ellipsis,
diff --git a/packages/devtools_app/lib/src/screens/logging/_when_column.dart b/packages/devtools_app/lib/src/screens/logging/_when_column.dart
index fd7fd58..0a46614 100644
--- a/packages/devtools_app/lib/src/screens/logging/_when_column.dart
+++ b/packages/devtools_app/lib/src/screens/logging/_when_column.dart
@@ -11,7 +11,7 @@
   WhenColumn()
       : super(
           'When',
-          fixedWidthPx: scaleByFontFactor(120),
+          fixedWidthPx: scaleByFontFactor(100),
         );
 
   @override
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 244f29a..054625d 100644
--- a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart
+++ b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart
@@ -63,6 +63,19 @@
       Future.value(fallback);
 }
 
+const _gcLogKind = 'gc';
+const _flutterFirstFrameKind = 'Flutter.FirstFrame';
+const _flutterFrameworkInitializationKind = 'Flutter.FrameworkInitialization';
+const _verboseFlutterFrameworkLogKinds = [
+  _flutterFirstFrameKind,
+  _flutterFrameworkInitializationKind,
+  _FrameInfo.eventName,
+  _ImageSizesForFrame.eventName,
+];
+const _verboseFlutterServiceLogKinds = [
+  ServiceExtensionStateChangedInfo.eventName,
+];
+
 class LoggingController extends DisposableController
     with
         SearchControllerMixin<LogData>,
@@ -93,11 +106,42 @@
     subscribeToFilterChanges();
   }
 
+  /// The toggle filters available for the Logging screen.
+  @override
+  List<ToggleFilter<LogData>> createToggleFilters() => [
+        if (serviceConnection.serviceManager.connectedApp?.isFlutterAppNow ??
+            true) ...[
+          ToggleFilter<LogData>(
+            name: 'Hide verbose Flutter framework logs (initialization, frame '
+                'times, image sizes)',
+            includeCallback: (log) => !_verboseFlutterFrameworkLogKinds
+                .any((kind) => kind.caseInsensitiveEquals(log.kind)),
+            enabledByDefault: true,
+          ),
+          ToggleFilter<LogData>(
+            name: 'Hide verbose Flutter service logs (service extension state '
+                'changes)',
+            includeCallback: (log) => !_verboseFlutterServiceLogKinds
+                .any((kind) => kind.caseInsensitiveEquals(log.kind)),
+            enabledByDefault: true,
+          ),
+        ],
+        ToggleFilter<LogData>(
+          name: 'Hide garbage collection logs',
+          includeCallback: (log) => !log.kind.caseInsensitiveEquals(_gcLogKind),
+          enabledByDefault: true,
+        ),
+      ];
+
   static const kindFilterId = 'logging-kind-filter';
 
   @override
-  Map<String, QueryFilterArgument> createQueryFilterArgs() => {
-        kindFilterId: QueryFilterArgument(keys: ['kind', 'k']),
+  Map<String, QueryFilterArgument<LogData>> createQueryFilterArgs() => {
+        kindFilterId: QueryFilterArgument<LogData>(
+          keys: ['kind', 'k'],
+          dataValueProvider: (log) => log.kind,
+          substringMatch: true,
+        ),
       };
 
   final StreamController<String> _logStatusController =
@@ -188,23 +232,13 @@
     );
   }
 
-  void _handleExtensionEvent(Event e) async {
+  void _handleExtensionEvent(Event e) {
     // Events to show without a summary in the table.
     const Set<String> untitledEvents = {
-      'Flutter.FirstFrame',
-      'Flutter.FrameworkInitialization',
+      _flutterFirstFrameKind,
+      _flutterFrameworkInitializationKind,
     };
 
-    // TODO(jacobr): make the list of filtered events configurable.
-    const Set<String> filteredEvents = {
-      // Suppress these events by default as they just add noise to the log
-      ServiceExtensionStateChangedInfo.eventName,
-    };
-
-    if (filteredEvents.contains(e.extensionKind)) {
-      return;
-    }
-
     if (e.extensionKind == _FrameInfo.eventName) {
       final _FrameInfo frame = _FrameInfo(e.extensionData!.data);
 
@@ -548,40 +582,45 @@
   @override
   void filterData(Filter<LogData> filter) {
     super.filterData(filter);
-    final queryFilter = filter.queryFilter;
-    if (queryFilter.isEmpty) {
-      filteredData
-        ..clear()
-        ..addAll(data);
-      return;
+
+    bool filterCallback(LogData log) {
+      final filteredOutByToggleFilters = filter.toggleFilters.any(
+        (toggleFilter) =>
+            toggleFilter.enabled.value && !toggleFilter.includeCallback(log),
+      );
+      if (filteredOutByToggleFilters) return false;
+
+      final queryFilter = filter.queryFilter;
+      if (!queryFilter.isEmpty) {
+        final filteredOutByQueryFilterArgument = queryFilter
+            .filterArguments.values
+            .any((argument) => !argument.matchesValue(log));
+        if (filteredOutByQueryFilterArgument) return false;
+
+        if (filter.queryFilter.substringExpressions.isNotEmpty) {
+          for (final substring in filter.queryFilter.substringExpressions) {
+            final matchesKind = log.kind.caseInsensitiveContains(substring);
+            if (matchesKind) return true;
+
+            final matchesSummary = log.summary != null &&
+                log.summary!.caseInsensitiveContains(substring);
+            if (matchesSummary) return true;
+
+            final matchesDetails = log.details != null &&
+                log.details!.caseInsensitiveContains(substring);
+            if (matchesDetails) return true;
+          }
+          return false;
+        }
+      }
+
+      return true;
     }
+
     filteredData
       ..clear()
       ..addAll(
-        data.where((log) {
-          final kindArg = filter.queryFilter.filterArguments[kindFilterId];
-          if (kindArg != null &&
-              !kindArg.matchesValue(log.kind.toLowerCase())) {
-            return false;
-          }
-
-          if (filter.queryFilter.substrings.isNotEmpty) {
-            for (final substring in filter.queryFilter.substrings) {
-              final matchesKind = log.kind.caseInsensitiveContains(substring);
-              if (matchesKind) return true;
-
-              final matchesSummary = log.summary != null &&
-                  log.summary!.caseInsensitiveContains(substring);
-              if (matchesSummary) return true;
-
-              final matchesDetails = log.details != null &&
-                  log.summary!.caseInsensitiveContains(substring);
-              if (matchesDetails) return true;
-            }
-            return false;
-          }
-          return true;
-        }).toList(),
+        data.where(filterCallback).toList(),
       );
   }
 }
@@ -742,7 +781,8 @@
 
   @override
   bool matchesSearchToken(RegExp regExpSearch) {
-    return (summary?.caseInsensitiveContains(regExpSearch) == true) ||
+    return kind.caseInsensitiveContains(regExpSearch) ||
+        (summary?.caseInsensitiveContains(regExpSearch) == true) ||
         (details?.caseInsensitiveContains(regExpSearch) == true);
   }
 
diff --git a/packages/devtools_app/lib/src/screens/logging/logging_screen.dart b/packages/devtools_app/lib/src/screens/logging/logging_screen.dart
index eb1b47e..3dd9fc4 100644
--- a/packages/devtools_app/lib/src/screens/logging/logging_screen.dart
+++ b/packages/devtools_app/lib/src/screens/logging/logging_screen.dart
@@ -215,19 +215,16 @@
     final theme = Theme.of(context);
     return DevToolsDialog(
       title: const DialogTitleText('Logging Settings'),
-      content: SizedBox(
-        width: defaultDialogWidth,
-        child: Column(
-          mainAxisSize: MainAxisSize.min,
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            ...dialogSubHeader(
-              theme,
-              'General',
-            ),
-            const StructuredErrorsToggle(),
-          ],
-        ),
+      content: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          ...dialogSubHeader(
+            theme,
+            'General',
+          ),
+          const StructuredErrorsToggle(),
+        ],
       ),
       actions: const [
         DialogCloseButton(),
diff --git a/packages/devtools_app/lib/src/screens/memory/framework/connected/memory_controller.dart b/packages/devtools_app/lib/src/screens/memory/framework/connected/memory_controller.dart
index 4c26cf8..ab45493 100644
--- a/packages/devtools_app/lib/src/screens/memory/framework/connected/memory_controller.dart
+++ b/packages/devtools_app/lib/src/screens/memory/framework/connected/memory_controller.dart
@@ -27,8 +27,8 @@
     ProfilePaneController? profilePaneController,
   ) {
     memoryTimeline = MemoryTimeline();
-    diff =
-        diffPaneController ?? DiffPaneController(SnapshotTaker(memoryTimeline));
+    diff = diffPaneController ??
+        DiffPaneController(SnapshotTakerRuntime(memoryTimeline));
     profile = profilePaneController ?? ProfilePaneController();
   }
 
@@ -39,7 +39,7 @@
 
   void reset() {
     diff.dispose();
-    diff = DiffPaneController(SnapshotTaker(memoryTimeline));
+    diff = DiffPaneController(SnapshotTakerRuntime(memoryTimeline));
 
     profile.dispose();
     profile = ProfilePaneController();
@@ -210,7 +210,7 @@
 
     // TODO(terry): Used to detect stream being closed from the
     // memoryController dispose method.  Needed when a HOT RELOAD
-    // will call dispose however, spinup (initState) doesn't seem
+    // will call dispose however, initState doesn't seem
     // to happen David is working on scaffolding.
     _memoryTrackerController.stream.listen(
       (_) {},
@@ -290,7 +290,7 @@
     }
   }
 
-  /// Detect stale isolates (sentinaled), may happen after a hot restart.
+  /// Detect stale isolates (sentineled), may happen after a hot restart.
   Future<bool> isIsolateLive(String isolateId) async {
     try {
       final service = serviceConnection.serviceManager.service!;
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_control_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_control_pane.dart
index 58ddfa5..ca76cc7 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_control_pane.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_control_pane.dart
@@ -142,16 +142,20 @@
       gaScreen: gac.memory,
       gaSelection: gac.topicDocumentationButton(_documentationTopic),
       dialogTitle: 'Memory Chart Help',
-      child: Column(
+      actions: [
+        MoreInfoLink(
+          url: DocLinks.chart.value,
+          gaScreenName: '',
+          gaSelectedItemDescription:
+              gac.topicDocumentationLink(_documentationTopic),
+        ),
+      ],
+      child: const Column(
         crossAxisAlignment: CrossAxisAlignment.end,
         children: [
-          const Text('Memory chart shows trace\n'
-              'of application memory usage.'),
-          MoreInfoLink(
-            url: DocLinks.chart.value,
-            gaScreenName: '',
-            gaSelectedItemDescription:
-                gac.topicDocumentationLink(_documentationTopic),
+          Text(
+            'The memory chart shows live and historical\n'
+            ' memory usage statistics for your application.',
           ),
         ],
       ),
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_pane.dart
index f890e14..53637f2 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_pane.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/chart_pane.dart
@@ -589,11 +589,11 @@
       }
     } else if (event[eventName] == devToolsEvent &&
         event.containsKey(customEvent)) {
-      final custom = event[customEvent] as Map<dynamic, dynamic>;
-      final data = custom[customEventData];
+      final custom = event[customEvent] as Map<Object?, Object?>;
+      final data = custom[customEventData] as Map<Object?, Object?>;
       for (var key in data.keys) {
         output.write('$key=');
-        output.writeln(_longValueToShort(data[key]));
+        output.writeln(_longValueToShort(data[key] as String));
       }
     } else {
       output.writeln('Unknown Event ${event[eventName]}');
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/interval_dropdown.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/interval_dropdown.dart
index 00d0536..f7d0533 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/chart/interval_dropdown.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/interval_dropdown.dart
@@ -32,8 +32,6 @@
 
   @override
   Widget build(BuildContext context) {
-    final textTheme = Theme.of(context).textTheme;
-
     final displayTypes =
         ChartInterval.values.map<DropdownMenuItem<ChartInterval>>(
       (
@@ -48,7 +46,6 @@
 
     return RoundedDropDownButton<ChartInterval>(
       isDense: true,
-      style: textTheme.bodyMedium,
       value: controller.displayInterval,
       onChanged: (ChartInterval? newValue) {
         final value = newValue!;
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/memory_android_chart.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/memory_android_chart.dart
index be9e8fb..ca67704 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/chart/memory_android_chart.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/memory_android_chart.dart
@@ -16,8 +16,10 @@
 import '../../shared/primitives/memory_timeline.dart';
 
 class AndroidChartController extends ChartController {
-  AndroidChartController(this._memoryController, {sharedLabels = const <int>[]})
-      : super(
+  AndroidChartController(
+    this._memoryController, {
+    List<int> sharedLabels = const <int>[],
+  }) : super(
           name: 'Android',
           sharedLabelimestamps: sharedLabels,
         );
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/control/settings_dialog.dart b/packages/devtools_app/lib/src/screens/memory/panes/control/settings_dialog.dart
index 7aa52dc..1d718fa 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/control/settings_dialog.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/control/settings_dialog.dart
@@ -53,9 +53,11 @@
                 ),
                 const SizedBox(width: defaultSpacing),
                 SizedBox(
+                  height: defaultTextFieldHeight,
                   width: defaultTextFieldNumberWidth,
                   child: TextField(
-                    decoration: dialogTextFieldDecoration,
+                    style: theme.regularTextStyle,
+                    decoration: singleLineDialogTextFieldDecoration,
                     controller: TextEditingController(
                       text: preferences.memory.refLimit.value.toString(),
                     ),
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/diff_pane_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/diff_pane_controller.dart
index b972b9b..b0a7a27 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/diff_pane_controller.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/diff_pane_controller.dart
@@ -12,6 +12,7 @@
 import '../../../../../shared/analytics/analytics.dart' as ga;
 import '../../../../../shared/analytics/constants.dart' as gac;
 import '../../../../../shared/config_specific/import_export/import_export.dart';
+import '../../../../../shared/file_import.dart';
 import '../../../../../shared/globals.dart';
 import '../../../../../shared/memory/class_name.dart';
 import '../../../shared/heap/class_filter.dart';
@@ -28,10 +29,6 @@
 
   final SnapshotTaker snapshotTaker;
 
-  /// If true, a snapshot is being taken.
-  ValueListenable<bool> get isTakingSnapshot => _isTakingSnapshot;
-  final _isTakingSnapshot = ValueNotifier<bool>(false);
-
   final retainingPathController = RetainingPathController();
 
   final core = CoreData();
@@ -42,7 +39,6 @@
   bool get hasSnapshots => core.snapshots.value.length > 1;
 
   Future<void> takeSnapshot() async {
-    _isTakingSnapshot.value = true;
     ga.select(
       gac.memory,
       gac.MemoryEvent.diffTakeSnapshotControlPane,
@@ -50,9 +46,36 @@
 
     final item = SnapshotInstanceItem(
       displayNumber: _nextDisplayNumber(),
-      isolateName: selectedIsolateName ?? '<isolate-not-detected>',
+      defaultName: selectedIsolateName ?? '<isolate-not-detected>',
     );
 
+    await _addSnapshot(snapshotTaker, item);
+    derived._updateValues();
+  }
+
+  /// Imports snapshots from files.
+  ///
+  /// Opens file selector and loads snapshots from the selected files.
+  Future<void> importSnapshots() async {
+    ga.select(
+      gac.memory,
+      gac.importFile,
+    );
+    final files = await importRawFilesFromPicker();
+    if (files.isEmpty) return;
+
+    final importers = files.map((file) async {
+      final item = SnapshotInstanceItem(defaultName: file.name);
+      await _addSnapshot(SnapshotTakerFromFile(file), item);
+    });
+    await Future.wait(importers);
+    derived._updateValues();
+  }
+
+  Future<void> _addSnapshot(
+    SnapshotTaker snapshotTaker,
+    SnapshotInstanceItem item,
+  ) async {
     final snapshots = core._snapshots;
     snapshots.add(item);
 
@@ -65,8 +88,6 @@
     } finally {
       final newElementIndex = snapshots.value.length - 1;
       core._selectedSnapshotIndex.value = newElementIndex;
-      _isTakingSnapshot.value = false;
-      derived._updateValues();
     }
   }
 
@@ -81,7 +102,7 @@
   }
 
   int _nextDisplayNumber() {
-    final numbers = core._snapshots.value.map((e) => e.displayNumber);
+    final numbers = core._snapshots.value.map((e) => e.displayNumber ?? 0);
     assert(numbers.isNotEmpty);
     return numbers.max + 1;
   }
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/item_controller.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/item_controller.dart
index cd3ce39..1395233 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/item_controller.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/controller/item_controller.dart
@@ -9,10 +9,8 @@
 import '../../../shared/heap/heap.dart';
 
 abstract class SnapshotItem extends DisposableController {
-  /// Number, that if shown in name, should be unique in the list.
-  ///
-  /// If the number is not expected to be shown in UI, it should be 0.
-  int get displayNumber;
+  /// Number to show with auto-generated names that may be non unique, like isolate name.
+  int? get displayNumber;
 
   ValueListenable<bool> get isProcessing => _isProcessing;
   final _isProcessing = ValueNotifier<bool>(false);
@@ -23,7 +21,7 @@
 
 class SnapshotDocItem extends SnapshotItem {
   @override
-  int get displayNumber => 0;
+  int? get displayNumber => null;
 
   @override
   bool get hasData => false;
@@ -31,13 +29,14 @@
 
 class SnapshotInstanceItem extends SnapshotItem {
   SnapshotInstanceItem({
-    required this.displayNumber,
-    required this.isolateName,
+    this.displayNumber,
+    required this.defaultName,
   }) {
     _isProcessing.value = true;
   }
 
-  final String isolateName;
+  /// Automatically assigned name like isolate name or file name.
+  final String defaultName;
 
   AdaptedHeap? heap;
 
@@ -52,9 +51,11 @@
   }
 
   @override
-  final int displayNumber;
+  final int? displayNumber;
 
-  String get name => nameOverride ?? '$isolateName-$displayNumber';
+  String get name =>
+      nameOverride ??
+      '$defaultName${displayNumber == null ? '' : '-$displayNumber'}';
 
   String? nameOverride;
 
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/diff_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/diff_pane.dart
index 03270d5..6518b94 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/diff_pane.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/diff_pane.dart
@@ -11,6 +11,7 @@
 import '../../../../shared/analytics/constants.dart' as gac;
 import '../../../../shared/common_widgets.dart';
 import '../../../../shared/config_specific/launch_url/launch_url.dart';
+import '../../../../shared/globals.dart';
 import '../../../../shared/primitives/simple_items.dart';
 import '../../shared/widgets/shared_memory_widgets.dart';
 import 'controller/diff_pane_controller.dart';
@@ -59,31 +60,48 @@
       builder: (_, item, __) {
         if (item is SnapshotDocItem) {
           return Padding(
-            padding: const EdgeInsets.all(defaultSpacing),
+            padding: const EdgeInsets.all(denseSpacing),
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.start,
               children: [
                 Expanded(
-                  child: ListView(
+                  child: Row(
                     children: [
-                      SizedBox(
-                        height: 450,
+                      Expanded(
                         child: Markdown(
-                          data: _snapshotDocumentation,
+                          data: _snapshotDocumentation(
+                            preferences.darkModeTheme.value,
+                          ),
+                          styleSheet: MarkdownStyleSheet(
+                            p: Theme.of(context).regularTextStyle,
+                          ),
                           onTapLink: (text, url, title) =>
                               unawaited(launchUrl(url!)),
                         ),
                       ),
-                      const ClassTypeLegend(),
+                      const SizedBox(width: densePadding),
+                      Column(
+                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                        crossAxisAlignment: CrossAxisAlignment.end,
+                        children: [
+                          const Padding(
+                            padding: EdgeInsets.only(
+                              top: defaultSpacing,
+                              right: denseSpacing,
+                            ),
+                            child: ClassTypeLegend(),
+                          ),
+                          MoreInfoLink(
+                            url: DocLinks.diff.value,
+                            gaScreenName: gac.memory,
+                            gaSelectedItemDescription:
+                                gac.topicDocumentationLink(_documentationTopic),
+                          ),
+                        ],
+                      ),
                     ],
                   ),
                 ),
-                MoreInfoLink(
-                  url: DocLinks.diff.value,
-                  gaScreenName: gac.memory,
-                  gaSelectedItemDescription:
-                      gac.topicDocumentationLink(_documentationTopic),
-                ),
               ],
             ),
           );
@@ -121,8 +139,12 @@
   }
 }
 
-/// `\v` adds vertical space
-const _snapshotDocumentation = '''
+String _snapshotDocumentation(bool isDark) {
+  final filePostfix = isDark ? 'dark' : 'light';
+  final uploadImageUrl = 'assets/img/doc/upload_$filePostfix.png';
+
+  // `\v` adds vertical space
+  return '''
 Find unexpected memory usage by comparing two heap snapshots:
 
 \v
@@ -131,9 +153,18 @@
 
 \v
 
-2. Take a **heap snapshot** to view current memory allocation:
+2. Use one of the following ways to get a **heap snapshot**:
 
-    a. In the Snapshots panel, click the ● button
+    a. To view current memory allocation click the ● button
+
+    b. To import a snapshot taken with
+    [auto-snapshotting](https://github.com/dart-lang/leak_tracker/blob/main/doc/USAGE.md) or
+    [writeHeapSnapshotToFile](https://api.flutter.dev/flutter/dart-developer/NativeRuntime/writeHeapSnapshotToFile.html)
+    click the ![import]($uploadImageUrl) button
+
+\v
+
+3. Review the snapshot:
 
     b. If you want to refine results, use the **Filter** button
 
@@ -143,19 +174,17 @@
 
 \v
 
-3. Check the **diff** between snapshots to detect allocation issues:
+4. Check the **diff** between snapshots to detect allocation issues:
 
-    a. Take a **snapshot**
-
-    b. Execute the feature in your application
-
-    c. Take a second snapshot. If you are experiencing DevTools crashes due to size of snapshots,
+    a. Get **snapshots** before and after a feature execution.
+       If you are experiencing DevTools crashes due to size of snapshots,
        switch to the [desktop version](https://github.com/flutter/devtools/blob/master/BETA_TESTING.md).
 
-    d. While viewing the second snapshot, click **Diff with:** and select the first snapshot from the drop-down menu;
+    b. While viewing the second snapshot, click **Diff with:** and select the first snapshot from the drop-down menu;
     the results area will display the diff
 
-    e. Use the **Filter** button to refine the diff results, if needed
+    c. Use the **Filter** button to refine the diff results, if needed
 
-    f. Select a class from the diff to view its retaining paths, and see which objects hold the references to those instances
+    d. Select a class from the diff to view its retaining paths, and see which objects hold the references to those instances
 ''';
+}
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details/class_details.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details/class_details.dart
index b7a3834..03f3bfa 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details/class_details.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details/class_details.dart
@@ -5,6 +5,7 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/widgets.dart';
 
+import '../../../../../../shared/common_widgets.dart';
 import '../../../../shared/heap/heap.dart';
 import '../../controller/class_data.dart';
 import 'path.dart';
@@ -30,8 +31,8 @@
   Widget build(BuildContext context) {
     final theEntries = entries;
     if (theEntries == null) {
-      return const Center(
-        child: Text('Click a table row to see retaining paths here.'),
+      return const CenteredMessage(
+        'Click a table row to see retaining paths here.',
       );
     }
 
@@ -46,10 +47,8 @@
       valueListenable: selection,
       builder: (_, selection, __) {
         if (selection == null) {
-          return const Center(
-            child: Text(
-              'Click a table row to see the detailed path.',
-            ),
+          return const CenteredMessage(
+            'Click a table row to see the detailed path.',
           );
         }
 
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details/paths.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details/paths.dart
index bd4c729..cf84837 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details/paths.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/class_details/paths.dart
@@ -39,7 +39,7 @@
           isDiff ? 'Instance\nDelta' : 'Instances',
           titleTooltip: 'Number of instances of the class\n'
               'retained by the path.',
-          fixedWidthPx: scaleByFontFactor(85.0),
+          fixedWidthPx: scaleByFontFactor(80.0),
           alignment: ColumnAlignment.right,
         );
 
@@ -55,7 +55,7 @@
       : super(
           isDiff ? 'Shallow\nSize Delta' : 'Shallow\nDart Size',
           titleTooltip: SizeType.shallow.description,
-          fixedWidthPx: scaleByFontFactor(85.0),
+          fixedWidthPx: scaleByFontFactor(80.0),
           alignment: ColumnAlignment.right,
         );
 
@@ -78,7 +78,7 @@
       : super(
           isDiff ? 'Retained\nSize Delta' : 'Retained\nDart Size',
           titleTooltip: SizeType.retained.description,
-          fixedWidthPx: scaleByFontFactor(85.0),
+          fixedWidthPx: scaleByFontFactor(80.0),
           alignment: ColumnAlignment.right,
         );
 
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart
index 98d7a8a..c550f00 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart
@@ -80,7 +80,7 @@
       : super(
           'Instances',
           titleTooltip: nonGcableInstancesColumnTooltip,
-          fixedWidthPx: scaleByFontFactor(110.0),
+          fixedWidthPx: scaleByFontFactor(80.0),
           alignment: ColumnAlignment.right,
         );
 
@@ -114,7 +114,7 @@
       : super(
           'Shallow Dart Size',
           titleTooltip: SizeType.shallow.description,
-          fixedWidthPx: scaleByFontFactor(140.0),
+          fixedWidthPx: scaleByFontFactor(120.0),
           alignment: ColumnAlignment.right,
         );
 
@@ -137,7 +137,7 @@
       : super(
           'Retained Dart Size',
           titleTooltip: SizeType.retained.description,
-          fixedWidthPx: scaleByFontFactor(140.0),
+          fixedWidthPx: scaleByFontFactor(130.0),
           alignment: ColumnAlignment.right,
         );
 
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/instances.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/instances.dart
index f1dc1a4..64ac0a5 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/instances.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/instances.dart
@@ -77,7 +77,7 @@
     }
 
     MenuItemButton item(
-      title, {
+      String title, {
       required bool subclasses,
       required bool implementers,
     }) =>
@@ -127,15 +127,11 @@
       menuChildren: <Widget>[
         MenuItemButton(
           onPressed: sampler.oneStaticToConsole,
-          child: const Text(
-            'Any',
-          ),
+          child: const Text('Any'),
         ),
         MenuItemButton(
           onPressed: liveItemsEnabled ? sampler.oneLiveStaticToConsole : null,
-          child: const Text(
-            'Any, not garbage collected',
-          ),
+          child: const Text('Any, not garbage collected'),
         ),
       ],
       child: const Text(menuText),
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_control_pane.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_control_pane.dart
index ff40fa9..b1f4208 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_control_pane.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_control_pane.dart
@@ -22,44 +22,39 @@
 
   @override
   Widget build(BuildContext context) {
-    return ValueListenableBuilder<bool>(
-      valueListenable: controller.isTakingSnapshot,
-      builder: (_, isProcessing, __) {
-        final current = controller.core.selectedItem as SnapshotInstanceItem;
-        final heapIsReady = !isProcessing && current.heap != null;
-        if (heapIsReady) {
-          return Row(
-            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+    final current = controller.core.selectedItem as SnapshotInstanceItem;
+    final heapIsReady = current.heap != null;
+    if (heapIsReady) {
+      return Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Row(
             children: [
-              Row(
-                children: [
-                  _DiffDropdown(
-                    current: current,
-                    controller: controller,
-                  ),
-                  const SizedBox(width: defaultSpacing),
-                  DownloadButton(
-                    tooltip: 'Download data in CSV format',
-                    label: 'CSV',
-                    minScreenWidthForTextBeforeScaling:
-                        memoryControlsMinVerboseWidth,
-                    gaScreen: gac.memory,
-                    gaSelection: gac.MemoryEvent.diffSnapshotDownloadCsv,
-                    onPressed: controller.downloadCurrentItemToCsv,
-                  ),
-                ],
+              _DiffDropdown(
+                current: current,
+                controller: controller,
               ),
-              Expanded(
-                child: _SnapshotSizeView(
-                  footprint: current.heap!.footprint,
-                ),
+              const SizedBox(width: defaultSpacing),
+              DownloadButton(
+                tooltip: 'Download data in CSV format',
+                label: 'CSV',
+                minScreenWidthForTextBeforeScaling:
+                    memoryControlsMinVerboseWidth,
+                gaScreen: gac.memory,
+                gaSelection: gac.MemoryEvent.diffSnapshotDownloadCsv,
+                onPressed: controller.downloadCurrentItemToCsv,
               ),
             ],
-          );
-        }
-        return const SizedBox.shrink();
-      },
-    );
+          ),
+          Expanded(
+            child: _SnapshotSizeView(
+              footprint: current.heap!.footprint,
+            ),
+          ),
+        ],
+      );
+    }
+    return const SizedBox.shrink();
   }
 }
 
@@ -103,7 +98,6 @@
           const SizedBox(width: defaultSpacing),
           RoundedDropDownButton<SnapshotInstanceItem>(
             isDense: true,
-            style: Theme.of(context).textTheme.bodyMedium,
             value: current.diffWith.value ?? current,
             onChanged: (SnapshotInstanceItem? value) {
               late SnapshotInstanceItem? newDiffWith;
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_list.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_list.dart
index 2145474..186a799 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_list.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_list.dart
@@ -15,7 +15,6 @@
 import '../../../../../shared/common_widgets.dart';
 import '../../../../../shared/dialogs.dart';
 import '../../../../../shared/primitives/utils.dart';
-import '../../../../../shared/table/table.dart';
 import '../controller/diff_pane_controller.dart';
 import '../controller/item_controller.dart';
 
@@ -73,23 +72,23 @@
 
   @override
   Widget build(BuildContext context) {
-    return ValueListenableBuilder<bool>(
-      valueListenable: controller.isTakingSnapshot,
-      builder: (_, isProcessing, __) {
-        final clearAllEnabled = !isProcessing && controller.hasSnapshots;
-        return Row(
-          children: [
-            ToolbarAction(
-              icon: iconToTakeSnapshot,
-              tooltip: 'Take heap snapshot for the selected isolate',
-              onPressed: controller.isTakingSnapshot.value
-                  ? null
-                  : () => unawaited(_takeSnapshot(context)),
-            ),
-            ToolbarAction(
-              icon: Icons.block,
-              tooltip: 'Clear all snapshots',
-              onPressed: clearAllEnabled
+    return Row(
+      children: [
+        ToolbarAction(
+          icon: iconToTakeSnapshot,
+          size: defaultIconSize,
+          tooltip: 'Take heap snapshot for the selected isolate',
+          onPressed: () => unawaited(_takeSnapshot(context)),
+        ),
+        const SizedBox(width: densePadding),
+        ValueListenableBuilder(
+          valueListenable: controller.core.snapshots,
+          builder: (context, snapshots, _) {
+            return ToolbarAction(
+              icon: Icons.delete,
+              size: defaultIconSize,
+              tooltip: 'Delete all snapshots',
+              onPressed: controller.hasSnapshots
                   ? () {
                       ga.select(
                         gac.memory,
@@ -98,10 +97,16 @@
                       controller.clearSnapshots();
                     }
                   : null,
-            ),
-          ],
-        );
-      },
+            );
+          },
+        ),
+        const Spacer(),
+        ToolbarAction(
+          icon: Icons.file_upload,
+          tooltip: 'Import snapshot(s) from disk',
+          onPressed: () => unawaited(controller.importSnapshots()),
+        ),
+      ],
     );
   }
 }
@@ -278,13 +283,14 @@
 
   @override
   Widget build(BuildContext context) {
+    // TODO(polina-c): start using ellipsis when it is available, https://github.com/flutter/devtools/issues/7130
     return TextField(
       controller: textEditingController,
       focusNode: textFieldFocusNode,
       autofocus: true,
       showCursor: widget.editMode,
       enabled: widget.editMode,
-      style: Theme.of(context).textTheme.bodyMedium,
+      style: Theme.of(context).regularTextStyle,
       decoration: const InputDecoration(
         isDense: true,
         border: InputBorder.none,
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_view.dart
index 4ea382c..97c6a06 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_view.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/snapshot_view.dart
@@ -10,6 +10,7 @@
 import '../../../shared/heap/heap.dart';
 import '../controller/diff_pane_controller.dart';
 import '../controller/heap_diff.dart';
+import '../controller/item_controller.dart';
 import 'class_details/class_details.dart';
 import 'classes_table_diff.dart';
 import 'classes_table_single.dart';
@@ -35,7 +36,8 @@
 
         final classes = controller.derived.heapClasses.value;
         if (classes == null) {
-          return controller.isTakingSnapshot.value
+          final current = controller.core.selectedItem as SnapshotInstanceItem;
+          return current.isProcessing.value
               ? const SizedBox.shrink()
               : const Center(child: Text('Could not take snapshot.'));
         }
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/instances.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/instances.dart
index a5aaab3..cb7a767 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/profile/instances.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/instances.dart
@@ -55,7 +55,7 @@
     const menuText = 'Store all class instances';
 
     MenuItemButton item(
-      title, {
+      String title, {
       required bool subclasses,
       required bool implementers,
     }) =>
@@ -92,9 +92,7 @@
   Widget build(BuildContext context) {
     return MenuItemButton(
       onPressed: sampler.oneLiveToConsole,
-      child: const Text(
-        'Store one instance as a console variable',
-      ),
+      child: const Text('Store one instance as a console variable'),
     );
   }
 }
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart
index bd80542..12564ee 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart
@@ -29,7 +29,7 @@
 
 /// The default width for columns containing *mostly* numeric data (e.g.,
 /// instances, memory).
-const _defaultNumberFieldWidth = 90.0;
+const _defaultNumberFieldWidth = 80.0;
 
 class _FieldClassNameColumn extends ColumnData<ProfileRecord>
     implements
@@ -192,7 +192,8 @@
 }
 
 class _FieldSizeColumn extends ColumnData<ProfileRecord> {
-  factory _FieldSizeColumn({required heap}) => _FieldSizeColumn._(
+  factory _FieldSizeColumn({required HeapGeneration heap}) =>
+      _FieldSizeColumn._(
         title: 'Total Size',
         titleTooltip: "The sum of the type's total shallow memory "
             'consumption in the Dart heap and associated external (e.g., '
@@ -486,7 +487,7 @@
                   // and columns) and one data row. We add a slight padding to
                   // ensure the underlying scrollable area has enough space to not
                   // display a scroll bar.
-                  height: defaultRowHeight + areaPaneHeaderHeight * 2 + 1,
+                  height: defaultRowHeight + defaultHeaderHeight * 2 + 1,
                   child: _GCStatsTable(
                     controller: widget.controller,
                   ),
@@ -703,20 +704,24 @@
       gaScreen: gac.memory,
       gaSelection: gac.topicDocumentationButton(_documentationTopic),
       dialogTitle: 'Memory Allocation Profile Help',
-      child: Column(
+      actions: [
+        MoreInfoLink(
+          url: DocLinks.profile.value,
+          gaScreenName: '',
+          gaSelectedItemDescription:
+              gac.topicDocumentationLink(_documentationTopic),
+        ),
+      ],
+      child: const Column(
         crossAxisAlignment: CrossAxisAlignment.end,
         children: [
-          const Text('The allocation profile tab displays information about\n'
-              'allocated objects in the Dart heap of the selected\n'
-              'isolate.'),
-          const SizedBox(height: denseSpacing),
-          const ClassTypeLegend(),
-          MoreInfoLink(
-            url: DocLinks.profile.value,
-            gaScreenName: '',
-            gaSelectedItemDescription:
-                gac.topicDocumentationLink(_documentationTopic),
+          Text(
+            'The allocation profile tab displays information about\n'
+            'allocated objects in the Dart heap of the selected\n'
+            'isolate.',
           ),
+          SizedBox(height: denseSpacing),
+          ClassTypeLegend(),
         ],
       ),
     );
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/tracing/class_table.dart b/packages/devtools_app/lib/src/screens/memory/panes/tracing/class_table.dart
index 64db6f2..63dd813 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/tracing/class_table.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/tracing/class_table.dart
@@ -18,7 +18,7 @@
 
 /// The default width for columns containing *mostly* numeric data (e.g.,
 /// instances, memory).
-const _defaultNumberFieldWidth = 80.0;
+const _defaultNumberFieldWidth = 70.0;
 
 class _TraceCheckBoxColumn extends ColumnData<TracedClass>
     implements ColumnRenderer<TracedClass> {
@@ -27,7 +27,7 @@
           'Trace',
           titleTooltip:
               'Enable or disable allocation tracing for a specific type',
-          fixedWidthPx: scaleByFontFactor(55.0),
+          fixedWidthPx: scaleByFontFactor(40.0),
           alignment: ColumnAlignment.left,
         );
 
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/tracing/tracing_tree.dart b/packages/devtools_app/lib/src/screens/memory/panes/tracing/tracing_tree.dart
index ecbf34d..286e48f 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/tracing/tracing_tree.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/tracing/tracing_tree.dart
@@ -4,6 +4,7 @@
 
 import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
 
 import '../../../../shared/analytics/constants.dart' as gac;
 import '../../../../shared/common_widgets.dart';
@@ -15,7 +16,7 @@
 import '../../../profiler/panes/cpu_profile_columns.dart';
 import 'tracing_pane_controller.dart';
 
-const double _countColumnWidth = 130;
+const double _countColumnWidth = 100;
 
 /// Displays an allocation profile as a tree of stack frames, displaying
 /// inclusive and exclusive allocation counts.
@@ -73,26 +74,19 @@
             if (selection == null) {
               return const _TracingInstructions();
             } else if (!selection.traceAllocations) {
-              return Column(
-                mainAxisAlignment: MainAxisAlignment.center,
-                children: [
-                  Text(
-                    'Allocation tracing is not enabled for class ${selection.cls.name}.\n',
-                  ),
-                  const _TracingInstructions(),
-                ],
+              return _TracingInstructions(
+                prefix: 'Allocation tracing is not enabled for class '
+                    '${selection.cls.name}.',
               );
             } else if (selection.traceAllocations &&
                 (state.selectedTracedClassAllocationData == null ||
                     state.selectedTracedClassAllocationData!.bottomUpRoots
                         .isEmpty)) {
-              return Column(
-                mainAxisAlignment: MainAxisAlignment.center,
-                children: [
-                  Text(
-                    'No allocation samples have been collected for class ${selection.cls.name}.\n',
-                  ),
-                ],
+              return Padding(
+                padding: const EdgeInsets.all(largeSpacing),
+                child: Text(
+                  'No allocation samples have been collected for class ${selection.cls.name}.\n',
+                ),
               );
             }
             return Column(
@@ -133,39 +127,40 @@
 class _TracingInstructions extends StatelessWidget {
   const _TracingInstructions({
     Key? key,
+    this.prefix,
   }) : super(key: key);
 
+  final String? prefix;
+
   @override
   Widget build(BuildContext context) {
-    return Column(
-      mainAxisAlignment: MainAxisAlignment.center,
-      children: [
-        const Text(
-          'To trace allocations for a class, enable the '
-          'checkbox for that class in the table.',
-        ),
-        Row(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            const Text(
-              'After interacting with your app, come '
-              'back to tab and click the refresh button ',
-            ),
-            Icon(
-              Icons.refresh,
-              size: defaultIconSize,
-            ),
-          ],
-        ),
-        const Text(
-          'to view the tree of collected stack '
-          'traces of constructor calls.',
-        ),
-      ],
+    var data = _tracingInstructions;
+    if (prefix != null) {
+      data = '$prefix\n\n$data';
+    }
+    return Markdown(
+      data: data,
+      styleSheet: MarkdownStyleSheet(
+        p: Theme.of(context).regularTextStyle,
+      ),
     );
   }
 }
 
+/// `\v` adds vertical space
+const _tracingInstructions = '''
+To trace allocations for a class:
+
+\v
+
+1. Enable the 'Trace' checkbox for that class in the table.
+
+2. Interact with your app to trigger an allocation of the class.
+
+3. Click 'Refresh' above to view the tree of collected stack traces of
+constructor calls for the selected class. 
+''';
+
 class _TracingTreeHeader extends StatelessWidget {
   const _TracingTreeHeader({
     Key? key,
@@ -176,7 +171,7 @@
   }) : super(key: key);
 
   final TracingPaneController controller;
-  final Function(VoidCallback) updateTreeStateCallback;
+  final void Function(VoidCallback) updateTreeStateCallback;
   final TabController tabController;
   final List<DevToolsTab> tabs;
 
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/tracing/tracing_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/tracing/tracing_view.dart
index a448857..cc5e0c4 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/tracing/tracing_view.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/tracing/tracing_view.dart
@@ -49,33 +49,24 @@
           controller: widget.controller,
         ),
         Expanded(
-          child: isProfileMode
-              ? OutlineDecoration.onlyTop(
-                  child: const Center(
-                    child: Text(
-                      'Allocation tracing is temporary disabled in profile mode.\n'
-                      'Run the application in debug mode to trace allocations.',
-                    ),
-                  ),
-                )
-              : OutlineDecoration.onlyTop(
-                  child: Split(
-                    axis: Axis.horizontal,
-                    initialFractions: const [0.25, 0.75],
-                    children: [
-                      OutlineDecoration.onlyRight(
-                        child: AllocationTracingTable(
-                          controller: widget.controller,
-                        ),
-                      ),
-                      OutlineDecoration.onlyLeft(
-                        child: AllocationTracingTree(
-                          controller: widget.controller,
-                        ),
-                      ),
-                    ],
+          child: OutlineDecoration.onlyTop(
+            child: Split(
+              axis: Axis.horizontal,
+              initialFractions: const [0.25, 0.75],
+              children: [
+                OutlineDecoration.onlyRight(
+                  child: AllocationTracingTable(
+                    controller: widget.controller,
                   ),
                 ),
+                OutlineDecoration.onlyLeft(
+                  child: AllocationTracingTree(
+                    controller: widget.controller,
+                  ),
+                ),
+              ],
+            ),
+          ),
         ),
       ],
     );
@@ -130,10 +121,18 @@
       gaScreen: gac.memory,
       gaSelection: gac.topicDocumentationButton(_documentationTopic),
       dialogTitle: 'Memory Allocation Tracing Help',
-      child: Column(
+      actions: [
+        MoreInfoLink(
+          url: DocLinks.trace.value,
+          gaScreenName: gac.memory,
+          gaSelectedItemDescription:
+              gac.topicDocumentationLink(_documentationTopic),
+        ),
+      ],
+      child: const Column(
         crossAxisAlignment: CrossAxisAlignment.end,
         children: [
-          const Text(
+          Text(
             'The allocation tracing tab allows for toggling allocation\n'
             'tracing for specific types, which records the locations of\n'
             'allocations of instances of traced types within the\n'
@@ -144,14 +143,8 @@
             'list, displaying a condensed view of locations where objects\n'
             'were allocated.',
           ),
-          const SizedBox(height: denseSpacing),
-          const ClassTypeLegend(),
-          MoreInfoLink(
-            url: DocLinks.trace.value,
-            gaScreenName: gac.memory,
-            gaSelectedItemDescription:
-                gac.topicDocumentationLink(_documentationTopic),
-          ),
+          SizedBox(height: denseSpacing),
+          ClassTypeLegend(),
         ],
       ),
     );
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/class_filter.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/class_filter.dart
index bd2d830..533ce0b 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/heap/class_filter.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/class_filter.dart
@@ -37,7 +37,7 @@
   reuse,
 }
 
-typedef ApplyFilterCallback = Function(ClassFilter);
+typedef ApplyFilterCallback = void Function(ClassFilter);
 
 @immutable
 class ClassFilter {
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart
index b6ef581..5f30c39 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/heap.dart
@@ -46,7 +46,7 @@
     assert(data.allFieldsCalculated);
 
     final result = <HeapClassName, SingleClassStats>{};
-    for (var i in Iterable.generate(data.objects.length)) {
+    for (var i in Iterable<int>.generate(data.objects.length)) {
       if (_uiReleaser.step()) await _uiReleaser.releaseUi();
       final object = data.objects[i];
       final className = object.heapClass;
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart
index dc92b9e..ec2e327 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/model.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:collection/collection.dart';
+import 'package:file_selector/file_selector.dart';
 import 'package:vm_service/vm_service.dart';
 
 import '../../../../shared/analytics/analytics.dart' as ga;
@@ -23,7 +24,7 @@
 
   String toShortString({String? delimiter, bool inverted = false}) => _asString(
         data: classes.map((e) => e.className).toList(),
-        delimiter: _delimeter(
+        delimiter: _delimiter(
           delimiter: delimiter,
           inverted: inverted,
           isLong: false,
@@ -58,7 +59,7 @@
 
     return _asString(
       data: data,
-      delimiter: _delimeter(
+      delimiter: _delimiter(
         delimiter: delimiter,
         inverted: inverted,
         isLong: true,
@@ -67,7 +68,7 @@
     );
   }
 
-  static String _delimeter({
+  static String _delimiter({
     required String? delimiter,
     required bool inverted,
     required bool isLong,
@@ -91,7 +92,7 @@
     return data.join().trim();
   }
 
-  late final _listEquality = const ListEquality().equals;
+  late final _listEquality = const ListEquality<HeapClassName>().equals;
 
   @override
   bool operator ==(Object other) {
@@ -105,12 +106,17 @@
   late final int hashCode = Object.hashAll(classes);
 }
 
+abstract class SnapshotTaker {
+  Future<AdaptedHeapData?> take();
+}
+
 /// This class is needed to make the snapshot taking operation mockable.
-class SnapshotTaker {
-  SnapshotTaker(this._timeline);
+class SnapshotTakerRuntime extends SnapshotTaker {
+  SnapshotTakerRuntime(this._timeline);
 
   final MemoryTimeline? _timeline;
 
+  @override
   Future<AdaptedHeapData?> take() async {
     final snapshot = await snapshotMemoryInSelectedIsolate();
     _timeline?.addSnapshotEvent();
@@ -120,6 +126,18 @@
   }
 }
 
+class SnapshotTakerFromFile implements SnapshotTaker {
+  SnapshotTakerFromFile(this._file);
+
+  final XFile _file;
+
+  @override
+  Future<AdaptedHeapData?> take() async {
+    final bytes = await _file.readAsBytes();
+    return AdaptedHeapData.fromBytes(bytes);
+  }
+}
+
 Future<AdaptedHeapData> _adaptSnapshotGaWrapper(HeapSnapshotGraph graph) async {
   late final AdaptedHeapData result;
   await ga.timeAsync(
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/spanning_tree.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/spanning_tree.dart
index 22f41d1..c564958 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/heap/spanning_tree.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/spanning_tree.dart
@@ -25,7 +25,7 @@
 
 Future<void> _setInboundRefs(AdaptedHeapData heap) async {
   int totalDartSize = 0;
-  for (final from in Iterable.generate(heap.objects.length)) {
+  for (final from in Iterable<int>.generate(heap.objects.length)) {
     totalDartSize += heap.objects[from].shallowSize;
     if (_uiReleaser.step()) await _uiReleaser.releaseUi();
     for (final to in heap.objects[from].outRefs) {
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/primitives/instance_context_menu.dart b/packages/devtools_app/lib/src/screens/memory/shared/primitives/instance_context_menu.dart
index 1e690e9..3ff1b03 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/primitives/instance_context_menu.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/primitives/instance_context_menu.dart
@@ -27,9 +27,15 @@
     const menuButtonWidth = ContextMenuButton.defaultWidth;
 
     return Row(
+      mainAxisSize: MainAxisSize.min,
       mainAxisAlignment: MainAxisAlignment.end,
       children: [
-        Text(nf.format(count)),
+        Expanded(
+          child: Text(
+            nf.format(count),
+            textAlign: TextAlign.end,
+          ),
+        ),
         if (shouldShowMenu)
           ContextMenuButton(
             // ignore: avoid_redundant_argument_values, ensures consistency with [SizedBox] below.
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/primitives/simple_elements.dart b/packages/devtools_app/lib/src/screens/memory/shared/primitives/simple_elements.dart
index 17847ae..630e778 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/primitives/simple_elements.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/primitives/simple_elements.dart
@@ -8,7 +8,7 @@
     "and therefore can't be garbage collected.";
 
 /// When to have verbose Dropdown based on media width.
-const memoryControlsMinVerboseWidth = 950.0;
+const memoryControlsMinVerboseWidth = 240.0;
 
 enum SizeType {
   shallow(
diff --git a/packages/devtools_app/lib/src/screens/memory/shared/widgets/class_filter.dart b/packages/devtools_app/lib/src/screens/memory/shared/widgets/class_filter.dart
index 4c50859..ead40c2 100644
--- a/packages/devtools_app/lib/src/screens/memory/shared/widgets/class_filter.dart
+++ b/packages/devtools_app/lib/src/screens/memory/shared/widgets/class_filter.dart
@@ -67,7 +67,7 @@
   });
 
   final ClassFilter classFilter;
-  final Function(ClassFilter filter) onChanged;
+  final void Function(ClassFilter filter) onChanged;
   final String rootPackage;
 
   @override
@@ -116,7 +116,11 @@
     Widget textField(TextEditingController controller) => Padding(
           padding: EdgeInsets.only(left: textFieldLeftPadding),
           child: TextField(
-            decoration: dialogTextFieldDecoration,
+            style: Theme.of(context).regularTextStyle,
+            decoration: const InputDecoration(
+              contentPadding: EdgeInsets.all(denseSpacing),
+              border: OutlineInputBorder(),
+            ),
             keyboardType: TextInputType.multiline,
             maxLines: null,
             controller: controller,
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 026fcfc..cd79d55 100644
--- a/packages/devtools_app/lib/src/screens/network/network_controller.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart
@@ -36,6 +36,11 @@
   }
 }
 
+enum _NetworkTrafficType {
+  http,
+  socket,
+}
+
 class NetworkController extends DisposableController
     with
         SearchControllerMixin<NetworkRequest>,
@@ -56,10 +61,22 @@
   static const typeFilterId = 'network-type-filter';
 
   @override
-  Map<String, QueryFilterArgument> createQueryFilterArgs() => {
-        methodFilterId: QueryFilterArgument(keys: ['method', 'm']),
-        statusFilterId: QueryFilterArgument(keys: ['status', 's']),
-        typeFilterId: QueryFilterArgument(keys: ['type', 't']),
+  Map<String, QueryFilterArgument<NetworkRequest>> createQueryFilterArgs() => {
+        methodFilterId: QueryFilterArgument<NetworkRequest>(
+          keys: ['method', 'm'],
+          dataValueProvider: (request) => request.method,
+          substringMatch: false,
+        ),
+        statusFilterId: QueryFilterArgument<NetworkRequest>(
+          keys: ['status', 's'],
+          dataValueProvider: (request) => request.status,
+          substringMatch: false,
+        ),
+        typeFilterId: QueryFilterArgument<NetworkRequest>(
+          keys: ['type', 't'],
+          dataValueProvider: (request) => request.type,
+          substringMatch: false,
+        ),
       };
 
   /// Notifies that new Network requests have been processed.
@@ -102,8 +119,13 @@
   /// timeline events.
   late int _timelineMicrosOffset;
 
-  /// The last timestamp at which HTTP and Socket information was refreshed.
-  int lastRefreshMicros = 0;
+  /// The last time at which HTTP information was refreshed.
+  DateTime lastHttpDataRefreshTime = DateTime.fromMicrosecondsSinceEpoch(0);
+
+  /// The last timestamp at which Socket information was refreshed.
+  ///
+  /// This timestamp is on the monotonic clock used by the timeline.
+  int lastSocketDataRefreshMicros = 0;
 
   Timer? _pollingTimer;
 
@@ -111,13 +133,10 @@
   bool get isPolling => _pollingTimer != null;
 
   void _processHttpProfileRequests({
-    required int timelineMicrosOffset,
     required List<HttpProfileRequest> newOrUpdatedHttpRequests,
     required CurrentNetworkRequests currentRequests,
   }) {
-    for (final request in newOrUpdatedHttpRequests) {
-      currentRequests.updateOrAdd(request, timelineMicrosOffset);
-    }
+    newOrUpdatedHttpRequests.forEach(currentRequests.updateOrAdd);
   }
 
   @visibleForTesting
@@ -138,7 +157,6 @@
     }
 
     _processHttpProfileRequests(
-      timelineMicrosOffset: timelineMicrosOffset,
       newOrUpdatedHttpRequests: httpRequests!,
       currentRequests: currentRequests,
     );
@@ -178,7 +196,12 @@
   }
 
   Future<void> startRecording() async {
-    await _startRecording(alreadyRecordingHttp: await recordingHttpTraffic());
+    await _startRecording(
+      alreadyRecordingHttp:
+          await _recordingNetworkTraffic(type: _NetworkTrafficType.http),
+      alreadyRecordingSocketData:
+          await _recordingNetworkTraffic(type: _NetworkTrafficType.socket),
+    );
   }
 
   /// Enables network traffic recording on all isolates and starts polling for
@@ -188,13 +211,17 @@
   /// be the beginning of the process (time 0).
   Future<void> _startRecording({
     bool alreadyRecordingHttp = false,
+    bool alreadyRecordingSocketData = false,
   }) async {
     // Cancel existing polling timer before starting recording.
     _updatePollingState(false);
 
-    final timestamp = await _networkService.updateLastRefreshTime(
+    _networkService.updateLastHttpDataRefreshTime(
       alreadyRecordingHttp: alreadyRecordingHttp,
     );
+    final timestamp = await _networkService.updateLastSocketDataRefreshTime(
+      alreadyRecordingSocketData: alreadyRecordingSocketData,
+    );
 
     // Determine the offset that we'll use to calculate the approximate
     // wall-time a request was made. This won't be 100% accurate, but it should
@@ -228,16 +255,22 @@
     _recordingNotifier.value = state;
   }
 
-  Future<bool> recordingHttpTraffic() async {
+  Future<bool> _recordingNetworkTraffic({
+    required _NetworkTrafficType type,
+  }) async {
     bool enabled = true;
     final service = serviceConnection.serviceManager.service!;
     await service.forEachIsolate(
       (isolate) async {
-        final httpFuture =
-            service.httpEnableTimelineLoggingWrapper(isolate.id!);
+        final future = switch (type) {
+          _NetworkTrafficType.http =>
+            service.httpEnableTimelineLoggingWrapper(isolate.id!),
+          _NetworkTrafficType.socket =>
+            service.socketProfilingEnabledWrapper(isolate.id!),
+        };
         // The above call won't complete immediately if the isolate is paused,
         // so give up waiting after 500ms.
-        final state = await timeout(httpFuture, 500);
+        final state = await timeout(future, 500);
         if (state?.enabled != true) {
           enabled = false;
         }
@@ -291,23 +324,13 @@
       ..clear()
       ..addAll(
         _requests.value.requests.where((NetworkRequest r) {
-          final methodArg = queryFilter.filterArguments[methodFilterId];
-          if (methodArg != null && !methodArg.matchesValue(r.method)) {
-            return false;
-          }
+          final filteredOutByQueryFilterArgument = queryFilter
+              .filterArguments.values
+              .any((argument) => !argument.matchesValue(r));
+          if (filteredOutByQueryFilterArgument) return false;
 
-          final statusArg = queryFilter.filterArguments[statusFilterId];
-          if (statusArg != null && !statusArg.matchesValue(r.status)) {
-            return false;
-          }
-
-          final typeArg = queryFilter.filterArguments[typeFilterId];
-          if (typeArg != null && !typeArg.matchesValue(r.type)) {
-            return false;
-          }
-
-          if (queryFilter.substrings.isNotEmpty) {
-            for (final substring in queryFilter.substrings) {
+          if (queryFilter.substringExpressions.isNotEmpty) {
+            for (final substring in queryFilter.substringExpressions) {
               bool matches(String? stringToMatch) {
                 if (stringToMatch?.caseInsensitiveContains(substring) == true) {
                   _checkForError(r);
@@ -352,12 +375,8 @@
   /// Update or add the [request] to the [requests] depending on whether or not
   /// its [request.id] already exists in the list.
   ///
-  void updateOrAdd(
-    HttpProfileRequest request,
-    int timelineMicrosOffset,
-  ) {
+  void updateOrAdd(HttpProfileRequest request) {
     final wrapped = DartIOHttpRequestData(
-      timelineMicrosOffset,
       request,
       requestFullDataFromVmService: false,
     );
diff --git a/packages/devtools_app/lib/src/screens/network/network_model.dart b/packages/devtools_app/lib/src/screens/network/network_model.dart
index 61bd8e6..5bf973d 100644
--- a/packages/devtools_app/lib/src/screens/network/network_model.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_model.dart
@@ -8,10 +8,6 @@
 import '../../shared/ui/search.dart';
 
 abstract class NetworkRequest with SearchableDataMixin {
-  NetworkRequest(this._timelineMicrosBase);
-
-  final int _timelineMicrosBase;
-
   String get method;
 
   String get uri;
@@ -48,10 +44,6 @@
     return 'Duration: $text';
   }
 
-  int timelineMicrosecondsSinceEpoch(int micros) {
-    return _timelineMicrosBase + micros;
-  }
-
   @override
   bool matchesSearchToken(RegExp regExpSearch) {
     return uri.caseInsensitiveContains(regExpSearch);
@@ -90,9 +82,16 @@
 }
 
 class WebSocket extends NetworkRequest {
-  WebSocket(this._socket, int timelineMicrosBase) : super(timelineMicrosBase);
+  WebSocket(this._socket, this._timelineMicrosBase);
+
+  final int _timelineMicrosBase;
 
   final SocketStatistic _socket;
+
+  int timelineMicrosecondsSinceEpoch(int micros) {
+    return _timelineMicrosBase + micros;
+  }
+
   @override
   String get id => _socket.id;
 
diff --git a/packages/devtools_app/lib/src/screens/network/network_request_inspector.dart b/packages/devtools_app/lib/src/screens/network/network_request_inspector.dart
index a3913c9..e26602f 100644
--- a/packages/devtools_app/lib/src/screens/network/network_request_inspector.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_request_inspector.dart
@@ -22,19 +22,6 @@
   static const _responseTabTitle = 'Response';
   static const _cookiesTabTitle = 'Cookies';
 
-  // TODO(kenz): remove these keys and use a text finder to lookup widgets in test.
-
-  @visibleForTesting
-  static const overviewTabKey = Key(_overviewTabTitle);
-  @visibleForTesting
-  static const headersTabKey = Key(_headersTabTitle);
-  @visibleForTesting
-  static const responseTabKey = Key(_responseTabTitle);
-  @visibleForTesting
-  static const cookiesTabKey = Key(_cookiesTabTitle);
-  @visibleForTesting
-  static const noRequestSelectedKey = Key('No Request Selected');
-
   final NetworkController controller;
 
   DevToolsTab _buildTab({required String tabName, Widget? trailing}) {
@@ -55,8 +42,7 @@
               ? Center(
                   child: Text(
                     'No request selected',
-                    key: NetworkRequestInspector.noRequestSelectedKey,
-                    style: Theme.of(context).textTheme.titleLarge,
+                    style: Theme.of(context).regularTextStyle,
                   ),
                 )
               : AnalyticsTabbedView(
@@ -119,9 +105,7 @@
             ),
           if (data.hasCookies)
             (
-              tab: _buildTab(
-                tabName: NetworkRequestInspector._cookiesTabTitle,
-              ),
+              tab: _buildTab(tabName: NetworkRequestInspector._cookiesTabTitle),
               tabView: HttpRequestCookiesView(data),
             ),
         ],
diff --git a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
index e5a7d74..b1b5cc6 100644
--- a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
@@ -13,7 +13,6 @@
 import '../../shared/http/http.dart';
 import '../../shared/http/http_request_data.dart';
 import '../../shared/primitives/utils.dart';
-import '../../shared/table/table.dart';
 import '../../shared/ui/colors.dart';
 import 'network_controller.dart';
 import 'network_model.dart';
@@ -162,7 +161,8 @@
         }
 
         final isJson = switch (requestContentType) {
-          List() => requestContentType.any((e) => e.contains('json')),
+          List() =>
+            requestContentType.any((e) => (e as String).contains('json')),
           String() => requestContentType.contains('json'),
           _ => throw StateError(
               "Expected 'content-type' to be a List or String, but got: "
@@ -786,8 +786,7 @@
     }
     final duration = Duration(
       microseconds: data.endTimestamp!.microsecondsSinceEpoch -
-          data.instantEvents.last.timestampMicros -
-          data.timelineMicrosecondsSinceEpoch(0),
+          data.instantEvents.last.timestamp.microsecondsSinceEpoch,
     );
     timingWidgets.add(
       _buildTimingRow(nextColor(), 'Response', duration),
diff --git a/packages/devtools_app/lib/src/screens/network/network_service.dart b/packages/devtools_app/lib/src/screens/network/network_service.dart
index 963a296..5e78a4d 100644
--- a/packages/devtools_app/lib/src/screens/network/network_service.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_service.dart
@@ -13,27 +13,43 @@
 
   final NetworkController networkController;
 
-  /// Updates the last refresh time to the current time.
+  /// Updates the last Socket data refresh time to the current time.
   ///
-  /// If [alreadyRecordingHttp] is true it's unclear when the last refresh time
-  /// would have occurred, so the refresh time is not updated. Otherwise,
-  /// [NetworkController.lastRefreshMicros] is updated to the current
-  /// timeline timestamp.
+  /// If [alreadyRecordingSocketData] is true, it's unclear when the last
+  /// refresh time would have occurred, so the refresh time is not updated.
+  /// Otherwise, [NetworkController.lastSocketDataRefreshMicros] is updated to
+  /// the current timeline timestamp.
   ///
-  /// Returns the current timestamp.
-  Future<int> updateLastRefreshTime({bool alreadyRecordingHttp = false}) async {
+  /// Returns the current timeline timestamp.
+  Future<int> updateLastSocketDataRefreshTime({
+    bool alreadyRecordingSocketData = false,
+  }) async {
     // Set the current timeline time as the time of the last refresh.
     final timestampObj =
         await serviceConnection.serviceManager.service!.getVMTimelineMicros();
 
     final timestamp = timestampObj.timestamp!;
-    if (!alreadyRecordingHttp) {
-      // Only include HTTP requests issued after the current time.
-      networkController.lastRefreshMicros = timestamp;
+    if (!alreadyRecordingSocketData) {
+      // Only include Socket requests issued after the current time.
+      networkController.lastSocketDataRefreshMicros = timestamp;
     }
     return timestamp;
   }
 
+  /// Updates the last HTTP data refresh time to the current time.
+  ///
+  /// If [alreadyRecordingHttp] is true it's unclear when the last refresh time
+  /// would have occurred, so the refresh time is not updated. Otherwise,
+  /// [NetworkController.lastHttpDataRefreshTime] is updated to the current
+  /// time.
+  void updateLastHttpDataRefreshTime({
+    bool alreadyRecordingHttp = false,
+  }) {
+    if (!alreadyRecordingHttp) {
+      networkController.lastHttpDataRefreshTime = DateTime.now();
+    }
+  }
+
   /// Force refreshes the HTTP requests logged to the timeline as well as any
   /// recorded Socket traffic.
   Future<void> refreshNetworkData() async {
@@ -41,10 +57,11 @@
     final timestampObj =
         await serviceConnection.serviceManager.service!.getVMTimelineMicros();
     final timestamp = timestampObj.timestamp!;
-    final sockets = await _refreshSockets(timestamp);
+    final sockets = await _refreshSockets();
+    networkController.lastSocketDataRefreshMicros = timestamp;
     List<HttpProfileRequest>? httpRequests;
     httpRequests = await _refreshHttpProfile();
-    networkController.lastRefreshMicros = timestamp;
+    networkController.lastHttpDataRefreshTime = DateTime.now();
     networkController.processNetworkTraffic(
       sockets: sockets,
       httpRequests: httpRequests,
@@ -59,7 +76,7 @@
     await service.forEachIsolate((isolate) async {
       final request = await service.getHttpProfileWrapper(
         isolate.id!,
-        updatedSince: networkController.lastRefreshMicros,
+        updatedSince: networkController.lastHttpDataRefreshTime,
       );
       requests.addAll(request.requests);
     });
@@ -80,7 +97,7 @@
     });
   }
 
-  Future<List<SocketStatistic>> _refreshSockets(int lastRefreshMicros) async {
+  Future<List<SocketStatistic>> _refreshSockets() async {
     final service = serviceConnection.serviceManager.service;
     if (service == null) return [];
     final sockets = <SocketStatistic>[];
@@ -94,10 +111,14 @@
     return sockets
         .where(
           (element) =>
-              element.startTime > lastRefreshMicros ||
-              (element.endTime ?? 0) > lastRefreshMicros ||
-              (element.lastReadTime ?? 0) > lastRefreshMicros ||
-              (element.lastWriteTime ?? 0) > lastRefreshMicros,
+              element.startTime >
+                  networkController.lastSocketDataRefreshMicros ||
+              (element.endTime ?? 0) >
+                  networkController.lastSocketDataRefreshMicros ||
+              (element.lastReadTime ?? 0) >
+                  networkController.lastSocketDataRefreshMicros ||
+              (element.lastWriteTime ?? 0) >
+                  networkController.lastSocketDataRefreshMicros,
         )
         .toList();
   }
@@ -142,7 +163,8 @@
   }
 
   Future<void> clearData() async {
-    await updateLastRefreshTime();
+    await updateLastSocketDataRefreshTime();
+    updateLastHttpDataRefreshTime();
     await _clearSocketProfile();
     await _clearHttpProfile();
   }
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/controls/enhance_tracing/enhance_tracing.dart b/packages/devtools_app/lib/src/screens/performance/panes/controls/enhance_tracing/enhance_tracing.dart
index 4c64eb8..5867b94 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/controls/enhance_tracing/enhance_tracing.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/controls/enhance_tracing/enhance_tracing.dart
@@ -285,11 +285,14 @@
           ),
         ),
         if (docsUrl != null)
-          MoreInfoLink(
-            url: docsUrl,
-            gaScreenName: extension.gaScreenName!,
-            gaSelectedItemDescription: extension.gaDocsItem!,
-            padding: const EdgeInsets.symmetric(vertical: denseSpacing),
+          Padding(
+            padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
+            child: MoreInfoLink(
+              url: docsUrl,
+              gaScreenName: extension.gaScreenName!,
+              gaSelectedItemDescription: extension.gaDocsItem!,
+              padding: const EdgeInsets.symmetric(vertical: denseSpacing),
+            ),
           ),
       ],
     );
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/controls/more_debugging_options.dart b/packages/devtools_app/lib/src/screens/performance/panes/controls/more_debugging_options.dart
index 6782372..07c991e 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/controls/more_debugging_options.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/controls/more_debugging_options.dart
@@ -14,7 +14,7 @@
 class MoreDebuggingOptionsButton extends StatelessWidget {
   const MoreDebuggingOptionsButton({Key? key}) : super(key: key);
 
-  static const _width = 720.0;
+  static const _width = 620.0;
 
   @override
   Widget build(BuildContext context) {
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_settings.dart b/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_settings.dart
index c68ab70..72c134b 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_settings.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_settings.dart
@@ -20,28 +20,24 @@
     return DevToolsDialog(
       title: const DialogTitleText('Performance Settings'),
       includeDivider: false,
-      content: SizedBox(
-        width: defaultDialogWidth,
-        child: Column(
-          mainAxisSize: MainAxisSize.min,
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            if (serviceConnection
-                .serviceManager.connectedApp!.isFlutterAppNow!) ...[
-              FlutterSettings(
-                flutterFramesController: controller.flutterFramesController,
-              ),
-              const SizedBox(height: denseSpacing),
-            ],
-            CheckboxSetting(
-              notifier:
-                  controller.timelineEventsController.useLegacyTraceViewer,
-              title: 'Use legacy trace viewer',
-              onChanged: controller
-                  .timelineEventsController.toggleUseLegacyTraceViewer,
+      content: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          if (serviceConnection
+              .serviceManager.connectedApp!.isFlutterAppNow!) ...[
+            FlutterSettings(
+              flutterFramesController: controller.flutterFramesController,
             ),
+            const SizedBox(height: denseSpacing),
           ],
-        ),
+          CheckboxSetting(
+            notifier: controller.timelineEventsController.useLegacyTraceViewer,
+            title: 'Use legacy trace viewer',
+            onChanged:
+                controller.timelineEventsController.toggleUseLegacyTraceViewer,
+          ),
+        ],
       ),
       actions: const [
         DialogCloseButton(),
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart
index c801532..dc7e2a4 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart
@@ -636,14 +636,15 @@
 
   @override
   Widget build(BuildContext context) {
+    final textStyle = Theme.of(context).regularTextStyle;
     return HoverCardTooltip.sync(
       enabled: () => true,
-      generateHoverCardData: (_) => _buildCardData(),
+      generateHoverCardData: (_) => _buildCardData(textStyle),
       child: child,
     );
   }
 
-  HoverCardData _buildCardData() {
+  HoverCardData _buildCardData(TextStyle textStyle) {
     final uiText = 'UI: ${durationText(
       frame.buildTime,
       unit: DurationDisplayUnit.milliseconds,
@@ -663,41 +664,48 @@
         : '';
     return HoverCardData(
       position: HoverCardPosition.element,
-      width: _calculateTooltipWidth([uiText, rasterText, shaderText]),
+      width: _calculateTooltipWidth(
+        [uiText, rasterText, shaderText],
+        textStyle,
+      ),
       contents: Material(
-        child: Column(
-          mainAxisSize: MainAxisSize.min,
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Text(uiText),
-            const SizedBox(height: densePadding),
-            Text(rasterText),
-            if (hasShaderJank)
-              Row(
-                children: [
-                  const Icon(
-                    Icons.subdirectory_arrow_right,
-                    size: defaultIconSizeBeforeScaling,
-                  ),
-                  Text(shaderText),
-                  MoreInfoLink(
-                    url: preCompileShadersDocsUrl,
-                    gaScreenName: gac.performance,
-                    gaSelectedItemDescription: gac
-                        .PerformanceDocs.shaderCompilationDocsTooltipLink.name,
-                  ),
-                ],
-              ),
-          ],
+        child: DefaultTextStyle(
+          style: textStyle,
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(uiText),
+              const SizedBox(height: densePadding),
+              Text(rasterText),
+              if (hasShaderJank)
+                Row(
+                  children: [
+                    const Icon(
+                      Icons.subdirectory_arrow_right,
+                      size: defaultIconSizeBeforeScaling,
+                    ),
+                    Text(shaderText),
+                    MoreInfoLink(
+                      url: preCompileShadersDocsUrl,
+                      gaScreenName: gac.performance,
+                      gaSelectedItemDescription: gac.PerformanceDocs
+                          .shaderCompilationDocsTooltipLink.name,
+                    ),
+                  ],
+                ),
+            ],
+          ),
         ),
       ),
     );
   }
 
-  double _calculateTooltipWidth(List<String> lines) {
+  double _calculateTooltipWidth(List<String> lines, TextStyle style) {
     var maxWidth = 0.0;
     for (final line in lines) {
-      final lineWidth = calculateTextSpanWidth(TextSpan(text: line));
+      final lineWidth =
+          calculateTextSpanWidth(TextSpan(text: line, style: style));
       maxWidth = math.max(maxWidth, lineWidth);
     }
     // Add (2 * denseSpacing) for the card padding, and add
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis.dart
index 0656687..7a94f3f 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis.dart
@@ -7,6 +7,7 @@
 
 import '../../../../service/service_extension_widgets.dart';
 import '../../../../service/service_extensions.dart' as extensions;
+import '../../../../shared/common_widgets.dart';
 import '../../../../shared/feature_flags.dart';
 import '../../../../shared/globals.dart';
 import '../controls/enhance_tracing/enhance_tracing_controller.dart';
@@ -34,14 +35,11 @@
   Widget build(BuildContext context) {
     final frameAnalysis = this.frameAnalysis;
     if (frameAnalysis == null) {
-      return const Center(
-        child: Text(
-          'No analysis data available for this frame. This means that the '
-          'timeline events\nfor this frame occurred too long ago and DevTools '
-          'could not access them.\n\nTo avoid this, open the DevTools Performance '
-          'page earlier.',
-          textAlign: TextAlign.center,
-        ),
+      return const CenteredMessage(
+        'No analysis data available for this frame. This means that the '
+        'timeline events\nfor this frame occurred too long ago and DevTools '
+        'could not access them.\n\nTo avoid this, open the DevTools Performance '
+        'page earlier.',
       );
     }
     final rebuilds = rebuildCountModel.rebuildsForFrame(frameAnalysis.frame.id);
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_hints.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_hints.dart
index a8e0e8c..b9f1266 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_hints.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_hints.dart
@@ -59,7 +59,7 @@
             if (intrinsicOperationsCount > 0)
               IntrinsicOperationsHint(intrinsicOperationsCount),
           ]
-        : [];
+        : <Widget>[];
     final rasterHints = showRasterJankHints
         ? [
             const Text('Raster Jank Detected'),
@@ -71,7 +71,7 @@
             const SizedBox(height: denseSpacing),
             const RasterStatsHint(),
           ]
-        : [];
+        : <Widget>[];
 
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart b/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart
index ea20d1f..3debad4 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:collection/collection.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/foundation.dart';
 
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/legacy/timeline_flame_chart.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/legacy/timeline_flame_chart.dart
index 66fd2b1..2e3df68 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/legacy/timeline_flame_chart.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/legacy/timeline_flame_chart.dart
@@ -122,7 +122,7 @@
     required ValueListenable<TimelineEvent?> selectionNotifier,
     required ValueListenable<List<TimelineEvent>> searchMatchesNotifier,
     required ValueListenable<TimelineEvent?> activeSearchMatchNotifier,
-    required Function(TimelineEvent event) onDataSelected,
+    required void Function(TimelineEvent event) onDataSelected,
   }) : super(
           data,
           time: data.time,
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart
index 07ae519..5be1a77 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart
@@ -6,7 +6,7 @@
 import 'dart:ui_web' as ui_web;
 
 import 'package:flutter/foundation.dart';
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import '../../../../../shared/globals.dart';
 import '../../../../../shared/primitives/trace_event.dart';
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
index 5517750..1b28938 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_web.dart
@@ -10,7 +10,7 @@
 import 'package:devtools_app_shared/web_utils.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import '../../../../../shared/analytics/analytics.dart' as ga;
 import '../../../../../shared/analytics/constants.dart' as gac;
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_view.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_view.dart
index 54f2676..717cce5 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_view.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_view.dart
@@ -100,9 +100,9 @@
             if (!offlineController.offlineMode.value) ...[
               // TODO(kenz): add a switch to enable the CPU profiler once the
               // tracing format supports it (when we switch to protozero).
-              const SizedBox(width: denseSpacing),
+              const SizedBox(width: densePadding),
               TraceCategoriesButton(controller: controller),
-              const SizedBox(width: denseSpacing),
+              const SizedBox(width: densePadding),
               RefreshTimelineEventsButton(controller: controller),
             ],
           ],
diff --git a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart
index 10a12c9..53fe775 100644
--- a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart
+++ b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart
@@ -198,7 +198,7 @@
     'https://developer.chrome.com/docs/devtools/performance';
 
 const flutterWebInstructionsMd = '''
-# How to use Chrome DevTools for performance profiling
+## How to use Chrome DevTools for performance profiling
 
 The Flutter framework emits timeline events as it works to build frames, draw 
 scenes, and track other activity such as garbage collections. These events are 
@@ -208,14 +208,14 @@
 [Timeline]($timelineLink) and [TimelineTask]($timelineTaskLink) APIs for further
 performance analysis.
 
-## Optional flags to enhance tracing
+### Optional flags to enhance tracing
 
 - [debugProfileBuildsEnabled]($debugBuildsLink): Adds Timeline events for every Widget built.
 - [debugProfileBuildsEnabledUserWidgets]($debugUserBuildsLink): Adds Timeline events for every user-created Widget built.
 - [debugProfileLayoutsEnabled]($debugLayoutsLink): Adds Timeline events for every RenderObject layout.
 - [debugProfilePaintsEnabled]($debugPaintsLink): Adds Timeline events for every RenderObject painted.
 
-## Instructions
+### Instructions
 
 1. *[Optional]* Set any desired tracing flags to true from your app's main method.
 2. Run your Flutter web app in [profile mode]($profileModeLink).
@@ -224,7 +224,7 @@
 ''';
 
 const dartWebInstructionsMd = '''
-# How to use Chrome DevTools for performance profiling
+## How to use Chrome DevTools for performance profiling
 
 Any events emitted using the `dart:developer` [Timeline]($timelineLink) and 
 [TimelineTask]($timelineTaskLink) APIs are exposed in the Chrome DevTools 
diff --git a/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart b/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart
index 975c756..e870f9a 100644
--- a/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart
+++ b/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart
@@ -139,8 +139,8 @@
                         controller.enhanceTracingController,
                     rebuildCountModel: controller.data!.rebuildCountModel,
                   )
-                : const Center(
-                    child: Text('Select a frame above to view analysis data.'),
+                : const CenteredMessage(
+                    'Select a frame above to view analysis data.',
                   ),
           ),
           featureController: null,
diff --git a/packages/devtools_app/lib/src/screens/profiler/cpu_profile_service.dart b/packages/devtools_app/lib/src/screens/profiler/cpu_profile_service.dart
index 88332a6..dc184dc 100644
--- a/packages/devtools_app/lib/src/screens/profiler/cpu_profile_service.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/cpu_profile_service.dart
@@ -49,7 +49,8 @@
       for (int i = 0; i < samples.length; ++i) {
         final cpuSample = samples[i];
         final rawSample = rawSamples[i];
-        cpuSample.setCodeStack(rawSample[kCodeStack].cast<int>());
+        final codeStack = (rawSample[kCodeStack] as List).cast<int>();
+        cpuSample.setCodeStack(codeStack);
       }
     }
 
diff --git a/packages/devtools_app/lib/src/screens/profiler/cpu_profiler_controller.dart b/packages/devtools_app/lib/src/screens/profiler/cpu_profiler_controller.dart
index da6d567..0e51455 100644
--- a/packages/devtools_app/lib/src/screens/profiler/cpu_profiler_controller.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/cpu_profiler_controller.dart
@@ -159,7 +159,11 @@
 
   @override
   Map<String, QueryFilterArgument> createQueryFilterArgs() => {
-        uriFilterId: QueryFilterArgument(keys: ['uri', 'u']),
+        uriFilterId: QueryFilterArgument<CpuStackFrame>(
+          keys: ['uri', 'u'],
+          dataValueProvider: (stackFrame) => stackFrame.packageUri,
+          substringMatch: true,
+        ),
       };
 
   int selectedProfilerTabIndex = 0;
@@ -680,17 +684,13 @@
 
       final queryFilter = filter.queryFilter;
       if (!queryFilter.isEmpty) {
-        final uriArg = queryFilter.filterArguments[uriFilterId];
-        if (uriArg != null &&
-            !uriArg.matchesValue(
-              stackFrame.packageUri,
-              substringMatch: true,
-            )) {
-          return false;
-        }
+        final filteredOutByQueryFilterArgument = queryFilter
+            .filterArguments.values
+            .any((argument) => !argument.matchesValue(stackFrame));
+        if (filteredOutByQueryFilterArgument) return false;
 
-        if (queryFilter.substrings.isNotEmpty) {
-          for (final substring in queryFilter.substrings) {
+        if (queryFilter.substringExpressions.isNotEmpty) {
+          for (final substring in queryFilter.substringExpressions) {
             bool matches(String? stringToMatch) {
               return stringToMatch?.caseInsensitiveContains(substring) ?? false;
             }
diff --git a/packages/devtools_app/lib/src/screens/profiler/panes/controls/cpu_profiler_controls.dart b/packages/devtools_app/lib/src/screens/profiler/panes/controls/cpu_profiler_controls.dart
index 04845a7..b7617c1 100644
--- a/packages/devtools_app/lib/src/screens/profiler/panes/controls/cpu_profiler_controls.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/panes/controls/cpu_profiler_controls.dart
@@ -71,7 +71,6 @@
               builder: (context, vmDeveloperModeEnabled, _) {
                 return RoundedDropDownButton<String>(
                   isDense: true,
-                  style: Theme.of(context).textTheme.bodyMedium,
                   value: userTag,
                   items: [
                     _buildMenuItem(
@@ -158,7 +157,6 @@
             message: tooltip,
             child: RoundedDropDownButton<CpuProfilerViewType>(
               isDense: true,
-              style: Theme.of(context).textTheme.bodyMedium,
               value: viewType,
               items: [
                 _buildMenuItem(
diff --git a/packages/devtools_app/lib/src/screens/profiler/panes/cpu_profile_columns.dart b/packages/devtools_app/lib/src/screens/profiler/panes/cpu_profile_columns.dart
index b679dd0..613a8bd 100644
--- a/packages/devtools_app/lib/src/screens/profiler/panes/cpu_profile_columns.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/panes/cpu_profile_columns.dart
@@ -58,9 +58,6 @@
   bool get supportsSorting => true;
 
   @override
-  String getTooltip(CpuStackFrame dataObject) => '';
-
-  @override
   Widget? build(
     BuildContext context,
     CpuStackFrame data, {
diff --git a/packages/devtools_app/lib/src/screens/profiler/panes/method_table/method_table.dart b/packages/devtools_app/lib/src/screens/profiler/panes/method_table/method_table.dart
index b1cc82e..7429977 100644
--- a/packages/devtools_app/lib/src/screens/profiler/panes/method_table/method_table.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/panes/method_table/method_table.dart
@@ -13,6 +13,8 @@
 import 'method_table_controller.dart';
 import 'method_table_model.dart';
 
+final _methodColumnMinWidth = scaleByFontFactor(800.0);
+
 /// Widget that displays a method table for a CPU profile.
 class CpuMethodTable extends StatelessWidget {
   const CpuMethodTable({super.key, required this.methodTableController});
@@ -146,7 +148,10 @@
           DevToolsTooltip(
             message: selectedNodeDisplay,
             child: Padding(
-              padding: const EdgeInsets.all(denseSpacing),
+              padding: const EdgeInsets.symmetric(
+                horizontal: denseSpacing,
+                vertical: densePadding,
+              ),
               child: MethodAndSourceDisplay(
                 methodName: selectedNode.name,
                 packageUri: selectedNode.packageUri,
@@ -243,7 +248,11 @@
 
 class _MethodColumn extends ColumnData<MethodTableGraphNode>
     implements ColumnRenderer<MethodTableGraphNode> {
-  _MethodColumn() : super.wide('Method');
+  _MethodColumn()
+      : super.wide(
+          'Method',
+          minWidthPx: _methodColumnMinWidth,
+        );
 
   @override
   bool get supportsSorting => true;
@@ -273,8 +282,8 @@
   }
 }
 
-const _totalAndSelfColumnWidth = 75.0;
-const _callGraphColumnWidth = 80.0;
+const _totalAndSelfColumnWidth = 60.0;
+const _callGraphColumnWidth = 70.0;
 
 class _SelfTimeColumn extends TimeAndPercentageColumn<MethodTableGraphNode> {
   _SelfTimeColumn({String? titleTooltip})
diff --git a/packages/devtools_app/lib/src/screens/profiler/profiler_status.dart b/packages/devtools_app/lib/src/screens/profiler/profiler_status.dart
index e6a9c6e..8ec4677 100644
--- a/packages/devtools_app/lib/src/screens/profiler/profiler_status.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/profiler_status.dart
@@ -5,6 +5,7 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
 
+import '../../shared/common_widgets.dart';
 import 'cpu_profiler_controller.dart';
 
 class CpuProfilerDisabled extends StatelessWidget {
@@ -14,19 +15,22 @@
 
   @override
   Widget build(BuildContext context) {
-    return Center(
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: <Widget>[
-          const Text('CPU profiler is disabled.'),
-          Padding(
-            padding: const EdgeInsets.all(8.0),
-            child: ElevatedButton(
-              onPressed: controller.enableCpuProfiler,
-              child: const Text('Enable profiler'),
+    return DefaultTextStyle(
+      style: Theme.of(context).regularTextStyle,
+      child: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            const Text('CPU profiler is disabled.'),
+            Padding(
+              padding: const EdgeInsets.all(8.0),
+              child: ElevatedButton(
+                onPressed: controller.enableCpuProfiler,
+                child: const Text('Enable profiler'),
+              ),
             ),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }
@@ -37,21 +41,24 @@
 
   @override
   Widget build(BuildContext context) {
-    return const Center(
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          Text(
-            'There are no app start up samples available.',
-            textAlign: TextAlign.center,
-          ),
-          SizedBox(height: denseSpacing),
-          Text(
-            'To avoid this, try to open the DevTools CPU profiler '
-            'sooner after starting your app.',
-            textAlign: TextAlign.center,
-          ),
-        ],
+    return DefaultTextStyle(
+      style: Theme.of(context).regularTextStyle,
+      child: const Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Text(
+              'There are no app start up samples available.',
+              textAlign: TextAlign.center,
+            ),
+            SizedBox(height: denseSpacing),
+            Text(
+              'To avoid this, try to open the DevTools CPU profiler '
+              'sooner after starting your app.',
+              textAlign: TextAlign.center,
+            ),
+          ],
+        ),
       ),
     );
   }
@@ -62,9 +69,7 @@
 
   @override
   Widget build(BuildContext context) {
-    return const Center(
-      child: Text('No CPU samples recorded.'),
-    );
+    return const CenteredMessage('No CPU samples recorded.');
   }
 }
 
@@ -73,28 +78,30 @@
 
   @override
   Widget build(BuildContext context) {
-    const stopRow = Row(
-      mainAxisAlignment: MainAxisAlignment.center,
-      children: [
-        Text('Click the stop button '),
-        Icon(Icons.stop),
-        Text(' to end the recording.'),
-      ],
-    );
-    return const Center(
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          Row(
-            mainAxisAlignment: MainAxisAlignment.center,
-            children: [
-              Text('Click the record button '),
-              Icon(Icons.fiber_manual_record),
-              Text(' to start recording CPU samples.'),
-            ],
-          ),
-          stopRow,
-        ],
+    return DefaultTextStyle(
+      style: Theme.of(context).regularTextStyle,
+      child: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                const Text('Click the record button '),
+                Icon(Icons.fiber_manual_record, size: defaultIconSize),
+                const Text(' to start recording CPU samples.'),
+              ],
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                const Text('Click the stop button '),
+                Icon(Icons.stop, size: defaultIconSize),
+                const Text(' to end the recording.'),
+              ],
+            ),
+          ],
+        ),
       ),
     );
   }
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics/isolate_statistics_view_controller.dart b/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics/isolate_statistics_view_controller.dart
index 632f5eb..6dbc423 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics/isolate_statistics_view_controller.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/isolate_statistics/isolate_statistics_view_controller.dart
@@ -73,8 +73,8 @@
     // given tag's scope in the VM. These raw counts are reported here and
     // need to be processed.
     final tagCounters = isolate.tagCounters;
-    final names = tagCounters['names'];
-    final List<int> counters = tagCounters['counters'].cast<int>();
+    final names = tagCounters['names'] as List;
+    final counters = (tagCounters['counters'] as List).cast<int>();
     final percentages = <String, double>{};
     int totalTickCount = 0;
     for (int i = 0; i < counters.length; ++i) {
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_tree_columns.dart b/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_tree_columns.dart
new file mode 100644
index 0000000..7ecc4d7
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_tree_columns.dart
@@ -0,0 +1,31 @@
+// Copyright 2024 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 '../../../shared/charts/treemap.dart';
+import '../../../shared/table/table_data.dart';
+import 'process_memory_view_controller.dart';
+
+class CategoryColumn extends TreeColumnData<TreemapNode> {
+  CategoryColumn() : super('Category');
+
+  @override
+  String getValue(TreemapNode dataObject) => dataObject.name;
+}
+
+class DescriptionColumn extends ColumnData<TreemapNode> {
+  DescriptionColumn() : super.wide('Description');
+
+  @override
+  String getValue(TreemapNode dataObject) => dataObject.caption ?? '';
+}
+
+class MemoryColumn extends SizeAndPercentageColumn<TreemapNode> {
+  MemoryColumn({required VMProcessMemoryViewController controller})
+      : super(
+          title: 'Memory Usage',
+          sizeProvider: (node) => node.byteSize,
+          percentAsDoubleProvider: (node) =>
+              node.byteSize / controller.treeRoot.value!.byteSize,
+        );
+}
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view.dart
new file mode 100644
index 0000000..959740f
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view.dart
@@ -0,0 +1,254 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:flutter/material.dart';
+
+import '../../../shared/analytics/constants.dart' as gac;
+import '../../../shared/charts/treemap.dart';
+import '../../../shared/common_widgets.dart';
+import '../../../shared/primitives/utils.dart';
+import '../../../shared/table/table.dart';
+import '../../../shared/table/table_data.dart';
+import '../../../shared/ui/tab.dart';
+import '../vm_developer_tools_screen.dart';
+import 'process_memory_tree_columns.dart';
+import 'process_memory_view_controller.dart';
+
+/// Displays a breakdown of the target application's overall memory footprint.
+///
+/// This allows for developers to determine:
+///
+///  - The total resident set size (RSS)
+///  - How much memory can be attributed to each isolate group's heap,
+///    including system isolate groups like the vm-service and kernel-service
+///  - How much memory can be attributed to developer tooling functionality
+///    (e.g., CPU profiler buffer size, size of all recorded timeline events)
+///
+/// This view provides both a tree table and a tree map to explore the
+/// process's memory footprint.
+class VMProcessMemoryView extends VMDeveloperView {
+  const VMProcessMemoryView()
+      : super(
+          title: 'Process Memory',
+          icon: Icons.memory,
+        );
+
+  static const id = 'vm-process-memory';
+
+  @override
+  Widget build(BuildContext context) => VMProcessMemoryViewBody();
+}
+
+enum ProcessMemoryTab {
+  tree('Tree', _treeTab),
+  treeMap('Tree Map', _treeMapTab);
+
+  const ProcessMemoryTab(this.title, this.key);
+
+  final String title;
+  final Key key;
+
+  static const Key _treeTab = Key('process memory usage tree tab');
+  static const Key _treeMapTab = Key('process memory usage tree map tab');
+
+  static ProcessMemoryTab byKey(Key? k) {
+    return ProcessMemoryTab.values.firstWhere((tab) => tab.key == k);
+  }
+}
+
+class VMProcessMemoryViewBody extends StatefulWidget {
+  VMProcessMemoryViewBody({super.key})
+      : tabs = [
+          _buildTab(ProcessMemoryTab.tree),
+          _buildTab(ProcessMemoryTab.treeMap),
+        ];
+
+  static DevToolsTab _buildTab(ProcessMemoryTab processMemoryTab) {
+    return DevToolsTab.create(
+      key: processMemoryTab.key,
+      tabName: processMemoryTab.title,
+      gaPrefix: 'processMemoryTab',
+    );
+  }
+
+  final List<DevToolsTab> tabs;
+
+  @override
+  State<VMProcessMemoryViewBody> createState() =>
+      _VMProcessMemoryViewBodyState();
+}
+
+class _VMProcessMemoryViewBodyState extends State<VMProcessMemoryViewBody>
+    with TickerProviderStateMixin, AutoDisposeMixin {
+  static const _expandCollapseMinIncludeTextWidth = 610.0;
+
+  final controller = VMProcessMemoryViewController();
+
+  bool _tabControllerInitialized = false;
+  late TabController _tabController;
+
+  @override
+  void initState() {
+    super.initState();
+    _initTabController();
+  }
+
+  @override
+  void didUpdateWidget(VMProcessMemoryViewBody oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.tabs.length != oldWidget.tabs.length) {
+      _initTabController();
+    }
+  }
+
+  void _initTabController() {
+    if (_tabControllerInitialized) {
+      _tabController
+        ..removeListener(_onTabChanged)
+        ..dispose();
+    }
+
+    _tabController = TabController(
+      length: widget.tabs.length,
+      vsync: this,
+    )..addListener(_onTabChanged);
+    _tabControllerInitialized = true;
+  }
+
+  void _onTabChanged() {
+    setState(() {});
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final textTheme = theme.textTheme;
+    final colorScheme = theme.colorScheme;
+    final currentTab = widget.tabs[_tabController.index];
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        RefreshButton(
+          gaScreen: gac.vmTools,
+          gaSelection: gac.refreshProcessMemoryStatistics,
+          onPressed: controller.refresh,
+        ),
+        const SizedBox(height: denseRowSpacing),
+        AreaPaneHeader(
+          leftPadding: 0,
+          tall: true,
+          title: TabBar(
+            labelColor: textTheme.bodyLarge?.color ?? colorScheme.onSurface,
+            isScrollable: true,
+            controller: _tabController,
+            tabs: widget.tabs,
+          ),
+          actions: [
+            if (currentTab.key == ProcessMemoryTab.tree.key) ...[
+              ExpandAllButton(
+                gaScreen: gac.cpuProfiler,
+                gaSelection: gac.expandAll,
+                minScreenWidthForTextBeforeScaling:
+                    _expandCollapseMinIncludeTextWidth,
+                onPressed: () => setState(controller.expandTree),
+              ),
+              const SizedBox(width: denseSpacing),
+              CollapseAllButton(
+                gaScreen: gac.cpuProfiler,
+                gaSelection: gac.collapseAll,
+                minScreenWidthForTextBeforeScaling:
+                    _expandCollapseMinIncludeTextWidth,
+                onPressed: () => setState(controller.collapseTree),
+              ),
+            ],
+          ],
+        ),
+        Expanded(
+          child: OutlineDecoration(
+            showTop: false,
+            child: TabBarView(
+              physics: defaultTabBarViewPhysics,
+              controller: _tabController,
+              children: [
+                _ProcessMemoryTree(controller: controller),
+                _ProcessMemoryTreeMap(controller: controller),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class _ProcessMemoryTree extends StatelessWidget {
+  _ProcessMemoryTree({
+    required this.controller,
+  });
+
+  final VMProcessMemoryViewController controller;
+
+  static final categoryColumn = CategoryColumn();
+  static final descriptionColumn = DescriptionColumn();
+
+  late final memoryColumn = MemoryColumn(controller: controller);
+  late final columns = List<ColumnData<TreemapNode>>.unmodifiable([
+    memoryColumn,
+    categoryColumn,
+    descriptionColumn,
+  ]);
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueListenableBuilder<TreemapNode?>(
+      valueListenable: controller.treeRoot,
+      builder: (context, root, _) {
+        return TreeTable<TreemapNode>(
+          keyFactory: (e) =>
+              PageStorageKey<String>('${e.name}+${e.depth}+${e.byteSize}'),
+          displayTreeGuidelines: true,
+          dataRoots: [
+            if (root != null) root,
+          ],
+          dataKey: 'process-memory-tree',
+          columns: columns,
+          treeColumn: categoryColumn,
+          defaultSortColumn: memoryColumn,
+          defaultSortDirection: SortDirection.descending,
+        );
+      },
+    );
+  }
+}
+
+class _ProcessMemoryTreeMap extends StatelessWidget {
+  const _ProcessMemoryTreeMap({
+    required this.controller,
+  });
+
+  final VMProcessMemoryViewController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        return ValueListenableBuilder<TreemapNode?>(
+          valueListenable: controller.treeMapRoot,
+          builder: (context, root, __) {
+            return Treemap.fromRoot(
+              rootNode: root!,
+              levelsVisible: 2,
+              width: constraints.maxWidth,
+              height: constraints.maxHeight,
+              isOutermostLevel: true,
+              onRootChangedCallback: controller.setTreeMapRoot,
+            );
+          },
+        );
+      },
+    );
+  }
+}
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view_controller.dart b/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view_controller.dart
new file mode 100644
index 0000000..b1d5766
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/vm_developer/process_memory/process_memory_view_controller.dart
@@ -0,0 +1,86 @@
+// Copyright 2024 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:devtools_app_shared/utils.dart';
+import 'package:flutter/foundation.dart';
+import 'package:vm_service/vm_service.dart' hide VmService;
+
+import '../../../service/vm_service_wrapper.dart';
+import '../../../shared/charts/treemap.dart';
+import '../../../shared/globals.dart';
+
+/// Stores process memory usage state for the [VMProcessMemoryView].
+class VMProcessMemoryViewController extends DisposableController {
+  VMProcessMemoryViewController() {
+    unawaited(refresh());
+  }
+
+  /// Fetches the most up-to-date process memory information.
+  Future<void> refresh() async {
+    final processMemoryUsage = await _service.getProcessMemoryUsage();
+
+    TreemapNode processNode(ProcessMemoryItem memoryItem) {
+      final node = TreemapNode(
+        name: memoryItem.name!,
+        byteSize: memoryItem.size!,
+        caption: memoryItem.description,
+      );
+      for (final child in memoryItem.children ?? const <ProcessMemoryItem>[]) {
+        node.addChild(processNode(child));
+      }
+      return node;
+    }
+
+    final currentRoot = processNode(processMemoryUsage.root!);
+
+    // Insert a synthetic node for memory that isn't accounted for by the VM.
+    // This value includes memory allocated through malloc and other mechanisms
+    // that aren't explicitly tracked by the VM.
+    currentRoot.addChild(
+      TreemapNode(
+        name: 'Other',
+        byteSize: currentRoot.byteSize -
+            currentRoot.children.fold<int>(
+              0,
+              (sum, e) => sum + e.byteSize,
+            ),
+      ),
+    );
+
+    // Expand the tree by default since the tree should be relatively small.
+    currentRoot.expandCascading();
+
+    _treeRoot.value = currentRoot;
+    _treeMapRoot.value = currentRoot;
+  }
+
+  VmServiceWrapper get _service => serviceConnection.serviceManager.service!;
+
+  /// The root of the process memory tree, used by the tree view.
+  ValueListenable<TreemapNode?> get treeRoot => _treeRoot;
+  final _treeRoot = ValueNotifier<TreemapNode?>(null);
+
+  /// The current root of the process memory tree being used by the tree map
+  /// viewer.
+  ValueListenable<TreemapNode?> get treeMapRoot => _treeMapRoot;
+  final _treeMapRoot = ValueNotifier<TreemapNode?>(null);
+
+  /// Called when a user interacts with the tree map viewer that results in the
+  /// displayed root of the tree map being updated.
+  void setTreeMapRoot(TreemapNode? newRoot) {
+    _treeMapRoot.value = newRoot;
+  }
+
+  /// Expands all the entries in the tree viewer.
+  void expandTree() {
+    _treeRoot.value?.expandCascading();
+  }
+
+  /// Collapses all the entries in the tree viewer.
+  void collapseTree() {
+    _treeRoot.value?.collapseCascading();
+  }
+}
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_common_widgets.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_common_widgets.dart
index a0d0b20..5a8e402 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_common_widgets.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_common_widgets.dart
@@ -16,7 +16,6 @@
 import '../../shared/common_widgets.dart';
 import '../../shared/globals.dart';
 import '../../shared/primitives/utils.dart';
-import '../../shared/table/table.dart';
 import '../../shared/tree.dart';
 import '../debugger/codeview.dart';
 import '../debugger/codeview_controller.dart';
@@ -68,7 +67,7 @@
       return Size.infinite;
     }
     return Size.fromHeight(
-      areaPaneHeaderHeight +
+      defaultHeaderHeight +
           (rowKeyValues?.length ?? 0) * defaultRowHeight +
           defaultSpacing,
     );
@@ -478,7 +477,7 @@
   });
 
   final RetainingObject object;
-  final Function(ObjRef? obj) onTap;
+  final void Function(ObjRef? obj) onTap;
 
   @override
   Widget build(BuildContext context) {
@@ -650,7 +649,7 @@
       ),
     );
     return DefaultTextStyle(
-      style: theme.fixedFontStyle,
+      style: theme.regularTextStyle,
       child: Row(
         children: rowContent,
       ),
@@ -738,9 +737,9 @@
 
     final TextStyle style;
     if (isServiceObject) {
-      style = theme.fixedFontLinkStyle;
+      style = theme.linkTextStyle;
     } else {
-      style = theme.fixedFontStyle;
+      style = theme.regularTextStyle;
     }
     return TextSpan(
       text: text,
@@ -759,14 +758,9 @@
 
   @override
   Widget build(BuildContext context) {
-    final theme = Theme.of(context);
     return RichText(
       maxLines: 1,
       text: TextSpan(
-        style: theme.linkTextStyle.apply(
-          fontFamily: theme.fixedFontStyle.fontFamily,
-          overflow: TextOverflow.ellipsis,
-        ),
         children: [buildTextSpan(context)],
       ),
     );
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart
index c080fb7..a9d7bf2 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart
@@ -11,6 +11,7 @@
 import '../../shared/utils.dart';
 import 'isolate_statistics/isolate_statistics_view.dart';
 import 'object_inspector/object_inspector_view.dart';
+import 'process_memory/process_memory_view.dart';
 import 'vm_developer_tools_controller.dart';
 import 'vm_statistics/vm_statistics_view.dart';
 
@@ -51,10 +52,11 @@
 class VMDeveloperToolsScreenBody extends StatefulWidget {
   const VMDeveloperToolsScreenBody({super.key});
 
-  static List<VMDeveloperView> views = [
+  static final views = <VMDeveloperView>[
     const VMStatisticsView(),
     const IsolateStatisticsView(),
     ObjectInspectorView(),
+    const VMProcessMemoryView(),
   ];
 
   @override
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
index d7c835d..0e3f082 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart
@@ -36,11 +36,19 @@
   int get dartHeapSize => newSpaceUsage + oldSpaceUsage;
   int get dartHeapCapacity => newSpaceCapacity + oldSpaceCapacity;
 
-  int get newSpaceUsage => json!['_heaps']['new']['used'];
-  int get oldSpaceUsage => json!['_heaps']['old']['used'];
+  int get newSpaceUsage => _newHeap['used'] as int;
+  int get oldSpaceUsage => _oldHeap['used'] as int;
 
-  int get newSpaceCapacity => json!['_heaps']['new']['capacity'];
-  int get oldSpaceCapacity => json!['_heaps']['old']['capacity'];
+  int get newSpaceCapacity => _newHeap['capacity'] as int;
+  int get oldSpaceCapacity => _oldHeap['capacity'] as int;
+
+  Map<String, Object?> get _newHeap =>
+      (_heaps['new'] as Map).cast<String, Object?>();
+  Map<String, Object?> get _oldHeap =>
+      (_heaps['old'] as Map).cast<String, Object?>();
+
+  Map<String, Object?> get _heaps =>
+      (json!['_heaps'] as Map).cast<String, Object?>();
 }
 
 /// An extension on [Class] which allows for access to VM internal fields.
@@ -56,7 +64,10 @@
   static const _parentWordOffsetKey = '_parentWordOffset';
 
   int? parentWordOffset(int inboundReferenceIndex) {
-    return json![_referencesKey]?[inboundReferenceIndex]?[_parentWordOffsetKey];
+    final references = (json![_referencesKey] as List?)?.cast<Object?>();
+    final inboundReference =
+        (references?[inboundReferenceIndex] as Map?)?.cast<String, Object?>();
+    return inboundReference?[_parentWordOffsetKey] as int?;
   }
 }
 
@@ -89,10 +100,10 @@
   static const _oldSpaceKey = '_old';
 
   HeapStats get newSpace => json!.containsKey(_newSpaceKey)
-      ? HeapStats.parse(json![_newSpaceKey].cast<int>())
+      ? HeapStats.parse((json![_newSpaceKey] as List).cast<int>())
       : const HeapStats.empty();
   HeapStats get oldSpace => json!.containsKey(_oldSpaceKey)
-      ? HeapStats.parse(json![_oldSpaceKey].cast<int>())
+      ? HeapStats.parse((json![_oldSpaceKey] as List).cast<int>())
       : const HeapStats.empty();
 }
 
@@ -105,11 +116,18 @@
     required this.averageCollectionTime,
   });
 
-  GCStats.parse({required this.heap, required Map<String, dynamic> json})
-      : usage = json[usedKey],
-        capacity = json[capacityKey],
-        collections = json[collectionsKey] {
-    averageCollectionTime = json[timeKey] * 1000 / collections;
+  factory GCStats.parse({
+    required String heap,
+    required Map<String, dynamic> json,
+  }) {
+    final collections = json[collectionsKey] as int;
+    return GCStats(
+      heap: heap,
+      usage: json[usedKey],
+      capacity: json[capacityKey],
+      collections: collections,
+      averageCollectionTime: (json[timeKey] as num) * 1000 / collections,
+    );
   }
 
   static const usedKey = 'used';
@@ -121,7 +139,7 @@
   final int usage;
   final int capacity;
   final int collections;
-  late final double averageCollectionTime;
+  final double averageCollectionTime;
 }
 
 extension AllocationProfilePrivateViewExtension on AllocationProfile {
@@ -131,14 +149,17 @@
 
   GCStats get newSpaceGCStats => GCStats.parse(
         heap: HeapGeneration.newSpace.toString(),
-        json: json![heapsKey][newSpaceKey],
+        json: (_heaps[newSpaceKey] as Map).cast<String, Object?>(),
       );
 
   GCStats get oldSpaceGCStats => GCStats.parse(
         heap: HeapGeneration.oldSpace.toString(),
-        json: json![heapsKey][oldSpaceKey],
+        json: (_heaps[oldSpaceKey] as Map).cast<String, Object?>(),
       );
 
+  Map<String, Object?> get _heaps =>
+      (json![heapsKey] as Map).cast<String, Object?>();
+
   GCStats get totalGCStats {
     final newSpace = newSpaceGCStats;
     final oldSpace = oldSpaceGCStats;
@@ -436,7 +457,7 @@
       return;
     }
     final rawObject = data[3] as Map<String, dynamic>;
-    object = rawObject['type'].contains('Instance')
+    object = (rawObject['type'] as List).contains('Instance')
         ? InstanceRef.parse(rawObject)
         : createServiceObject(data[3], const <String>[]) as Response?;
   }
@@ -509,7 +530,9 @@
   String? get vmName => json!['_vmName'];
 
   Future<Instance?> get icDataArray async {
-    final String? icDataArrayId = json![_icDataArrayKey]?['id'];
+    final icDataArray =
+        (json![_icDataArrayKey] as Map?)?.cast<String, Object?>();
+    final icDataArrayId = icDataArray?['id'] as String?;
     if (icDataArrayId != null) {
       final service = serviceConnection.serviceManager.service!;
       final isolate =
@@ -616,7 +639,7 @@
 
   factory InliningData.parse(Map<String, dynamic> json) {
     final startAddress = int.parse(json[kStartAddressKey], radix: 16);
-    final intervals = json[kInlinedIntervals] as List;
+    final intervals = (json[kInlinedIntervals] as List).cast<List>();
     final functions = (json[kInlinedFunctions] as List)
         .cast<Map<String, dynamic>>()
         .map<FuncRef>((e) => FuncRef.parse(e)!)
@@ -793,8 +816,10 @@
       final isolate =
           serviceConnection.serviceManager.isolateManager.selectedIsolate.value;
 
-      return await service.getObject(isolate!.id!, json![_guardClassKey]['id'])
-          as Class;
+      return await service.getObject(
+        isolate!.id!,
+        guardClassData['id'] as String,
+      ) as Class;
     }
 
     return null;
@@ -816,11 +841,14 @@
     String? guardClassType;
 
     if (json![_guardClassKey] is Map) {
-      guardClassType = json![_guardClassKey]['type'];
+      guardClassType = guardClassData['type'] as String?;
     }
 
     return guardClassType == '@Class' || guardClassType == 'Class';
   }
+
+  Map<String, Object?> get guardClassData =>
+      (json![_guardClassKey] as Map).cast<String, Object?>();
 }
 
 /// The kinds of Guard Class that determine whether a Field object has
@@ -968,15 +996,10 @@
   }
 
   List<ProfileCode> get codes {
-    var codesFromExpando = _expando[this];
-    if (codesFromExpando == null) {
-      codesFromExpando = json![_kCodesKey]
-          .cast<Map<String, dynamic>>()
-          .map<ProfileCode>((e) => ProfileCode.parse(e)!)
-          .toList();
-      _expando[this] = codesFromExpando;
-    }
-    return codesFromExpando!;
+    return _expando[this] ??= (json![_kCodesKey] as List)
+        .cast<Map<String, dynamic>>()
+        .map<ProfileCode>((e) => ProfileCode.parse(e)!)
+        .toList();
   }
 }
 
diff --git a/packages/devtools_app/lib/src/service/service_extension_widgets.dart b/packages/devtools_app/lib/src/service/service_extension_widgets.dart
index d1f7de9..9fabc6f 100644
--- a/packages/devtools_app/lib/src/service/service_extension_widgets.dart
+++ b/packages/devtools_app/lib/src/service/service_extension_widgets.dart
@@ -600,11 +600,15 @@
               ),
             ),
             if (docsUrl != null)
-              MoreInfoLink(
-                url: docsUrl,
-                gaScreenName: widget.serviceExtension.gaScreenName!,
-                gaSelectedItemDescription: widget.serviceExtension.gaDocsItem!,
-                padding: const EdgeInsets.symmetric(vertical: denseSpacing),
+              Padding(
+                padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
+                child: MoreInfoLink(
+                  url: docsUrl,
+                  gaScreenName: widget.serviceExtension.gaScreenName!,
+                  gaSelectedItemDescription:
+                      widget.serviceExtension.gaDocsItem!,
+                  padding: const EdgeInsets.symmetric(vertical: denseSpacing),
+                ),
               ),
           ],
         );
@@ -679,7 +683,7 @@
 
   final double overlayWidth;
 
-  static const _defaultWidth = 700.0;
+  static const _defaultWidth = 600.0;
 
   @override
   State<ServiceExtensionCheckboxGroupButton> createState() =>
diff --git a/packages/devtools_app/lib/src/service/service_manager.dart b/packages/devtools_app/lib/src/service/service_manager.dart
index 4f01aab..73af59f 100644
--- a/packages/devtools_app/lib/src/service/service_manager.dart
+++ b/packages/devtools_app/lib/src/service/service_manager.dart
@@ -202,8 +202,8 @@
         await serviceManager.callServiceExtensionOnMainIsolate(
       registrations.flutterListViews,
     );
-    final List<Map<String, Object?>> views =
-        flutterViewListResponse.json!['views'].cast<Map<String, Object?>>();
+    final views = (flutterViewListResponse.json!['views'] as List)
+        .cast<Map<String, Object?>>();
 
     // Each isolate should only have one FlutterView.
     final flutterView = views.firstWhereOrNull(
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 06bae06..2a0960a 100644
--- a/packages/devtools_app/lib/src/service/vm_service_wrapper.dart
+++ b/packages/devtools_app/lib/src/service/vm_service_wrapper.dart
@@ -264,7 +264,7 @@
 
   Future<HttpProfile> getHttpProfileWrapper(
     String isolateId, {
-    int? updatedSince,
+    DateTime? updatedSince,
   }) {
     return getHttpProfile(isolateId, updatedSince: updatedSince);
   }
diff --git a/packages/devtools_app/lib/src/shared/analytics/_analytics_web.dart b/packages/devtools_app/lib/src/shared/analytics/_analytics_web.dart
index 992e35d..cc05e4c 100644
--- a/packages/devtools_app/lib/src/shared/analytics/_analytics_web.dart
+++ b/packages/devtools_app/lib/src/shared/analytics/_analytics_web.dart
@@ -12,7 +12,7 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:js/js.dart';
 import 'package:logging/logging.dart';
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import '../../../devtools.dart' as devtools show version;
 import '../globals.dart';
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart
index 641b686..5726b4f 100644
--- a/packages/devtools_app/lib/src/shared/analytics/constants.dart
+++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart
@@ -102,6 +102,7 @@
 // VM Tools UX Actions:
 const refreshIsolateStatistics = 'refreshIsolateStatistics';
 const refreshVmStatistics = 'refreshVmStatistics';
+const refreshProcessMemoryStatistics = 'refreshProcessMemoryStatistics';
 const requestSize = 'requestSize';
 
 // Settings actions:
diff --git a/packages/devtools_app/lib/src/shared/banner_messages.dart b/packages/devtools_app/lib/src/shared/banner_messages.dart
index 94984fb..4ece05e 100644
--- a/packages/devtools_app/lib/src/shared/banner_messages.dart
+++ b/packages/devtools_app/lib/src/shared/banner_messages.dart
@@ -170,14 +170,18 @@
 
   @override
   Widget build(BuildContext context) {
-    final colorScheme = Theme.of(context).colorScheme;
+    final theme = Theme.of(context);
+    final colorScheme = theme.colorScheme;
     return Card(
       color: messageType == BannerMessageType.error
           ? colorScheme.errorContainer
           : colorScheme.warningContainer,
       margin: const EdgeInsets.only(bottom: intermediateSpacing),
       child: Padding(
-        padding: const EdgeInsets.all(defaultSpacing),
+        padding: const EdgeInsets.symmetric(
+          vertical: densePadding,
+          horizontal: denseSpacing,
+        ),
         child: Column(
           mainAxisSize: MainAxisSize.min,
           crossAxisAlignment: CrossAxisAlignment.start,
@@ -191,6 +195,7 @@
                     messageType == BannerMessageType.error
                         ? Icons.error_outline
                         : Icons.warning_amber_outlined,
+                    size: actionsIconSize,
                     color: messageType == BannerMessageType.error
                         ? colorScheme.onErrorContainer
                         : colorScheme.onWarningContainer,
@@ -199,7 +204,7 @@
                 Expanded(
                   child: RichText(
                     text: TextSpan(
-                      style: TextStyle(
+                      style: theme.regularTextStyle.copyWith(
                         color: messageType == BannerMessageType.error
                             ? colorScheme.onErrorContainer
                             : colorScheme.onWarningContainer,
@@ -212,6 +217,7 @@
                 IconButton(
                   icon: Icon(
                     Icons.close,
+                    size: actionsIconSize,
                     color: messageType == BannerMessageType.error
                         ? colorScheme.onErrorContainer
                         : colorScheme.onWarningContainer,
diff --git a/packages/devtools_app/lib/src/shared/charts/chart.dart b/packages/devtools_app/lib/src/shared/charts/chart.dart
index 9a1fe38..27674fb 100644
--- a/packages/devtools_app/lib/src/shared/charts/chart.dart
+++ b/packages/devtools_app/lib/src/shared/charts/chart.dart
@@ -566,8 +566,8 @@
     int timestamp,
     double xTickCoord,
     Paint axis, {
-    shortTick = true,
-    displayTime = false,
+    bool shortTick = true,
+    bool displayTime = false,
   }) {
     if (displayTime) {
       // Draw vertical tick (short or long).
diff --git a/packages/devtools_app/lib/src/shared/charts/chart_controller.dart b/packages/devtools_app/lib/src/shared/charts/chart_controller.dart
index af763a0..5b40906 100644
--- a/packages/devtools_app/lib/src/shared/charts/chart_controller.dart
+++ b/packages/devtools_app/lib/src/shared/charts/chart_controller.dart
@@ -322,7 +322,7 @@
     buildLabelTimestamps(refresh: true);
   }
 
-  void buildLabelTimestamps({refresh = false}) {
+  void buildLabelTimestamps({bool refresh = false}) {
     if (isLabelsShared || timestamps.isEmpty) return;
 
     if (refresh) {
diff --git a/packages/devtools_app/lib/src/shared/charts/chart_trace.dart b/packages/devtools_app/lib/src/shared/charts/chart_trace.dart
index deece08..c9dda69 100644
--- a/packages/devtools_app/lib/src/shared/charts/chart_trace.dart
+++ b/packages/devtools_app/lib/src/shared/charts/chart_trace.dart
@@ -292,60 +292,57 @@
 double log10(num x) => log(x) / ln10;
 
 class AxisScale {
-  AxisScale(this.minPoint, this.maxPoint, this.maxTicks) {
-    _calculate();
+  factory AxisScale(double minPoint, double maxPoint, double maxTicks) {
+    final range = _niceNum(maxPoint, false);
+    final double tickSpacing;
+    final double exponent;
+    final double fraction;
+    if (range != 0) {
+      tickSpacing = range / (maxTicks - 1);
+      (:exponent, :fraction) = _exponentFraction(range);
+    } else {
+      tickSpacing = 1;
+      exponent = 0;
+      fraction = 0;
+    }
+    return AxisScale._(
+      minPoint: minPoint,
+      maxPoint: maxPoint,
+      maxTicks: maxTicks,
+      tickSpacing: tickSpacing,
+      labelUnitExponent: exponent,
+      labelTicks: fraction,
+    );
   }
 
+  AxisScale._({
+    required this.minPoint,
+    required this.maxPoint,
+    required this.maxTicks,
+    required this.tickSpacing,
+    required this.labelUnitExponent,
+    required this.labelTicks,
+  });
+
   final double minPoint, maxPoint;
 
   final double maxTicks;
 
-  double? get tickSpacing => _tickSpacing;
+  final double tickSpacing;
 
-  double? _tickSpacing;
-
-  double? get computedMin => _computedMin;
-  double? get computedMax => _computedMax;
-
-  double? _computedMin, _computedMax;
-
-  late double _range;
-
-  /// Unit for the label (exponent) e.g., 6 = 10^6
-  late double labelUnitExponent;
+  /// Unit for the label (exponent) e.g., 6 = 10^6.
+  final double labelUnitExponent;
 
   /// Number of labels.
-  late double labelTicks;
+  final double labelTicks;
 
-  void _calculate() {
-    _range = _niceNum(maxPoint, false);
-    if (_range != 0) {
-      _tickSpacing = _range / (maxTicks - 1);
-      _computedMin = _calculateMin();
-      _computedMax = _calculateMax();
-      final exponentFraction = _exponentFraction(_range);
-      labelUnitExponent = exponentFraction['exponent'];
-      labelTicks = exponentFraction['fraction'].roundToDouble();
-    } else {
-      _tickSpacing = 1;
-      _computedMin = 0;
-      _computedMax = 0;
-      labelUnitExponent = 0;
-      labelTicks = 0;
-    }
-  }
-
-  double _calculateMin() => (minPoint / _tickSpacing!).floor() * _tickSpacing!;
-
-  double _calculateMax() => (maxPoint / _tickSpacing!).ceil() * _tickSpacing!;
-
-  Map _exponentFraction(double range) {
-    if (range == 0) return {};
+  static ({double exponent, double fraction}) _exponentFraction(double range) {
+    assert(range != 0);
 
     final exponent = log10(range).floor().toDouble();
     final fraction = range / pow(10, exponent);
 
-    return {'exponent': exponent, 'fraction': fraction};
+    return (exponent: exponent, fraction: fraction.roundToDouble());
   }
 
   /// Produce a whole number for the Y-axis scale and its unit using
@@ -360,37 +357,28 @@
   ///    10K, 20K, 30K, 40K, 50K
   /// This allows new data points >30K to be plotted w/o having to
   /// re-layout new Y-axis scale.
-  double _niceNum(double range, bool round) {
+  static double _niceNum(double range, bool round) {
     if (range == 0) return 0;
 
-    double exponent; // exponent of range
-    double fraction; // fractional part of range
-    late double niceFraction; // nice, rounded fraction
-
-    exponent = log10(range).floor().toDouble();
-    fraction = range / pow(10, exponent);
-
-    if (round) {
-      niceFraction = fraction.roundToDouble();
-    } else {
-      if (fraction <= 1) {
-        niceFraction = 1;
-      } else if (fraction <= 2) {
-        niceFraction = 2;
-      } else if (fraction <= 3) {
-        niceFraction = 3;
-      } else if (fraction <= 5) {
-        niceFraction = 5;
-      } else if (fraction <= 7) {
-        niceFraction = 7;
-      } else if (fraction <= 10) {
-        niceFraction = 10;
-      }
-    }
+    // Exponent of range.
+    final exponent = log10(range).floor().toDouble();
+    // Fractional part of range.
+    final fraction = range / pow(10, exponent);
+    // Nice, rounded fraction.
+    final niceFraction = round
+        ? fraction.roundToDouble()
+        : switch (fraction) {
+            <= 1 => 1.0,
+            <= 2 => 2.0,
+            <= 3 => 3.0,
+            <= 5 => 5.0,
+            <= 7 => 7.0,
+            _ => 10.0,
+          };
 
     return niceFraction * pow(10, exponent);
   }
 
   double tickFromValue(double value) =>
-      (value / tickSpacing!).truncateToDouble();
+      (value / tickSpacing).truncateToDouble();
 }
diff --git a/packages/devtools_app/lib/src/shared/charts/flame_chart.dart b/packages/devtools_app/lib/src/shared/charts/flame_chart.dart
index 0f871aa..fec18d6 100644
--- a/packages/devtools_app/lib/src/shared/charts/flame_chart.dart
+++ b/packages/devtools_app/lib/src/shared/charts/flame_chart.dart
@@ -873,7 +873,7 @@
         searchMatch: node.data.isSearchMatch,
         activeSearchMatch: node.data.isActiveSearchMatch,
         zoom: FlameChartUtils.zoomForNode(node, zoom),
-        colorScheme: Theme.of(context).colorScheme,
+        theme: Theme.of(context),
       ),
     );
   }
@@ -1067,7 +1067,7 @@
     required bool searchMatch,
     required bool activeSearchMatch,
     required double zoom,
-    required ColorScheme colorScheme,
+    required ThemeData theme,
   }) {
     // This math.max call prevents using a rect with negative width for
     // small events that have padding.
@@ -1094,19 +1094,19 @@
         selected: selected,
         searchMatch: searchMatch,
         activeSearchMatch: activeSearchMatch,
-        colorScheme: colorScheme,
+        colorScheme: theme.colorScheme,
       ),
       child: zoomedWidth >= _minWidthForText
           ? Text(
               text,
               textAlign: TextAlign.left,
               overflow: TextOverflow.ellipsis,
-              style: TextStyle(
-                color: _textColor(
+              style: theme.regularTextStyleWithColor(
+                _textColor(
                   selected: selected,
                   searchMatch: searchMatch,
                   activeSearchMatch: activeSearchMatch,
-                  colorScheme: colorScheme,
+                  colorScheme: theme.colorScheme,
                 ),
               ),
             )
@@ -1478,7 +1478,7 @@
 
   /// A fixed width for the first column in the help dialog to ensure that the
   /// subsections are aligned.
-  double get firstColumnWidth => scaleByFontFactor(190.0);
+  double get firstColumnWidth => scaleByFontFactor(120.0);
 
   @override
   Widget build(BuildContext context) {
diff --git a/packages/devtools_app/lib/src/shared/charts/treemap.dart b/packages/devtools_app/lib/src/shared/charts/treemap.dart
index 1fb9031..1c66c6a 100644
--- a/packages/devtools_app/lib/src/shared/charts/treemap.dart
+++ b/packages/devtools_app/lib/src/shared/charts/treemap.dart
@@ -161,7 +161,9 @@
     final isHorizontalRectangle = width > height;
 
     final totalByteSize = computeByteSizeForNodes(nodes: children);
-    if (children.isEmpty) {
+    // If there's no children or the children have a size of zero, there's
+    // nothing to display.
+    if (children.isEmpty || totalByteSize == 0) {
       return [];
     }
 
@@ -367,7 +369,7 @@
       return buildSubTreemaps();
     } else {
       // If constructed with Treemap.fromRoot
-      return buildTreemap(context);
+      return buildTreemap();
     }
   }
 
@@ -375,6 +377,7 @@
     assert(widget.nodes != null && widget.nodes!.isNotEmpty);
     return LayoutBuilder(
       builder: (context, constraints) {
+        final defaultTextStyle = Theme.of(context).regularTextStyle;
         // TODO(peterdjlee): Investigate why exception is thrown without this check
         //                   and if there are any other cases.
         if (constraints.maxHeight == 0 || constraints.maxWidth == 0) {
@@ -391,12 +394,14 @@
           // If this is the second to the last level, paint all cells in the last level
           // instead of creating widgets to improve performance.
 
-          final tooltipMessage = hoveredNode?.displayText();
           return DevToolsTooltip(
             // A key is required to force a rebuild of the tooltips for each cell.
             // Use tooltipMessage as the key to prevent rebuilds within a cell.
-            key: Key(tooltipMessage?.toPlainText() ?? ''),
-            richMessage: tooltipMessage ?? const TextSpan(text: ''),
+            key: Key(hoveredNode?.displayText() ?? ''),
+            richMessage: hoveredNode?.displayTextSpan(
+                  defaultTextStyle: defaultTextStyle,
+                ) ??
+                const TextSpan(text: ''),
             waitDuration: tooltipWaitLong,
             child: MouseRegion(
               onHover: (event) => _onHover(event, positionedChildren),
@@ -406,7 +411,10 @@
                   widget.onRootChangedCallback(hoveredNode);
                 },
                 child: CustomPaint(
-                  painter: MultiCellPainter(nodes: positionedChildren),
+                  painter: MultiCellPainter(
+                    nodes: positionedChildren,
+                    defaultTextStyle: defaultTextStyle,
+                  ),
                   size: Size(constraints.maxWidth, constraints.maxHeight),
                 ),
               ),
@@ -431,24 +439,28 @@
   /// |                          |
   /// ----------------------------
   /// ```
-  Widget buildTreemap(BuildContext context) {
+  Widget buildTreemap() {
     assert(widget.rootNode != null);
     if (widget.rootNode!.children.isNotEmpty) {
       return Padding(
         padding: const EdgeInsets.all(1.0),
-        child: buildTreemapFromNodes(context),
+        child: buildTreemapFromNodes(),
       );
     } else {
       // If given a root node but its children are empty, draw itself.
-      return buildTreemapFromRoot(context);
+      return buildTreemapFromRoot();
     }
   }
 
-  Widget buildTreemapFromNodes(BuildContext context) {
+  Widget buildTreemapFromNodes() {
     return Column(
       children: [
         if (widget.height > Treemap.minHeightToDisplayTitleText)
-          buildTitleText(context),
+          _TitleBar(
+            rootNode: widget.rootNode!,
+            isOutermostLevel: widget.isOutermostLevel,
+            onRootChangedCallback: widget.onRootChangedCallback,
+          ),
         Expanded(
           child: Treemap.fromNodes(
             nodes: widget.rootNode!.children,
@@ -462,111 +474,33 @@
     );
   }
 
-  Column buildTreemapFromRoot(BuildContext context) {
+  Column buildTreemapFromRoot() {
+    final rootNode = widget.rootNode!;
+    final child = _TreeMapCell(
+      treeMapHeight: widget.height,
+      node: rootNode,
+    );
     return Column(
       children: [
-        if (widget.isOutermostLevel) buildTitleText(context),
+        if (widget.isOutermostLevel)
+          _TitleBar(
+            rootNode: widget.rootNode!,
+            isOutermostLevel: widget.isOutermostLevel,
+            onRootChangedCallback: widget.onRootChangedCallback,
+          ),
         Expanded(
           child: widget.isOutermostLevel
-              ? buildCell()
-              : buildSelectable(child: buildCell()),
+              ? child
+              : _SelectableTreemapNode(
+                  node: rootNode,
+                  onRootChangedCallback: widget.onRootChangedCallback,
+                  child: child,
+                ),
         ),
       ],
     );
   }
 
-  Container buildCell() {
-    return Container(
-      decoration: BoxDecoration(
-        color: widget.rootNode!.displayColor,
-        border: Border.all(color: Colors.black87),
-      ),
-      child: Center(
-        child: widget.height > Treemap.minHeightToDisplayCellText
-            ? buildNameAndSizeText(
-                textColor:
-                    widget.rootNode!.showDiff ? Colors.white : Colors.black,
-                oneLine: false,
-              )
-            : const SizedBox(),
-      ),
-    );
-  }
-
-  Widget buildTitleText(BuildContext context) {
-    if (widget.isOutermostLevel) {
-      return buildBreadcrumbNavigator();
-    } else {
-      return buildSelectable(
-        child: Container(
-          height: Treemap.treeMapHeaderHeight,
-          width: double.infinity,
-          decoration: BoxDecoration(
-            border: Border.all(color: Colors.black87),
-          ),
-          child: buildNameAndSizeText(
-            textColor:
-                (Theme.of(context).textTheme.bodyMedium ?? const TextStyle())
-                    .color,
-            oneLine: true,
-          ),
-        ),
-      );
-    }
-  }
-
-  RichText buildNameAndSizeText({
-    required bool oneLine,
-    required Color? textColor,
-  }) {
-    return RichText(
-      text: widget.rootNode!.displayText(oneLine: oneLine, color: textColor),
-      selectionColor: textColor,
-      textAlign: TextAlign.center,
-      overflow: TextOverflow.ellipsis,
-    );
-  }
-
-  Widget buildBreadcrumbNavigator() {
-    final pathFromRoot = widget.rootNode!.pathFromRoot();
-    return Padding(
-      padding: const EdgeInsets.only(bottom: denseRowSpacing),
-      child: BreadcrumbNavigator.builder(
-        itemCount: pathFromRoot.length,
-        builder: (context, index) {
-          final node = pathFromRoot[index];
-          return Breadcrumb(
-            text: index < pathFromRoot.length - 1
-                ? node.name
-                : node.displayText().toPlainText(),
-            isRoot: index == 0,
-            onPressed: () => widget.onRootChangedCallback(node),
-          );
-        },
-      ),
-    );
-  }
-
-  /// Builds a selectable container with [child] as its child.
-  ///
-  /// Selecting this widget will trigger a re-root of the tree
-  /// to the associated [TreemapNode].
-  ///
-  /// The default value for newRoot is [widget.rootNode].
-  Widget buildSelectable({required Widget child, TreemapNode? newRoot}) {
-    newRoot ??= widget.rootNode;
-    return DevToolsTooltip(
-      message: widget.rootNode!.displayText().toPlainText(),
-      waitDuration: tooltipWaitLong,
-      child: InkWell(
-        onTap: () {
-          widget.onRootChangedCallback(newRoot);
-        },
-        child: child,
-      ),
-    );
-  }
-
   /// Checks if the touch point of the given [details] is overlapping with
   /// a cell in [positionedCells].
   ///
@@ -591,6 +525,160 @@
   }
 }
 
+class _TreeMapCell extends StatelessWidget {
+  const _TreeMapCell({
+    required this.treeMapHeight,
+    required this.node,
+  });
+
+  final double treeMapHeight;
+  final TreemapNode node;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        color: node.displayColor,
+        border: Border.all(color: Colors.black87),
+      ),
+      child: Center(
+        child: treeMapHeight > Treemap.minHeightToDisplayCellText
+            ? _NameAndSizeText(
+                node: node,
+                color: node.showDiff ? Colors.white : Colors.black,
+                singleLine: false,
+              )
+            : const SizedBox(),
+      ),
+    );
+  }
+}
+
+class _TitleBar extends StatelessWidget {
+  const _TitleBar({
+    required this.rootNode,
+    required this.onRootChangedCallback,
+    required this.isOutermostLevel,
+  });
+
+  final TreemapNode rootNode;
+  final void Function(TreemapNode? node) onRootChangedCallback;
+  final bool isOutermostLevel;
+
+  @override
+  Widget build(BuildContext context) {
+    if (isOutermostLevel) {
+      return _BreadcrumbNavigator(
+        pathFromRoot: rootNode.pathFromRoot(),
+        onRootChangedCallback: onRootChangedCallback,
+      );
+    } else {
+      return _SelectableTreemapNode(
+        node: rootNode,
+        onRootChangedCallback: onRootChangedCallback,
+        child: Container(
+          height: Treemap.treeMapHeaderHeight,
+          width: double.infinity,
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.black87),
+          ),
+          child: _NameAndSizeText(
+            node: rootNode,
+            color: Theme.of(context).colorScheme.onSurface,
+            singleLine: true,
+          ),
+        ),
+      );
+    }
+  }
+}
+
+class _NameAndSizeText extends StatelessWidget {
+  const _NameAndSizeText({
+    required this.node,
+    required this.color,
+    required this.singleLine,
+  });
+
+  final TreemapNode node;
+  final Color color;
+  final bool singleLine;
+
+  @override
+  Widget build(BuildContext context) {
+    return RichText(
+      text: node.displayTextSpan(
+        singleLine: singleLine,
+        color: color,
+        defaultTextStyle: Theme.of(context).regularTextStyle,
+      ),
+      selectionColor: color,
+      textAlign: TextAlign.center,
+      overflow: TextOverflow.ellipsis,
+    );
+  }
+}
+
+class _BreadcrumbNavigator extends StatelessWidget {
+  const _BreadcrumbNavigator({
+    required this.pathFromRoot,
+    required this.onRootChangedCallback,
+  });
+
+  final List<TreemapNode> pathFromRoot;
+  final void Function(TreemapNode? node) onRootChangedCallback;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.only(bottom: denseRowSpacing),
+      child: BreadcrumbNavigator.builder(
+        itemCount: pathFromRoot.length,
+        builder: (context, index) {
+          final node = pathFromRoot[index];
+          return Breadcrumb(
+            text: index < pathFromRoot.length - 1
+                ? node.name
+                : node.displayText(),
+            isRoot: index == 0,
+            onPressed: () => onRootChangedCallback(node),
+          );
+        },
+      ),
+    );
+  }
+}
+
+/// A selectable container with [child] as its child.
+///
+/// Selecting this widget will trigger a re-root of the tree
+/// to [node].
+class _SelectableTreemapNode extends StatelessWidget {
+  const _SelectableTreemapNode({
+    required this.node,
+    required this.onRootChangedCallback,
+    required this.child,
+  });
+
+  final TreemapNode node;
+  final void Function(TreemapNode? node) onRootChangedCallback;
+  final Widget child;
+
+  @override
+  Widget build(BuildContext context) {
+    return DevToolsTooltip(
+      message: node.displayText(),
+      waitDuration: tooltipWaitLong,
+      child: InkWell(
+        onTap: () {
+          onRootChangedCallback(node);
+        },
+        child: child,
+      ),
+    );
+  }
+}
+
 class TreemapNode extends TreeNode<TreemapNode> {
   TreemapNode({
     required this.name,
@@ -618,10 +706,8 @@
     return byteSize < 0 ? treemapDecreaseColor : treemapIncreaseColor;
   }
 
-  TextSpan displayText({Color? color, bool oneLine = true}) {
+  String displayText({bool singleLine = true, bool includeCaption = true}) {
     var displayName = name;
-    final textColor = color ?? (showDiff ? Colors.white : Colors.black);
-
     // Trim beginning of the name of [this] if it starts with its parent's name.
     // If the parent node and the child node's name are exactly the same,
     // do not trim in order to avoid empty names.
@@ -636,18 +722,35 @@
       }
     }
 
-    final separator = oneLine ? ' ' : '\n';
+    final separator = singleLine ? ' ' : '\n';
+    displayName = '$displayName$separator[${prettyByteSize()}]';
+    if (includeCaption && caption != null) {
+      displayName = '$displayName$separator$caption';
+    }
 
+    return displayName;
+  }
+
+  TextSpan displayTextSpan({
+    required TextStyle defaultTextStyle,
+    Color? color,
+    bool singleLine = true,
+  }) {
+    final textColor = color ?? (showDiff ? Colors.white : Colors.black);
+    final separator = singleLine ? ' ' : '\n';
     return TextSpan(
-      text: '$displayName$separator[${prettyByteSize()}]',
+      text: displayText(includeCaption: false, singleLine: singleLine),
+      style: defaultTextStyle.copyWith(color: textColor),
       children: [
         if (caption != null)
           TextSpan(
             text: '$separator$caption',
-            style: TextStyle(fontStyle: FontStyle.italic, color: textColor),
+            style: defaultTextStyle.copyWith(
+              fontStyle: FontStyle.italic,
+              color: textColor,
+            ),
           ),
       ],
-      style: TextStyle(color: textColor),
     );
   }
 
@@ -728,13 +831,12 @@
     super.key,
     required this.rect,
     required this.node,
-    required child,
+    required super.child,
   }) : super(
           left: rect.left,
           top: rect.top,
           width: rect.width,
           height: rect.height,
-          child: child,
         );
 
   final Rect rect;
@@ -743,9 +845,10 @@
 }
 
 class MultiCellPainter extends CustomPainter {
-  const MultiCellPainter({required this.nodes});
+  const MultiCellPainter({required this.nodes, required this.defaultTextStyle});
 
   final List<PositionedCell> nodes;
+  final TextStyle defaultTextStyle;
 
   @override
   void paint(Canvas canvas, Size size) {
@@ -780,7 +883,10 @@
     if (positionedCell.width! > Treemap.minWidthToDisplayCellText &&
         positionedCell.height! > Treemap.minHeightToDisplayCellText) {
       final textPainter = TextPainter(
-        text: node.displayText(oneLine: false),
+        text: node.displayTextSpan(
+          defaultTextStyle: defaultTextStyle,
+          singleLine: false,
+        ),
         textDirection: TextDirection.ltr,
         textAlign: TextAlign.center,
         ellipsis: '...',
diff --git a/packages/devtools_app/lib/src/shared/common_widgets.dart b/packages/devtools_app/lib/src/shared/common_widgets.dart
index 9419613..8f64509 100644
--- a/packages/devtools_app/lib/src/shared/common_widgets.dart
+++ b/packages/devtools_app/lib/src/shared/common_widgets.dart
@@ -295,34 +295,17 @@
   }) : super(outlined: true, icon: Icons.settings_outlined);
 }
 
-class HelpButton extends StatelessWidget {
-  const HelpButton({
+class HelpButton extends GaDevToolsButton {
+  HelpButton({
     super.key,
-    required this.gaScreen,
-    required this.gaSelection,
-    required this.onPressed,
-    this.outlined = true,
-  });
-
-  final VoidCallback onPressed;
-
-  final String gaScreen;
-
-  final String gaSelection;
-
-  final bool outlined;
-
-  @override
-  Widget build(BuildContext context) {
-    return GaDevToolsButton(
-      icon: Icons.help_outline,
-      onPressed: onPressed,
-      tooltip: 'Help',
-      gaScreen: gaScreen,
-      gaSelection: gaSelection,
-      outlined: outlined,
-    );
-  }
+    required super.gaScreen,
+    required super.gaSelection,
+    required super.onPressed,
+    super.outlined = true,
+  }) : super(
+          icon: Icons.help_outline,
+          tooltip: 'Help',
+        );
 }
 
 class ExpandAllButton extends StatelessWidget {
@@ -453,7 +436,7 @@
   @override
   Widget build(BuildContext context) {
     return Container(
-      height: defaultSwitchHeight,
+      height: defaultButtonHeight,
       padding: padding,
       child: FittedBox(
         fit: BoxFit.fill,
@@ -573,7 +556,8 @@
 
     // These constants are sized to give 1 digit badges a circular look.
     const badgeCornerRadius = 12.0;
-    const badgePadding = 6.0;
+    const verticalBadgePadding = 1.0;
+    const horizontalBadgePadding = 6.0;
 
     return Container(
       decoration: BoxDecoration(
@@ -581,8 +565,8 @@
         borderRadius: BorderRadius.circular(badgeCornerRadius),
       ),
       padding: const EdgeInsets.symmetric(
-        vertical: borderPadding,
-        horizontal: badgePadding,
+        vertical: verticalBadgePadding,
+        horizontal: horizontalBadgePadding,
       ),
       child: Text(
         text,
@@ -664,7 +648,7 @@
 
   final String tooltip;
 
-  final Function(BuildContext) onPressed;
+  final void Function(BuildContext) onPressed;
 
   final Color? color;
 
@@ -689,22 +673,6 @@
   }
 }
 
-/// A blank, drop-in replacement for [AreaPaneHeader].
-///
-/// Acts as an empty header widget with zero size that is compatible with
-/// interfaces that expect a [PreferredSizeWidget].
-class BlankHeader extends StatelessWidget implements PreferredSizeWidget {
-  const BlankHeader({super.key});
-
-  @override
-  Widget build(BuildContext context) {
-    return Container();
-  }
-
-  @override
-  Size get preferredSize => Size.zero;
-}
-
 /// Button to open related information / documentation.
 ///
 /// [tooltip] specifies the hover text for the button.
@@ -777,12 +745,14 @@
 
   @override
   Widget build(BuildContext context) {
-    final bgColor = Theme.of(context).colorScheme.backgroundColorSelected;
+    final theme = Theme.of(context);
+    final bgColor = theme.colorScheme.backgroundColorSelected;
 
     Radius selectRadius(bool show) {
       return show ? defaultRadius : Radius.zero;
     }
 
+    final style = this.style ?? theme.regularTextStyle;
     final showTopLeft = roundedCornerOptions?.showTopLeft ?? true;
     final showTopRight = roundedCornerOptions?.showTopRight ?? true;
     final showBottomLeft = roundedCornerOptions?.showBottomLeft ?? true;
@@ -830,7 +800,9 @@
     TextEditingController? controller,
     this.hintText,
     this.prefixIcon,
+    this.additionalSuffixActions = const <Widget>[],
     this.onChanged,
+    this.onSubmitted,
     this.autofocus = false,
   })  : controller = controller ?? TextEditingController(),
         super(key: key);
@@ -838,59 +810,93 @@
   final TextEditingController controller;
   final String? hintText;
   final Widget? prefixIcon;
+  final List<Widget> additionalSuffixActions;
   final String labelText;
-  final Function(String)? onChanged;
+  final void Function(String)? onChanged;
+  final void Function(String)? onSubmitted;
   final bool autofocus;
 
+  static const _contentVerticalPadding = 6.0;
+
   @override
   Widget build(BuildContext context) {
-    return TextField(
-      autofocus: autofocus,
-      controller: controller,
-      onChanged: onChanged,
-      decoration: InputDecoration(
-        contentPadding: const EdgeInsets.all(denseSpacing),
-        constraints: BoxConstraints(
-          minHeight: defaultTextFieldHeight,
-          maxHeight: defaultTextFieldHeight,
+    final theme = Theme.of(context);
+    return SizedBox(
+      height: defaultTextFieldHeight,
+      child: TextField(
+        autofocus: autofocus,
+        controller: controller,
+        onChanged: onChanged,
+        onSubmitted: onSubmitted,
+        style: theme.regularTextStyle,
+        decoration: InputDecoration(
+          isDense: true,
+          contentPadding: const EdgeInsets.only(
+            top: _contentVerticalPadding,
+            bottom: _contentVerticalPadding,
+            left: denseSpacing,
+            right: densePadding,
+          ),
+          constraints: BoxConstraints(
+            minHeight: defaultTextFieldHeight,
+            maxHeight: defaultTextFieldHeight,
+          ),
+          border: const OutlineInputBorder(),
+          labelText: labelText,
+          labelStyle: theme.subtleTextStyle,
+          hintText: hintText,
+          hintStyle: theme.subtleTextStyle,
+          prefixIcon: prefixIcon,
+          suffix: SizedBox(
+            height: inputDecorationElementHeight,
+            child: Row(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                clearInputButton(
+                  () {
+                    controller.clear();
+                    onChanged?.call('');
+                  },
+                ),
+                ...additionalSuffixActions,
+              ],
+            ),
+          ),
         ),
-        border: const OutlineInputBorder(),
-        labelText: labelText,
-        hintText: hintText,
-        prefixIcon: prefixIcon,
-        suffixIcon: IconButton(
-          tooltip: 'Clear',
-          icon: const Icon(Icons.clear),
-          onPressed: () {
-            controller.clear();
-            onChanged?.call('');
-          },
-        ),
-        isDense: true,
       ),
     );
   }
 }
 
 Widget clearInputButton(VoidCallback onPressed) {
-  return inputDecorationSuffixButton(Icons.clear, onPressed);
+  return inputDecorationSuffixButton(
+    icon: Icons.clear,
+    onPressed: onPressed,
+    tooltip: 'Clear',
+  );
 }
 
 Widget closeSearchDropdownButton(VoidCallback? onPressed) {
-  return inputDecorationSuffixButton(Icons.close, onPressed);
+  return inputDecorationSuffixButton(icon: Icons.close, onPressed: onPressed);
 }
 
-Widget inputDecorationSuffixButton(IconData icon, VoidCallback? onPressed) {
-  return Container(
-    padding: const EdgeInsets.symmetric(horizontal: densePadding),
-    height: inputDecorationElementHeight,
-    width: defaultIconSize + denseSpacing,
-    child: IconButton(
-      padding: const EdgeInsets.all(0.0),
-      onPressed: onPressed,
-      iconSize: defaultIconSize,
-      splashRadius: defaultIconSize,
-      icon: Icon(icon),
+Widget inputDecorationSuffixButton({
+  required IconData icon,
+  required VoidCallback? onPressed,
+  String? tooltip,
+}) {
+  return maybeWrapWithTooltip(
+    tooltip: tooltip,
+    child: SizedBox(
+      height: inputDecorationElementHeight,
+      width: inputDecorationElementHeight + denseSpacing,
+      child: IconButton(
+        padding: EdgeInsets.zero,
+        onPressed: onPressed,
+        iconSize: defaultIconSize,
+        splashRadius: defaultIconSize,
+        icon: Icon(icon),
+      ),
     ),
   );
 }
@@ -969,6 +975,7 @@
 /// Makes for nice-looking rectangles.
 final goldenRatio = 1 + sqrt(5) / 2;
 
+/// A centered text widget with the default DevTools text style applied.
 class CenteredMessage extends StatelessWidget {
   const CenteredMessage(this.message, {super.key});
 
@@ -977,7 +984,11 @@
   @override
   Widget build(BuildContext context) {
     return Center(
-      child: Text(message),
+      child: Text(
+        message,
+        textAlign: TextAlign.center,
+        style: Theme.of(context).regularTextStyle,
+      ),
     );
   }
 }
@@ -1099,7 +1110,7 @@
     required this.onPressed,
   });
 
-  static const height = 28.0;
+  static const height = 24.0;
 
   static const caretWidth = 4.0;
 
@@ -1119,7 +1130,7 @@
     final textPainter = TextPainter(
       text: TextSpan(
         text: text,
-        style: TextStyle(
+        style: theme.regularTextStyle.copyWith(
           color: theme.colorScheme.contrastTextColor,
           decoration: TextDecoration.underline,
         ),
@@ -1203,18 +1214,6 @@
   }
 }
 
-class JsonViewer extends StatefulWidget {
-  const JsonViewer({
-    super.key,
-    required this.encodedJson,
-  });
-
-  final String encodedJson;
-
-  @override
-  State<JsonViewer> createState() => _JsonViewerState();
-}
-
 /// A wrapper for a Text widget, which allows for concatenating text if it
 /// becomes too long.
 class TextViewer extends StatelessWidget {
@@ -1247,6 +1246,18 @@
   }
 }
 
+class JsonViewer extends StatefulWidget {
+  const JsonViewer({
+    super.key,
+    required this.encodedJson,
+  });
+
+  final String encodedJson;
+
+  @override
+  State<JsonViewer> createState() => _JsonViewerState();
+}
+
 class _JsonViewerState extends State<JsonViewer>
     with ProvidedControllerMixin<DebuggerController, JsonViewer> {
   late Future<void> _initializeTree;
@@ -1569,7 +1580,7 @@
               copyToClipboard(dataProvider!() ?? '', successMessage),
             );
           };
-
+    final size = this.size ?? defaultIconSize;
     return SizedBox(
       height: size,
       child: ToolbarAction(
@@ -1703,7 +1714,7 @@
         Flexible(
           child: RichText(
             overflow: TextOverflow.visible,
-            maxLines: 2,
+            maxLines: 3,
             text: TextSpan(
               text: title,
               style: enabled ? theme.regularTextStyle : theme.subtleTextStyle,
@@ -1855,13 +1866,13 @@
 /// A widget that listens for changes to multiple different [ValueListenable]s
 /// and rebuilds for change notifications from any of them.
 ///
-/// The current value of each [ValueListenable] is provided by the [values]
+/// The current value of each [ValueListenable] is provided by the `values`
 /// parameter in [builder], where the index of each value in the list is equal
 /// to the index of its parent [ValueListenable] in [listenables].
 ///
 /// This widget is preferred over nesting many [ValueListenableBuilder]s in a
 /// single build method.
-class MultiValueListenableBuilder<T, U> extends StatefulWidget {
+class MultiValueListenableBuilder extends StatefulWidget {
   const MultiValueListenableBuilder({
     super.key,
     required this.listenables,
@@ -1880,12 +1891,12 @@
   final Widget? child;
 
   @override
-  State<MultiValueListenableBuilder<T, U>> createState() =>
-      _MultiValueListenableBuilderState<T, U>();
+  State<MultiValueListenableBuilder> createState() =>
+      _MultiValueListenableBuilderState();
 }
 
-class _MultiValueListenableBuilderState<T, U>
-    extends State<MultiValueListenableBuilder<T, U>> with AutoDisposeMixin {
+class _MultiValueListenableBuilderState
+    extends State<MultiValueListenableBuilder> with AutoDisposeMixin {
   @override
   void initState() {
     super.initState();
@@ -1988,6 +1999,7 @@
     required this.gaSelection,
     required this.dialogTitle,
     required this.child,
+    this.actions = const <Widget>[],
     this.outlined = true,
   });
 
@@ -1999,25 +2011,19 @@
 
   final Widget child;
 
+  final List<Widget> actions;
+
   final bool outlined;
 
   @override
   Widget build(BuildContext context) {
     return HelpButton(
       onPressed: () {
-        ga.select(gaScreen, gaSelection);
-        unawaited(
-          showDialog(
-            context: context,
-            builder: (context) => DevToolsDialog(
-              title: DialogTitleText(dialogTitle),
-              includeDivider: false,
-              content: child,
-              actions: const [
-                DialogCloseButton(),
-              ],
-            ),
-          ),
+        showDevToolsDialog(
+          context: context,
+          title: dialogTitle,
+          content: child,
+          actions: actions,
         );
       },
       gaScreen: gaScreen,
@@ -2038,18 +2044,15 @@
 
   @override
   Widget build(BuildContext context) {
-    final ThemeData theme = Theme.of(context);
-
-    final textStyle = theme.textTheme.bodyMedium;
-    final mutedColor = textStyle?.color?.withAlpha(0x90);
-
+    final theme = Theme.of(context);
+    final mutedColor = theme.colorScheme.onSurface.withAlpha(0x90);
     return Container(
       width: width,
       height: actionWidgetSize,
       alignment: Alignment.center,
       child: Text(
         '•',
-        style: textStyle?.copyWith(color: color ?? mutedColor),
+        style: theme.regularTextStyle.copyWith(color: color ?? mutedColor),
       ),
     );
   }
diff --git a/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/_drag_and_drop_web.dart b/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/_drag_and_drop_web.dart
index 7b816e5..779e281 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/_drag_and_drop_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/drag_and_drop/_drag_and_drop_web.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import '../../globals.dart';
 import '../../primitives/utils.dart';
diff --git a/packages/devtools_app/lib/src/shared/config_specific/framework_initialize/_framework_initialize_web.dart b/packages/devtools_app/lib/src/shared/config_specific/framework_initialize/_framework_initialize_web.dart
index 632b2ec..d3b35c5 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/framework_initialize/_framework_initialize_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/framework_initialize/_framework_initialize_web.dart
@@ -6,7 +6,7 @@
 
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_app_shared/web_utils.dart';
-import 'package:web/helpers.dart' hide Storage;
+import 'package:web/web.dart' hide Storage;
 
 import '../../../service/service_manager.dart';
 import '../../globals.dart';
diff --git a/packages/devtools_app/lib/src/shared/config_specific/host_platform/_host_platform_web.dart b/packages/devtools_app/lib/src/shared/config_specific/host_platform/_host_platform_web.dart
index 607b718..6ccd06f 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/host_platform/_host_platform_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/host_platform/_host_platform_web.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.
 
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 class HostPlatform {
   HostPlatform._() {
diff --git a/packages/devtools_app/lib/src/shared/config_specific/import_export/_export_web.dart b/packages/devtools_app/lib/src/shared/config_specific/import_export/_export_web.dart
index 5abfde4..c2f22b9 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/import_export/_export_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/import_export/_export_web.dart
@@ -4,7 +4,7 @@
 
 import 'dart:js_interop';
 
-import 'package:web/helpers.dart' hide NodeGlue;
+import 'package:web/web.dart' hide NodeGlue;
 
 import 'import_export.dart';
 
diff --git a/packages/devtools_app/lib/src/shared/config_specific/launch_url/_launch_url_web.dart b/packages/devtools_app/lib/src/shared/config_specific/launch_url/_launch_url_web.dart
index d18fb01..6878bbc 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/launch_url/_launch_url_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/launch_url/_launch_url_web.dart
@@ -4,7 +4,7 @@
 
 import 'dart:js_interop';
 
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 void launchUrlVSCode(String url) {
   window.parent?.postMessage(
diff --git a/packages/devtools_app/lib/src/shared/config_specific/logger/_allowed_error_web.dart b/packages/devtools_app/lib/src/shared/config_specific/logger/_allowed_error_web.dart
index 27587f9..fc9236c 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/logger/_allowed_error_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/logger/_allowed_error_web.dart
@@ -4,7 +4,7 @@
 
 import 'dart:js_interop';
 
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 /// Catch and print errors from the given future. These errors are part of
 /// normal operation for an app, and don't need to be reported to analytics
diff --git a/packages/devtools_app/lib/src/shared/config_specific/logger/_logger_web.dart b/packages/devtools_app/lib/src/shared/config_specific/logger/_logger_web.dart
index 4e26fa8..4c648a8 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/logger/_logger_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/logger/_logger_web.dart
@@ -4,7 +4,7 @@
 
 import 'dart:js_interop';
 
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import 'logger.dart';
 
diff --git a/packages/devtools_app/lib/src/shared/config_specific/notifications/_notifications_web.dart b/packages/devtools_app/lib/src/shared/config_specific/notifications/_notifications_web.dart
index b0617d2..ef67bbd 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/notifications/_notifications_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/notifications/_notifications_web.dart
@@ -4,22 +4,20 @@
 
 import 'dart:js_interop';
 
-import 'package:web/helpers.dart' as web_helpers;
-import 'package:web/web.dart';
+import 'package:web/web.dart' as web;
 
 class Notification {
   Notification(String title, {String body = ''}) {
-    _impl = web_helpers.Notification(
+    _impl = web.Notification(
       title,
-      NotificationOptions(body: body),
+      web.NotificationOptions(body: body),
     );
   }
 
-  late final web_helpers.Notification _impl;
+  late final web.Notification _impl;
 
   static Future<String> requestPermission() async =>
-      ((await web_helpers.Notification.requestPermission().toDart) as JSString)
-          .toDart;
+      ((await web.Notification.requestPermission().toDart) as JSString).toDart;
 
   void close() {
     _impl.close();
diff --git a/packages/devtools_app/lib/src/shared/config_specific/post_message/_post_message_web.dart b/packages/devtools_app/lib/src/shared/config_specific/post_message/_post_message_web.dart
index cd73bc9..36d15b6 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/post_message/_post_message_web.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/post_message/_post_message_web.dart
@@ -4,7 +4,7 @@
 
 import 'dart:js_interop';
 
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import 'post_message.dart';
 
diff --git a/packages/devtools_app/lib/src/shared/console/console.dart b/packages/devtools_app/lib/src/shared/console/console.dart
index 7372da2..41c7639 100644
--- a/packages/devtools_app/lib/src/shared/console/console.dart
+++ b/packages/devtools_app/lib/src/shared/console/console.dart
@@ -189,7 +189,7 @@
               parent: RangeMaintainingScrollPhysics(),
             ),
             separatorBuilder: (_, __) {
-              return const Divider();
+              return const PaddedDivider.noPadding();
             },
             itemBuilder: (context, index) {
               if (index == _currentLines.length && widget.footer != null) {
@@ -203,7 +203,7 @@
                     // codes.
                     children: processAnsiTerminalCodes(
                       line.text,
-                      theme.fixedFontStyle,
+                      theme.regularTextStyle,
                     ),
                   ),
                 );
@@ -249,6 +249,7 @@
   Widget build(BuildContext context) {
     return ToolbarAction(
       icon: Icons.delete,
+      size: defaultIconSize,
       tooltip: tooltip,
       onPressed: onPressed,
       key: buttonKey,
diff --git a/packages/devtools_app/lib/src/shared/console/eval/eval_service.dart b/packages/devtools_app/lib/src/shared/console/eval/eval_service.dart
index b64b3c7..a03b377 100644
--- a/packages/devtools_app/lib/src/shared/console/eval/eval_service.dart
+++ b/packages/devtools_app/lib/src/shared/console/eval/eval_service.dart
@@ -78,14 +78,12 @@
 
     final isolateId = isolateRef.id!;
 
-    final scope = await _scopeIfSupported(isolateId);
-
     Future<Response> eval() async =>
         await serviceConnection.serviceManager.service!.evaluate(
           isolateId,
           (await isolate.isolate)!.rootLib!.id!,
           expressionText,
-          scope: scope,
+          scope: scope.value(isolateId: isolateId),
         );
 
     return await _evalWithVariablesRefresh(eval, isolateId);
@@ -166,42 +164,17 @@
       );
     }
 
-    final scope = await _scopeIfSupported(isolateRefId);
-
     Future<Response> evalFunction() => _service.evaluateInFrame(
           isolateRefId,
           frame.index!,
           expression,
           disableBreakpoints: true,
-          scope: scope,
+          scope: scope.value(isolateId: isolateRefId),
         );
 
     return await _evalWithVariablesRefresh(evalFunction, isolateRefId);
   }
 
-  Future<Map<String, String>?> _scopeIfSupported(String isolateRefId) async {
-    if (!isScopeSupported()) return null;
-
-    return scope.value(isolateId: isolateRefId);
-  }
-
-  /// If scope is supported, returns true.
-  ///
-  /// If [emitWarningToConsole] and scope is not supported, emits warning message to console.
-  bool isScopeSupported({bool emitWarningToConsole = false}) {
-    // Web does not support scopes yet.
-    final isWeb =
-        serviceConnection.serviceManager.connectedApp?.isDartWebAppNow ?? true;
-    if (isWeb && emitWarningToConsole) {
-      serviceConnection.consoleService.appendStdio(
-        'Scope variables are not supported for web applications.',
-      );
-
-      return false;
-    }
-    return true;
-  }
-
   Future<InstanceRef?> findObject(
     AdaptedHeapObject object,
     IsolateRef isolateRef,
diff --git a/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart b/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart
index b7b7436..3c40b2a 100644
--- a/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart
+++ b/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart
@@ -14,7 +14,6 @@
 
 import '../../diagnostics/diagnostics_node.dart';
 import '../../ui/search.dart';
-import '../../utils.dart';
 
 /// Split text into two groups, word characters at the start of a string and all
 /// other characters.
@@ -29,8 +28,8 @@
 
 const double iconPadding = 4.0;
 const double chartLineStrokeWidth = 1.0;
-double get columnWidth => scaleByFontFactor(isDense() ? 12.0 : 16.0);
-double get rowHeight => scaleByFontFactor(isDense() ? 20.0 : 24.0);
+double get inspectorColumnWidth => scaleByFontFactor(12.0);
+double get inspectorRowHeight => scaleByFontFactor(16.0);
 
 /// This class could be refactored out to be a reasonable generic collapsible
 /// tree ui node class but we choose to instead make it widget inspector
diff --git a/packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart b/packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart
index f0c6037..4f376d6 100644
--- a/packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart
+++ b/packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart
@@ -16,17 +16,19 @@
 // TODO(devoncarew): Show some small UI indicator when we receive stdout/stderr.
 
 class ConsolePaneHeader extends AreaPaneHeader {
-  ConsolePaneHeader({super.key, Color? backgroundColor})
+  ConsolePaneHeader({super.key})
       : super(
           title: const Text('Console'),
           roundedTopBorder: true,
           actions: [
             const ConsoleHelpLink(),
+            const SizedBox(width: densePadding),
             CopyToClipboardControl(
               dataProvider: () =>
                   serviceConnection.consoleService.stdio.value.join('\n'),
               buttonKey: ConsolePane.copyToClipboardButtonKey,
             ),
+            const SizedBox(width: densePadding),
             DeleteControl(
               buttonKey: ConsolePane.clearStdioButtonKey,
               tooltip: 'Clear console output',
diff --git a/packages/devtools_app/lib/src/shared/console/widgets/evaluate.dart b/packages/devtools_app/lib/src/shared/console/widgets/evaluate.dart
index bfd8e2e..0a1f506 100644
--- a/packages/devtools_app/lib/src/shared/console/widgets/evaluate.dart
+++ b/packages/devtools_app/lib/src/shared/console/widgets/evaluate.dart
@@ -37,6 +37,8 @@
 
   final AutoCompleteResultsFunction getAutoCompleteResults;
 
+  static const _evalFieldHeight = 32.0;
+
   @override
   ExpressionEvalFieldState createState() => ExpressionEvalFieldState();
 }
@@ -202,7 +204,7 @@
     return Row(
       children: [
         const Text('>'),
-        const SizedBox(width: 8.0),
+        const SizedBox(width: denseSpacing),
         Expanded(
           child: Focus(
             onKeyEvent: (_, event) {
@@ -220,38 +222,46 @@
 
               return KeyEventResult.ignored;
             },
-            child: AutoCompleteSearchField(
-              controller: _autoCompleteController,
-              searchFieldEnabled: true,
-              shouldRequestFocus: false,
-              clearFieldOnEscapeWhenOverlayHidden: true,
-              onSelection: _onSelection,
-              decoration: const InputDecoration(
-                contentPadding: EdgeInsets.all(denseSpacing),
-                border: OutlineInputBorder(),
-                focusedBorder: OutlineInputBorder(borderSide: BorderSide.none),
-                enabledBorder: OutlineInputBorder(borderSide: BorderSide.none),
-                labelText: 'Eval. Enter "?" for help.',
+            child: SizedBox(
+              height: ExpressionEvalField._evalFieldHeight,
+              child: AutoCompleteSearchField(
+                controller: _autoCompleteController,
+                searchFieldEnabled: true,
+                shouldRequestFocus: false,
+                clearFieldOnEscapeWhenOverlayHidden: true,
+                onSelection: _onSelection,
+                decoration: InputDecoration(
+                  contentPadding: const EdgeInsets.all(denseSpacing),
+                  border: const OutlineInputBorder(),
+                  focusedBorder:
+                      const OutlineInputBorder(borderSide: BorderSide.none),
+                  enabledBorder:
+                      const OutlineInputBorder(borderSide: BorderSide.none),
+                  labelText: 'Eval. Enter "?" for help.',
+                  labelStyle: Theme.of(context).subtleTextStyle,
+                ),
+                overlayXPositionBuilder:
+                    (String inputValue, TextStyle? inputStyle) {
+                  // X-coordinate is equivalent to the width of the input text
+                  // up to the last "." or the insertion point (cursor):
+                  final indexOfDot = inputValue.lastIndexOf('.');
+                  final textSegment = indexOfDot != -1
+                      ? inputValue.substring(0, indexOfDot + 1)
+                      : inputValue;
+                  return calculateTextSpanWidth(
+                    TextSpan(
+                      text: textSegment,
+                      style: inputStyle,
+                    ),
+                  );
+                },
+                // Disable ligatures, so the suggestions of the auto complete work correcly.
+                style: Theme.of(context).fixedFontStyle.copyWith(
+                  fontFeatures: [
+                    const FontFeature.disable('liga'),
+                  ],
+                ),
               ),
-              overlayXPositionBuilder:
-                  (String inputValue, TextStyle? inputStyle) {
-                // X-coordinate is equivalent to the width of the input text
-                // up to the last "." or the insertion point (cursor):
-                final indexOfDot = inputValue.lastIndexOf('.');
-                final textSegment = indexOfDot != -1
-                    ? inputValue.substring(0, indexOfDot + 1)
-                    : inputValue;
-                return calculateTextSpanWidth(
-                  TextSpan(
-                    text: textSegment,
-                    style: inputStyle,
-                  ),
-                );
-              },
-              // Disable ligatures, so the suggestions of the auto complete work correcly.
-              style: Theme.of(context)
-                  .fixedFontStyle
-                  .copyWith(fontFeatures: [const FontFeature.disable('liga')]),
             ),
           ),
         ),
@@ -438,10 +448,6 @@
     if (assignment == null) return false;
     const kSuccess = true;
 
-    if (!evalService.isScopeSupported(emitWarningToConsole: true)) {
-      return kSuccess;
-    }
-
     final variable = serviceConnection.consoleService
         .itemAt(assignment.consoleItemIndex + 1);
     final value = variable?.value;
diff --git a/packages/devtools_app/lib/src/shared/console/widgets/help_dialog.dart b/packages/devtools_app/lib/src/shared/console/widgets/help_dialog.dart
index b6b8d4f..f4ab848 100644
--- a/packages/devtools_app/lib/src/shared/console/widgets/help_dialog.dart
+++ b/packages/devtools_app/lib/src/shared/console/widgets/help_dialog.dart
@@ -70,6 +70,7 @@
   Widget build(BuildContext context) {
     return ToolbarAction(
       icon: Icons.help_outline,
+      size: defaultIconSize,
       tooltip: 'Console Help',
       onPressed: () {
         unawaited(
diff --git a/packages/devtools_app/lib/src/shared/development_helpers.dart b/packages/devtools_app/lib/src/shared/development_helpers.dart
index 1d23d5a..71fe7f0 100644
--- a/packages/devtools_app/lib/src/shared/development_helpers.dart
+++ b/packages/devtools_app/lib/src/shared/development_helpers.dart
@@ -21,19 +21,20 @@
 ///
 /// This flag should never be checked in with a value of true - this is covered
 /// by a test.
-final debugDevToolsExtensions = _debugDevToolsExtensions || integrationTestMode;
+final debugDevToolsExtensions =
+    _debugDevToolsExtensions || integrationTestMode || testMode || stagerMode;
 const _debugDevToolsExtensions = false;
 
 List<DevToolsExtensionConfig> debugHandleRefreshAvailableExtensions(
   // ignore: avoid-unused-parameters, false positive due to conditional imports
-  String rootPath,
+  Uri appRoot,
 ) {
   return debugExtensions;
 }
 
 ExtensionEnabledState debugHandleExtensionEnabledState({
   // ignore: avoid-unused-parameters, false positive due to conditional imports
-  required String rootPath,
+  required Uri appRoot,
   required String extensionName,
   bool? enable,
 }) {
diff --git a/packages/devtools_app/lib/src/shared/diagnostics/dap_object_node.dart b/packages/devtools_app/lib/src/shared/diagnostics/dap_object_node.dart
index 978e8f8..0427229 100644
--- a/packages/devtools_app/lib/src/shared/diagnostics/dap_object_node.dart
+++ b/packages/devtools_app/lib/src/shared/diagnostics/dap_object_node.dart
@@ -10,7 +10,7 @@
 import '../primitives/trees.dart';
 
 class DapObjectNode extends TreeNode<DapObjectNode> {
-  DapObjectNode({required this.variable, required service})
+  DapObjectNode({required this.variable, required VmServiceWrapper service})
       : _service = service;
 
   final dap.Variable variable;
diff --git a/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart b/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart
index 754fed8..c92f525 100644
--- a/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart
+++ b/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart
@@ -548,7 +548,7 @@
             WidgetInspectorServiceExtensions.disposeGroup.name,
             groupName,
           )
-        : Future.value();
+        : Future<void>.value();
     disposed = true;
     return disposeComplete;
   }
diff --git a/packages/devtools_app/lib/src/shared/directory_picker.dart b/packages/devtools_app/lib/src/shared/directory_picker.dart
index 1f84aa1..98ee326 100644
--- a/packages/devtools_app/lib/src/shared/directory_picker.dart
+++ b/packages/devtools_app/lib/src/shared/directory_picker.dart
@@ -31,7 +31,6 @@
 
   @override
   Widget build(BuildContext context) {
-    final rowHeight = defaultButtonHeight;
     return Row(
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
@@ -41,7 +40,7 @@
           fit: FlexFit.tight,
           child: RoundedOutlinedBorder(
             child: Container(
-              height: rowHeight,
+              height: defaultTextFieldHeight,
               padding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
               child: TextField(
                 controller: controller,
@@ -50,11 +49,10 @@
                   widget.onDirectoryPicked(path.trim());
                 },
                 decoration: const InputDecoration(
+                  isDense: true,
                   hintText: 'Enter path to a Flutter project here',
                 ),
-                style: TextStyle(
-                  color: Theme.of(context).textTheme.displayLarge!.color,
-                ),
+                style: Theme.of(context).regularTextStyle,
                 textAlign: TextAlign.left,
               ),
             ),
diff --git a/packages/devtools_app/lib/src/shared/editable_list.dart b/packages/devtools_app/lib/src/shared/editable_list.dart
index 6d01312..46ab21a 100644
--- a/packages/devtools_app/lib/src/shared/editable_list.dart
+++ b/packages/devtools_app/lib/src/shared/editable_list.dart
@@ -25,8 +25,8 @@
     required this.gaRefreshSelection,
     this.isRefreshing,
     this.onRefreshTriggered,
-    Function(String)? onEntryAdded,
-    Function(String)? onEntryRemoved,
+    void Function(String)? onEntryAdded,
+    void Function(String)? onEntryRemoved,
   })  : onEntryAdded = onEntryAdded ?? ((entry) => entries.add(entry)),
         onEntryRemoved = onEntryRemoved ?? ((entry) => entries.remove(entry));
 
@@ -48,16 +48,16 @@
   /// Triggered when an entry is added, using the interface.
   ///
   /// When not overridden, the default behaviour adds the entry to [entries]
-  late final Function(String) onEntryAdded;
+  late final void Function(String) onEntryAdded;
 
   /// Triggered when an entry is removed, using the interface.
   ///
   /// When not overridden, the default behaviour removes the entry
   /// from [entries].
-  late final Function(String) onEntryRemoved;
+  late final void Function(String) onEntryRemoved;
 
   /// Triggered when the refresh is triggered, using the interface.
-  final Function()? onRefreshTriggered;
+  final void Function()? onRefreshTriggered;
 
   final String gaScreen;
 
@@ -105,7 +105,7 @@
               gaScreen: widget.gaScreen,
               gaRefreshSelection: widget.gaRefreshSelection,
             ),
-            const SizedBox(height: denseSpacing),
+            const SizedBox(height: defaultSpacing),
             Expanded(
               child: _EditableListContentView(
                 entries: widget.entries,
@@ -137,8 +137,8 @@
   final TextEditingController textFieldController;
   final ValueListenable<bool>? isRefreshing;
   final String textFieldLabel;
-  final Function(String) onEntryAdded;
-  final Function()? onRefresh;
+  final void Function(String) onEntryAdded;
+  final void Function()? onRefresh;
   final String gaScreen;
   final String gaRefreshSelection;
 
@@ -159,22 +159,15 @@
       child: Row(
         children: [
           Expanded(
-            child: SizedBox(
-              height: defaultTextFieldHeight,
-              child: TextField(
-                focusNode: textFieldFocusNode,
-                controller: textFieldController,
-                decoration: InputDecoration(
-                  contentPadding: const EdgeInsets.all(denseSpacing),
-                  border: const OutlineInputBorder(),
-                  labelText: textFieldLabel,
-                ),
-                onSubmitted: (value) {
-                  _addNewItem();
-                },
-              ),
+            child: DevToolsClearableTextField(
+              controller: textFieldController,
+              labelText: textFieldLabel,
+              onSubmitted: (value) {
+                _addNewItem();
+              },
             ),
           ),
+          const SizedBox(width: densePadding),
           TextButton(
             onPressed: () {
               _addNewItem();
@@ -183,6 +176,7 @@
               'Add',
             ), // TODO:(https://github.com/flutter/devtools/issues/4381)
           ),
+          const SizedBox(width: densePadding),
           isRefreshing?.value ?? false
               ? SizedBox(
                   width: defaultTextFieldHeight,
@@ -212,7 +206,7 @@
   }) : super(key: key);
 
   final ListValueNotifier<String> entries;
-  final Function(String) onEntryRemoved;
+  final void Function(String) onEntryRemoved;
   final ScrollController _listContentScrollController = ScrollController();
 
   @override
@@ -243,33 +237,27 @@
   }) : super(key: key);
 
   final String entry;
-  final Function(String) onEntryRemoved;
+  final void Function(String) onEntryRemoved;
 
   @override
   Widget build(BuildContext context) {
-    return Padding(
-      padding: const EdgeInsets.symmetric(
-        horizontal: densePadding,
-      ),
-      child: Row(
-        children: [
-          Expanded(
-            child: Text(entry),
-          ),
-          EditableListCopyDirectoryButton(
-            value: entry,
-          ),
-          const SizedBox(width: denseSpacing),
-          EditableListRemoveDirectoryButton(
-            onPressed: () {
-              onEntryRemoved(
-                entry,
-              );
-            },
-          ),
-          const SizedBox(width: denseRowSpacing),
-        ],
-      ),
+    return Row(
+      children: [
+        Expanded(
+          child: Text(entry),
+        ),
+        EditableListCopyDirectoryButton(
+          value: entry,
+        ),
+        const SizedBox(width: denseSpacing),
+        EditableListRemoveDirectoryButton(
+          onPressed: () {
+            onEntryRemoved(
+              entry,
+            );
+          },
+        ),
+      ],
     );
   }
 }
@@ -285,15 +273,12 @@
 
   @override
   Widget build(BuildContext context) {
-    return IconButton(
-      key: key,
-      padding: EdgeInsets.zero,
+    return DevToolsButton.iconOnly(
+      icon: Icons.copy_outlined,
+      outlined: false,
       onPressed: () {
         unawaited(copyToClipboard(value, 'Copied to clipboard.'));
       },
-      iconSize: defaultIconSize,
-      splashRadius: defaultIconSize,
-      icon: const Icon(Icons.copy_outlined),
     );
   }
 }
@@ -306,13 +291,10 @@
 
   @override
   Widget build(BuildContext context) {
-    return IconButton(
-      key: key,
-      padding: EdgeInsets.zero,
+    return DevToolsButton.iconOnly(
+      icon: Icons.delete,
+      outlined: false,
       onPressed: onPressed,
-      iconSize: defaultIconSize,
-      splashRadius: defaultIconSize,
-      icon: const Icon(Icons.delete),
     );
   }
 }
diff --git a/packages/devtools_app/lib/src/shared/file_import.dart b/packages/devtools_app/lib/src/shared/file_import.dart
index 88dadf8..748d261 100644
--- a/packages/devtools_app/lib/src/shared/file_import.dart
+++ b/packages/devtools_app/lib/src/shared/file_import.dart
@@ -271,7 +271,17 @@
   final acceptedTypeGroups = [XTypeGroup(extensions: acceptedTypes)];
   final file = await openFile(acceptedTypeGroups: acceptedTypeGroups);
   if (file == null) return null;
+  return await _toDevToolsFile(file);
+}
 
+Future<List<XFile>> importRawFilesFromPicker({
+  List<String>? acceptedTypes,
+}) async {
+  final acceptedTypeGroups = [XTypeGroup(extensions: acceptedTypes)];
+  return await openFiles(acceptedTypeGroups: acceptedTypeGroups);
+}
+
+Future<DevToolsJsonFile> _toDevToolsFile(XFile file) async {
   final data = jsonDecode(await file.readAsString());
   final lastModifiedTime = await file.lastModified();
   // TODO(kenz): this will need to be modified if we need to support other file
@@ -337,7 +347,7 @@
   /// The title of the action button.
   final String actionText;
 
-  final Function(
+  final void Function(
     DevToolsJsonFile firstImportedFile,
     DevToolsJsonFile secondImportedFile,
     void Function(String error) onError,
diff --git a/packages/devtools_app/lib/src/shared/globals.dart b/packages/devtools_app/lib/src/shared/globals.dart
index 62c40c2..a1a5f5d 100644
--- a/packages/devtools_app/lib/src/shared/globals.dart
+++ b/packages/devtools_app/lib/src/shared/globals.dart
@@ -2,11 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:devtools_app_shared/service.dart';
 import 'package:devtools_app_shared/utils.dart';
+import 'package:flutter/foundation.dart';
 
 import '../extensions/extension_service.dart';
 import '../screens/debugger/breakpoint_manager.dart';
-import '../service/dtd_manager.dart';
 import '../service/service_manager.dart';
 import '../shared/banner_messages.dart';
 import '../shared/notifications.dart';
@@ -70,3 +71,19 @@
 void setIntegrationTestMode() {
   _integrationTestMode = true;
 }
+
+/// Whether DevTools is being run in a test environment.
+bool get testMode => _testMode;
+bool _testMode = false;
+void setTestMode() {
+  _testMode = true;
+}
+
+/// Whether DevTools is being run as a stager app.
+bool get stagerMode => _stagerMode;
+bool _stagerMode = false;
+void setStagerMode() {
+  if (!kReleaseMode) {
+    _stagerMode = true;
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/http/http_request_data.dart b/packages/devtools_app/lib/src/shared/http/http_request_data.dart
index e424bad..ed443fe 100644
--- a/packages/devtools_app/lib/src/shared/http/http_request_data.dart
+++ b/packages/devtools_app/lib/src/shared/http/http_request_data.dart
@@ -26,7 +26,7 @@
   String get name => _event.event;
 
   /// The time the instant event was recorded.
-  int get timestampMicros => _event.timestamp;
+  DateTime get timestamp => _event.timestamp;
 
   /// The amount of time since the last instant event completed.
   TimeRange? get timeRange => _timeRange;
@@ -38,10 +38,9 @@
 /// An abstraction of an HTTP request made through dart:io.
 class DartIOHttpRequestData extends NetworkRequest {
   DartIOHttpRequestData(
-    int timelineMicrosBase,
     this._request, {
     bool requestFullDataFromVmService = true,
-  }) : super(timelineMicrosBase) {
+  }) {
     if (requestFullDataFromVmService && _request.isResponseComplete) {
       unawaited(getFullRequestData());
     }
@@ -89,17 +88,14 @@
 
   bool get _hasError => _request.request?.hasError ?? false;
 
-  int? get _endTime =>
+  DateTime? get _endTime =>
       _hasError ? _request.endTime : _request.response?.endTime;
 
   @override
   Duration? get duration {
     if (inProgress || !isValid) return null;
     // Timestamps are in microseconds
-    final range = TimeRange()
-      ..start = Duration(microseconds: _request.startTime)
-      ..end = Duration(microseconds: _endTime!);
-    return range.duration;
+    return _endTime!.difference(_request.startTime);
   }
 
   /// Whether the request is safe to display in the UI.
@@ -189,10 +185,8 @@
   /// All instant events logged to the timeline for this HTTP request.
   List<DartIOHttpInstantEvent> get instantEvents {
     if (_instantEvents == null) {
-      _instantEvents = [
-        for (final event in _request.request?.events ?? [])
-          DartIOHttpInstantEvent._(event),
-      ];
+      _instantEvents =
+          _request.events.map((e) => DartIOHttpInstantEvent._(e)).toList();
       _recalculateInstantEventTimes();
     }
     return _instantEvents!;
@@ -245,19 +239,10 @@
   }
 
   @override
-  DateTime? get endTimestamp {
-    final endTime = _endTime;
-    return endTime == null
-        ? null
-        : DateTime.fromMicrosecondsSinceEpoch(
-            timelineMicrosecondsSinceEpoch(endTime),
-          );
-  }
+  DateTime? get endTimestamp => _endTime;
 
   @override
-  DateTime get startTimestamp => DateTime.fromMicrosecondsSinceEpoch(
-        timelineMicrosecondsSinceEpoch(_request.startTime),
-      );
+  DateTime get startTimestamp => _request.startTime;
 
   @override
   String? get status =>
@@ -310,12 +295,12 @@
   String? _requestBody;
 
   void _recalculateInstantEventTimes() {
-    int lastTime = _request.startTime;
+    DateTime lastTime = _request.startTime;
     for (final instant in instantEvents) {
-      final instantTime = instant.timestampMicros;
+      final instantTime = instant.timestamp;
       instant._timeRange = TimeRange()
-        ..start = Duration(microseconds: lastTime)
-        ..end = Duration(microseconds: instantTime);
+        ..start = Duration(microseconds: lastTime.microsecondsSinceEpoch)
+        ..end = Duration(microseconds: instantTime.microsecondsSinceEpoch);
       lastTime = instantTime;
     }
   }
diff --git a/packages/devtools_app/lib/src/shared/memory/adapted_heap_data.dart b/packages/devtools_app/lib/src/shared/memory/adapted_heap_data.dart
index 39280e7..acfbbdc 100644
--- a/packages/devtools_app/lib/src/shared/memory/adapted_heap_data.dart
+++ b/packages/devtools_app/lib/src/shared/memory/adapted_heap_data.dart
@@ -62,7 +62,7 @@
     HeapSnapshotGraph graph,
   ) async {
     final objects = <AdaptedHeapObject>[];
-    for (final i in Iterable.generate(graph.objects.length)) {
+    for (final i in Iterable<int>.generate(graph.objects.length)) {
       if (_uiReleaser.step()) await _uiReleaser.releaseUi();
       final object =
           AdaptedHeapObject.fromHeapSnapshotObject(graph.objects[i], i);
@@ -72,6 +72,14 @@
     return AdaptedHeapData(objects);
   }
 
+  static Future<AdaptedHeapData> fromBytes(
+    Uint8List bytes,
+  ) {
+    final data = bytes.buffer.asByteData();
+    final graph = HeapSnapshotGraph.fromChunks([data]);
+    return fromHeapSnapshot(graph);
+  }
+
   /// Default value for rootIndex is taken from the doc:
   /// https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/heap_snapshot.md#object-ids
   static const int _defaultRootIndex = 1;
@@ -93,9 +101,9 @@
 
   String snapshotName = '';
 
-  /// Heap objects by identityHashCode.
-  late final _objectsByCode = <IdentityHashCode, int>{
-    for (var i in Iterable.generate(objects.length)) objects[i].code: i,
+  /// Heap objects by `identityHashCode`.
+  late final _objectsByCode = <IdentityHashCode, int?>{
+    for (var i in Iterable<int>.generate(objects.length)) objects[i].code: i,
   };
 
   int? objectIndexByIdentityHashCode(IdentityHashCode code) =>
diff --git a/packages/devtools_app/lib/src/shared/notifications.dart b/packages/devtools_app/lib/src/shared/notifications.dart
index 35e9bb4..606276f 100644
--- a/packages/devtools_app/lib/src/shared/notifications.dart
+++ b/packages/devtools_app/lib/src/shared/notifications.dart
@@ -50,7 +50,7 @@
   /// [NotificationMessage.defaultDuration].
   bool push(
     String message, {
-    isDismissible = false,
+    bool isDismissible = false,
   }) =>
       pushNotification(
         NotificationMessage(
@@ -68,8 +68,8 @@
   bool pushError(
     String errorMessage, {
     String? stackTrace,
-    isDismissible = true,
-    isReportable = true,
+    bool isDismissible = true,
+    bool isReportable = true,
   }) {
     final reportErrorAction = NotificationAction(
       'Report error',
diff --git a/packages/devtools_app/lib/src/shared/preferences.dart b/packages/devtools_app/lib/src/shared/preferences.dart
index 7ecd303..7cf3425 100644
--- a/packages/devtools_app/lib/src/shared/preferences.dart
+++ b/packages/devtools_app/lib/src/shared/preferences.dart
@@ -32,8 +32,6 @@
       ValueNotifier<bool>(Logger.root.level == verboseLoggingLevel);
   static const _verboseLoggingStorageId = 'verboseLogging';
 
-  final denseModeEnabled = ValueNotifier<bool>(false);
-
   InspectorPreferencesController get inspector => _inspector;
   final _inspector = InspectorPreferencesController();
 
@@ -63,12 +61,6 @@
       storage.setValue('ui.vmDeveloperMode', '${vmDeveloperModeEnabled.value}');
     });
 
-    value = await storage.getValue('ui.denseMode');
-    toggleDenseMode(value == 'true');
-    addAutoDisposeListener(denseModeEnabled, () {
-      storage.setValue('ui.denseMode', '${denseModeEnabled.value}');
-    });
-
     await _initVerboseLogging();
 
     await inspector.init();
@@ -128,13 +120,6 @@
       verboseLoggingEnabled.value = enableVerboseLogging;
     }
   }
-
-  /// Change the value for the dense mode setting.
-  void toggleDenseMode(bool? enableDenseMode) {
-    if (enableDenseMode != null) {
-      denseModeEnabled.value = enableDenseMode;
-    }
-  }
 }
 
 class InspectorPreferencesController extends DisposableController
@@ -223,7 +208,7 @@
             // the directories
             unawaited(preferences.inspector.loadPubRootDirectories());
           } else {
-            late Function() pausedListener;
+            late void Function() pausedListener;
 
             pausedListener = () {
               if (debuggerState?.isPaused.value == false) {
diff --git a/packages/devtools_app/lib/src/shared/primitives/extent_delegate_list.dart b/packages/devtools_app/lib/src/shared/primitives/extent_delegate_list.dart
index 1592225..0099679 100644
--- a/packages/devtools_app/lib/src/shared/primitives/extent_delegate_list.dart
+++ b/packages/devtools_app/lib/src/shared/primitives/extent_delegate_list.dart
@@ -173,7 +173,7 @@
     required this.childrenDelegate,
     required this.extentDelegate,
     int? semanticChildCount,
-    Function(PointerSignalEvent event)? customPointerSignalHandler,
+    void Function(PointerSignalEvent event)? customPointerSignalHandler,
   }) : super(
           key: key,
           scrollDirection: scrollDirection,
diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart
index b5a04e5..ced88cb 100644
--- a/packages/devtools_app/lib/src/shared/primitives/utils.dart
+++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart
@@ -23,7 +23,7 @@
 
 final _log = Logger('utils');
 
-bool collectionEquals(e1, e2, {bool ordered = true}) {
+bool collectionEquals(Object? e1, Object? e2, {bool ordered = true}) {
   if (ordered) {
     return const DeepCollectionEquality().equals(e1, e2);
   }
@@ -1061,9 +1061,21 @@
     return true;
   }
 
-  /// Whether [other] is a case insensitive match for this String
-  bool caseInsensitiveEquals(String? other) {
-    return toLowerCase() == other?.toLowerCase();
+  /// Whether [other] is a case insensitive match for this String.
+  ///
+  /// If [pattern] is a [RegExp], this method will return true if and only if
+  /// this String is a complete [RegExp] match, meaning that the regular
+  /// expression finds a match with starting index 0 and ending index
+  /// [this.length].
+  bool caseInsensitiveEquals(Pattern? pattern) {
+    if (pattern is RegExp) {
+      assert(!pattern.isCaseSensitive);
+      final completeMatch = pattern
+          .allMatches(this)
+          .firstWhereOrNull((match) => match.start == 0 && match.end == length);
+      return completeMatch != null;
+    }
+    return toLowerCase() == pattern.toString().toLowerCase();
   }
 
   /// Find all case insensitive matches of query in this String
@@ -1112,6 +1124,16 @@
   T get second => this[1];
 
   T get third => this[2];
+
+  List<int> allIndicesWhere(bool Function(T element) test) {
+    final indices = <int>[];
+    for (var i = 0; i < length; i++) {
+      if (test(this[i])) {
+        indices.add(i);
+      }
+    }
+    return indices;
+  }
 }
 
 extension SetExtension<T> on Set<T> {
diff --git a/packages/devtools_app/lib/src/shared/profiler_utils.dart b/packages/devtools_app/lib/src/shared/profiler_utils.dart
index 6f858ce..a3cf593 100644
--- a/packages/devtools_app/lib/src/shared/profiler_utils.dart
+++ b/packages/devtools_app/lib/src/shared/profiler_utils.dart
@@ -268,7 +268,7 @@
 
   @override
   Widget build(BuildContext context) {
-    final fontStyle = Theme.of(context).fixedFontStyle;
+    final fontStyle = Theme.of(context).regularTextStyle;
     final sourceTextSpans = <TextSpan>[];
     final packageUriWithSourceLine = uriWithSourceLine(packageUri, sourceLine);
 
@@ -315,12 +315,12 @@
       ),
     );
     if (displayInRow) {
+      // Include this [Row] so that the clickable [VmServiceObjectLink]
+      // does not extend all the way to the end of the row.
       return Row(
+        mainAxisSize: MainAxisSize.min,
         children: [
-          richText,
-          // Include this [Spacer] so that the clickable [VmServiceObjectLink]
-          // does not extend all the way to the end of the row.
-          const Spacer(),
+          Flexible(child: richText),
         ],
       );
     }
diff --git a/packages/devtools_app/lib/src/shared/server/_dtd_api.dart b/packages/devtools_app/lib/src/shared/server/_dtd_api.dart
new file mode 100644
index 0000000..f3bdc2c
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/server/_dtd_api.dart
@@ -0,0 +1,21 @@
+// Copyright 2024 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.
+
+part of 'server.dart';
+
+/// Asks the Devtools Server to return a Dart Tooling Daemon uri if it has one.
+Future<Uri?> getDtdUri() async {
+  if (isDevToolsServerAvailable) {
+    final uri = Uri(path: DtdApi.apiGetDtdUri);
+    final resp = await request(uri.toString());
+    if (resp?.statusOk ?? false) {
+      final parsedResult = json.decode(resp!.body) as Map;
+
+      final uriString = parsedResult[DtdApi.uriPropertyName];
+
+      return Uri.parse(uriString);
+    }
+  }
+  return null;
+}
diff --git a/packages/devtools_app/lib/src/shared/server/_extensions_api.dart b/packages/devtools_app/lib/src/shared/server/_extensions_api.dart
index 60b5ade..95c24c5 100644
--- a/packages/devtools_app/lib/src/shared/server/_extensions_api.dart
+++ b/packages/devtools_app/lib/src/shared/server/_extensions_api.dart
@@ -8,20 +8,22 @@
 /// serve their assets on the server, and return the list of available
 /// extensions here.
 Future<List<DevToolsExtensionConfig>> refreshAvailableExtensions(
-  String rootPath,
+  Uri appRoot,
 ) async {
   if (isDevToolsServerAvailable) {
     final uri = Uri(
       path: ExtensionsApi.apiServeAvailableExtensions,
-      queryParameters: {ExtensionsApi.extensionRootPathPropertyName: rootPath},
+      queryParameters: {
+        ExtensionsApi.extensionRootPathPropertyName: appRoot.toString(),
+      },
     );
     final resp = await request(uri.toString());
     if (resp?.statusOk ?? false) {
-      final parsedResult = json.decode(resp!.body);
+      final parsedResult = json.decode(resp!.body) as Map;
       final extensionsAsJson =
           (parsedResult[ExtensionsApi.extensionsResultPropertyName]!
                   as List<Object?>)
-              .whereNotNull()
+              .nonNulls
               .cast<Map<String, Object?>>();
 
       final warningMessage =
@@ -38,7 +40,7 @@
       return [];
     }
   } else if (debugDevToolsExtensions) {
-    return debugHandleRefreshAvailableExtensions(rootPath);
+    return debugHandleRefreshAvailableExtensions(appRoot);
   }
   return [];
 }
@@ -51,7 +53,7 @@
 /// to the value set forth by [enable] and then return the value that is saved
 /// to disk.
 Future<ExtensionEnabledState> extensionEnabledState({
-  required String rootPath,
+  required Uri appRoot,
   required String extensionName,
   bool? enable,
 }) async {
@@ -59,7 +61,7 @@
     final uri = Uri(
       path: ExtensionsApi.apiExtensionEnabledState,
       queryParameters: {
-        ExtensionsApi.extensionRootPathPropertyName: rootPath,
+        ExtensionsApi.extensionRootPathPropertyName: appRoot.toString(),
         ExtensionsApi.extensionNamePropertyName: extensionName,
         if (enable != null)
           ExtensionsApi.enabledStatePropertyName: enable.toString(),
@@ -74,7 +76,7 @@
     }
   } else if (debugDevToolsExtensions) {
     return debugHandleExtensionEnabledState(
-      rootPath: rootPath,
+      appRoot: appRoot,
       extensionName: extensionName,
       enable: enable,
     );
diff --git a/packages/devtools_app/lib/src/shared/server/_release_notes_api.dart b/packages/devtools_app/lib/src/shared/server/_release_notes_api.dart
index edd7793..9003edd 100644
--- a/packages/devtools_app/lib/src/shared/server/_release_notes_api.dart
+++ b/packages/devtools_app/lib/src/shared/server/_release_notes_api.dart
@@ -29,7 +29,7 @@
       '$apiSetLastReleaseNotesVersion'
       '?$lastReleaseNotesVersionPropertyName=$version',
     );
-    if (resp == null || !resp.statusOk || !json.decode(resp.body)) {
+    if (resp == null || !resp.statusOk || !(json.decode(resp.body) as bool)) {
       logWarning(resp, apiSetLastReleaseNotesVersion, resp?.body);
     }
   }
diff --git a/packages/devtools_app/lib/src/shared/server/_survey_api.dart b/packages/devtools_app/lib/src/shared/server/_survey_api.dart
index a944348..326121c 100644
--- a/packages/devtools_app/lib/src/shared/server/_survey_api.dart
+++ b/packages/devtools_app/lib/src/shared/server/_survey_api.dart
@@ -58,7 +58,7 @@
       '$apiSetSurveyActionTaken'
       '?$surveyActionTakenPropertyName=true',
     );
-    if (resp == null || !resp.statusOk || !json.decode(resp.body)) {
+    if (resp == null || !resp.statusOk || !(json.decode(resp.body) as bool)) {
       logWarning(resp, apiSetSurveyActionTaken, resp?.body);
     }
   }
diff --git a/packages/devtools_app/lib/src/shared/server/server.dart b/packages/devtools_app/lib/src/shared/server/server.dart
index eac093c..1182109 100644
--- a/packages/devtools_app/lib/src/shared/server/server.dart
+++ b/packages/devtools_app/lib/src/shared/server/server.dart
@@ -5,7 +5,6 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'package:collection/collection.dart';
 import 'package:devtools_shared/devtools_deeplink.dart';
 import 'package:devtools_shared/devtools_extensions.dart';
 import 'package:devtools_shared/devtools_shared.dart';
@@ -22,6 +21,7 @@
 part '_extensions_api.dart';
 part '_release_notes_api.dart';
 part '_survey_api.dart';
+part '_dtd_api.dart';
 
 final _log = Logger('_server_web');
 
@@ -78,7 +78,7 @@
   Response resp,
   String filePath,
 ) {
-  final data = json.decode(resp.body);
+  final data = json.decode(resp.body) as Map;
   final lastModified = data['lastModifiedTime'];
   final lastModifiedTime =
       lastModified != null ? DateTime.parse(lastModified) : DateTime.now();
diff --git a/packages/devtools_app/lib/src/shared/table/_flat_table.dart b/packages/devtools_app/lib/src/shared/table/_flat_table.dart
new file mode 100644
index 0000000..64c4b83
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/table/_flat_table.dart
@@ -0,0 +1,360 @@
+// 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.
+
+part of 'table.dart';
+
+/// A [FlatTable] widget that is searchable.
+///
+/// The table requires a [searchController], which is responsible for feeding
+/// information about search matches and the active search match to the table.
+///
+/// This table will automatically refresh search matches on the
+/// [searchController] after sort operations that are triggered from the table.
+class SearchableFlatTable<T extends SearchableDataMixin> extends FlatTable<T> {
+  SearchableFlatTable({
+    super.key,
+    required SearchControllerMixin<T> searchController,
+    required super.keyFactory,
+    required super.data,
+    required super.dataKey,
+    required super.columns,
+    required super.defaultSortColumn,
+    required super.defaultSortDirection,
+    super.secondarySortColumn,
+    super.sortOriginalData = false,
+    super.pinBehavior = FlatTablePinBehavior.none,
+    super.columnGroups,
+    super.autoScrollContent = false,
+    super.onItemSelected,
+    super.preserveVerticalScrollPosition = false,
+    super.includeColumnGroupHeaders = true,
+    super.sizeColumnsToFit = true,
+    super.selectionNotifier,
+  }) : super(
+          searchMatchesNotifier: searchController.searchMatches,
+          activeSearchMatchNotifier: searchController.activeSearchMatch,
+          onDataSorted: () => WidgetsBinding.instance.addPostFrameCallback((_) {
+            // This needs to be in a post frame callback so that the search
+            // matches are not updated in the middle of a table build.
+            searchController.refreshSearchMatches();
+          }),
+        );
+}
+
+/// A table that displays in a collection of [data], based on a collection of
+/// [ColumnData].
+///
+/// The [ColumnData] gives this table information about how to size its columns,
+/// and how to present each row of `data`.
+class FlatTable<T> extends StatefulWidget {
+  FlatTable({
+    Key? key,
+    required this.keyFactory,
+    required this.data,
+    required this.dataKey,
+    required this.columns,
+    this.columnGroups,
+    this.autoScrollContent = false,
+    this.onItemSelected,
+    required this.defaultSortColumn,
+    required this.defaultSortDirection,
+    this.onDataSorted,
+    this.sortOriginalData = false,
+    this.pinBehavior = FlatTablePinBehavior.none,
+    this.secondarySortColumn,
+    this.searchMatchesNotifier,
+    this.activeSearchMatchNotifier,
+    this.preserveVerticalScrollPosition = false,
+    this.includeColumnGroupHeaders = true,
+    this.tallHeaders = false,
+    this.sizeColumnsToFit = true,
+    this.headerColor,
+    this.fillWithEmptyRows = false,
+    this.enableHoverHandling = false,
+    ValueNotifier<T?>? selectionNotifier,
+  })  : selectionNotifier = selectionNotifier ?? ValueNotifier<T?>(null),
+        super(key: key);
+
+  /// List of columns to display.
+  ///
+  /// These [ColumnData] elements should be defined as static
+  /// OR if they cannot be defined as static,
+  /// they should not manage stateful data.
+  ///
+  /// [FlatTableState.didUpdateWidget] checks if the columns have
+  /// changed before re-initializing the table controller,
+  /// and the columns are compared by title only.
+  /// See also [FlatTableState. _tableConfigurationChanged].
+  final List<ColumnData<T>> columns;
+
+  final List<ColumnGroup>? columnGroups;
+
+  /// Whether the columns for this table should be sized so that the entire
+  /// table fits in view (e.g. so that there is no horizontal scrolling).
+  final bool sizeColumnsToFit;
+
+  // TODO(kenz): should we enable this behavior by default? Does it ever matter
+  // to preserve the order of the original data passed to a flat table?
+  /// Whether table sorting should sort the original data list instead of
+  /// creating a copy.
+  final bool sortOriginalData;
+
+  /// Determines if the headers for column groups should be rendered.
+  ///
+  /// If set to false and `columnGroups` is non-null and non-empty, only
+  /// the vertical dividing lines will be drawn for each column group boundary.
+  final bool includeColumnGroupHeaders;
+
+  /// Whether the table headers should be slightly taller than the table rows to
+  /// support multiline text.
+  final bool tallHeaders;
+
+  /// The background color of the header.
+  ///
+  /// If null, defaults to `Theme.of(context).canvasColor`.
+  final Color? headerColor;
+
+  /// Whether to fill the table with empty rows.
+  final bool fillWithEmptyRows;
+
+  /// Whether to enable hover handling.
+  final bool enableHoverHandling;
+
+  /// Data set to show as rows in this table.
+  final List<T> data;
+
+  /// Unique key for the data shown in this table.
+  ///
+  /// This key will be used to restore things like sort column, sort direction,
+  /// and scroll position for this table (when [preserveVerticalScrollPosition]
+  /// is true).
+  ///
+  /// We use [TableUiStateStore] to store [_TableUiState] by this key so that
+  /// we can save and restore this state without having to keep [State] or table
+  /// controller objects alive.
+  final String dataKey;
+
+  /// Auto-scrolling the table to keep new content visible.
+  final bool autoScrollContent;
+
+  /// Factory that creates keys for each row in this table.
+  final Key Function(T data) keyFactory;
+
+  /// Callback that, when non-null, will be called on each table row selection.
+  final ItemSelectedCallback<T?>? onItemSelected;
+
+  /// Determines how elements that request to be pinned are displayed.
+  ///
+  /// Defaults to [FlatTablePinBehavior.none], which disables pinning.
+  final FlatTablePinBehavior pinBehavior;
+
+  /// The default sort column for this table.
+  ///
+  /// This sort column is passed along to [TreeTableState.tableController],
+  /// which uses [defaultSortColumn] for the starting value in
+  /// [TableControllerBase.tableUiState].
+  final ColumnData<T> defaultSortColumn;
+
+  /// The default [SortDirection] for this table.
+  ///
+  /// This [SortDirection] is passed along to [TreeTableState.tableController],
+  /// which uses [defaultSortDirection] for the starting value in
+  /// [TableControllerBase.tableUiState].
+  final SortDirection defaultSortDirection;
+
+  /// The secondary sort column to be used in the sorting algorithm provided by
+  /// [TableControllerBase.sortDataAndNotify].
+  final ColumnData<T>? secondarySortColumn;
+
+  /// Callback that will be called after each table sort operation.
+  final VoidCallback? onDataSorted;
+
+  /// Notifies with the list of data items that should be marked as search
+  /// matches.
+  final ValueListenable<List<T>>? searchMatchesNotifier;
+
+  /// Notifies with the data item that should be marked as the active search
+  /// match.
+  final ValueListenable<T?>? activeSearchMatchNotifier;
+
+  /// Stores the selected data item (the selected row) for this table.
+  ///
+  /// This notifier's value will be updated when a row of the table is selected.
+  final ValueNotifier<T?> selectionNotifier;
+
+  /// Whether the vertical scroll position for this table should be preserved for
+  /// each data set.
+  ///
+  /// This should be set to true if the table is not disposed and completely
+  /// rebuilt when changing from one data set to another.
+  final bool preserveVerticalScrollPosition;
+
+  @override
+  FlatTableState<T> createState() => FlatTableState<T>();
+}
+
+@visibleForTesting
+class FlatTableState<T> extends State<FlatTable<T>> with AutoDisposeMixin {
+  FlatTableController<T> get tableController => _tableController!;
+  FlatTableController<T>? _tableController;
+
+  @override
+  void initState() {
+    super.initState();
+    _setUpTableController();
+    addAutoDisposeListener(tableController.tableData);
+
+    if (tableController.pinBehavior != FlatTablePinBehavior.none &&
+        this is! State<FlatTable<PinnableListEntry>>) {
+      throw StateError('$T must implement PinnableListEntry');
+    }
+  }
+
+  @override
+  void didUpdateWidget(FlatTable<T> oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (_tableConfigurationChanged(oldWidget, widget)) {
+      _setUpTableController();
+    } else if (!collectionEquals(oldWidget.data, widget.data)) {
+      _setUpTableController(reset: false);
+    }
+  }
+
+  @override
+  void dispose() {
+    _tableController = null;
+    super.dispose();
+  }
+
+  /// Sets up the [tableController] for the property values in [widget].
+  ///
+  /// [reset] determines whether or not we should re-initialize
+  /// [_tableController], which should only happen when the core table
+  /// configuration (columns & column groups) has changed.
+  ///
+  /// See [_tableConfigurationChanged].
+  void _setUpTableController({bool reset = true}) {
+    final shouldResetController = reset || _tableController == null;
+    if (shouldResetController) {
+      _tableController = FlatTableController<T>(
+        columns: widget.columns,
+        defaultSortColumn: widget.defaultSortColumn,
+        defaultSortDirection: widget.defaultSortDirection,
+        secondarySortColumn: widget.secondarySortColumn,
+        columnGroups: widget.columnGroups,
+        includeColumnGroupHeaders: widget.includeColumnGroupHeaders,
+        pinBehavior: widget.pinBehavior,
+        sizeColumnsToFit: widget.sizeColumnsToFit,
+        sortOriginalData: widget.sortOriginalData,
+        onDataSorted: widget.onDataSorted,
+      );
+    }
+
+    if (widget.preserveVerticalScrollPosition) {
+      // Order matters - this must be called before [tableController.setData]
+      tableController.storeScrollPosition();
+    }
+
+    tableController
+      ..setData(widget.data, widget.dataKey)
+      ..pinBehavior = widget.pinBehavior;
+  }
+
+  /// Whether the core table configuration has changed, determined by checking
+  /// the equality of columns and column groups.
+  bool _tableConfigurationChanged(
+    FlatTable<T> oldWidget,
+    FlatTable<T> newWidget,
+  ) {
+    final columnsChanged = !collectionEquals(
+          oldWidget.columns.map((c) => c.title),
+          newWidget.columns.map((c) => c.title),
+        ) ||
+        !collectionEquals(
+          oldWidget.columnGroups?.map((c) => c.title),
+          newWidget.columnGroups?.map((c) => c.title),
+        );
+    return columnsChanged;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Widget buildTable(List<double> columnWidths) => DevToolsTable<T>(
+          tableController: tableController,
+          columnWidths: columnWidths,
+          autoScrollContent: widget.autoScrollContent,
+          rowBuilder: _buildRow,
+          activeSearchMatchNotifier: widget.activeSearchMatchNotifier,
+          rowItemExtent: defaultRowHeight,
+          preserveVerticalScrollPosition: widget.preserveVerticalScrollPosition,
+          tallHeaders: widget.tallHeaders,
+          headerColor: widget.headerColor,
+          fillWithEmptyRows: widget.fillWithEmptyRows,
+          enableHoverHandling: widget.enableHoverHandling,
+        );
+    if (widget.sizeColumnsToFit || tableController.columnWidths == null) {
+      return LayoutBuilder(
+        builder: (context, constraints) => buildTable(
+          tableController.computeColumnWidthsSizeToFit(
+            constraints.maxWidth,
+          ),
+        ),
+      );
+    }
+    return buildTable(tableController.columnWidths!);
+  }
+
+  Widget _buildRow({
+    required BuildContext context,
+    required LinkedScrollControllerGroup linkedScrollControllerGroup,
+    required int index,
+    required List<double> columnWidths,
+    required bool isPinned,
+    required bool enableHoverHandling,
+  }) {
+    final pinnedData = tableController.pinnedData;
+    final data = isPinned ? pinnedData : tableController.tableData.value.data;
+    if (index >= data.length) {
+      return TableRow<T>.filler(
+        linkedScrollControllerGroup: linkedScrollControllerGroup,
+        columns: tableController.columns,
+        columnGroups: tableController.columnGroups,
+        columnWidths: columnWidths,
+        backgroundColor: alternatingColorForIndex(
+          index,
+          Theme.of(context).colorScheme,
+        ),
+      );
+    }
+
+    final node = data[index];
+    return ValueListenableBuilder<T?>(
+      valueListenable: widget.selectionNotifier,
+      builder: (context, selected, _) {
+        return TableRow<T>(
+          key: widget.keyFactory(node),
+          linkedScrollControllerGroup: linkedScrollControllerGroup,
+          node: node,
+          onPressed: (T? selection) {
+            widget.selectionNotifier.value = selection;
+            if (widget.onItemSelected != null && selection != null) {
+              widget.onItemSelected!(selection);
+            }
+          },
+          columns: tableController.columns,
+          columnGroups: tableController.columnGroups,
+          columnWidths: columnWidths,
+          backgroundColor: alternatingColorForIndex(
+            index,
+            Theme.of(context).colorScheme,
+          ),
+          isSelected: node != null && node == selected,
+          searchMatchesNotifier: widget.searchMatchesNotifier,
+          activeSearchMatchNotifier: widget.activeSearchMatchNotifier,
+          enableHoverHandling: enableHoverHandling,
+        );
+      },
+    );
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/table/_table_column.dart b/packages/devtools_app/lib/src/shared/table/_table_column.dart
new file mode 100644
index 0000000..6d36613
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/table/_table_column.dart
@@ -0,0 +1,206 @@
+// 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.
+
+part of 'table.dart';
+
+/// If a [ColumnData] implements this interface, it can override how that cell
+/// is rendered.
+abstract class ColumnRenderer<T> {
+  /// Render the given [data] to a [Widget].
+  ///
+  /// This method can return `null` to indicate that the default rendering
+  /// should be used instead.
+  /// `isRowHovered` is only used when `enableHoverHandling` is `true` on the table
+  /// that this column belongs to.
+  Widget? build(
+    BuildContext context,
+    T data, {
+    bool isRowSelected = false,
+    bool isRowHovered = false,
+    VoidCallback? onPressed,
+  });
+}
+
+/// If a [ColumnData] implements this interface, it can override how that column
+/// header is rendered.
+abstract class ColumnHeaderRenderer<T> {
+  /// Render the column header to a [Widget].
+  ///
+  /// This method can return `null` to indicate that the default rendering
+  /// should be used instead.
+  Widget? buildHeader(
+    BuildContext context,
+    Widget Function() defaultHeaderRenderer,
+  );
+}
+
+class _ColumnHeader<T> extends StatelessWidget {
+  const _ColumnHeader({
+    Key? key,
+    required this.column,
+    required this.isSortColumn,
+    required this.sortDirection,
+    required this.onSortChanged,
+    this.secondarySortColumn,
+  }) : super(key: key);
+
+  final ColumnData<T> column;
+
+  final ColumnData<T>? secondarySortColumn;
+
+  final bool isSortColumn;
+
+  final SortDirection sortDirection;
+
+  final void Function(
+    ColumnData<T> column,
+    SortDirection direction, {
+    ColumnData<T>? secondarySortColumn,
+  })? onSortChanged;
+
+  @override
+  Widget build(BuildContext context) {
+    late Widget content;
+    final title = Text(
+      column.title,
+      overflow: TextOverflow.ellipsis,
+      textAlign: column.headerAlignment,
+    );
+
+    final headerContent = Row(
+      mainAxisAlignment: column.mainAxisAlignment,
+      children: [
+        if (isSortColumn && column.supportsSorting) ...[
+          Icon(
+            sortDirection == SortDirection.ascending
+                ? Icons.expand_less
+                : Icons.expand_more,
+            size: defaultIconSize,
+          ),
+          const SizedBox(width: densePadding),
+        ],
+        Expanded(
+          child: column.titleTooltip != null
+              ? DevToolsTooltip(
+                  message: column.titleTooltip,
+                  padding: const EdgeInsets.all(denseSpacing),
+                  child: title,
+                )
+              : title,
+        ),
+      ],
+    );
+
+    content = column.includeHeader
+        ? InkWell(
+            canRequestFocus: false,
+            onTap: column.supportsSorting
+                ? () => _handleSortChange(
+                      column,
+                      secondarySortColumn: secondarySortColumn,
+                    )
+                : null,
+            child: headerContent,
+          )
+        : headerContent;
+    return content;
+  }
+
+  void _handleSortChange(
+    ColumnData<T> columnData, {
+    ColumnData<T>? secondarySortColumn,
+  }) {
+    SortDirection direction;
+    if (isSortColumn) {
+      direction = sortDirection.reverse();
+    } else if (columnData.numeric) {
+      direction = SortDirection.descending;
+    } else {
+      direction = SortDirection.ascending;
+    }
+    onSortChanged?.call(
+      columnData,
+      direction,
+      secondarySortColumn: secondarySortColumn,
+    );
+  }
+}
+
+class _ColumnGroupHeaderRow extends StatelessWidget {
+  const _ColumnGroupHeaderRow({
+    required this.groups,
+    required this.columnWidths,
+    required this.scrollController,
+    Key? key,
+  }) : super(key: key);
+
+  final List<ColumnGroup> groups;
+
+  final List<double> columnWidths;
+
+  final ScrollController scrollController;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
+      decoration: BoxDecoration(
+        border: Border(
+          bottom: defaultBorderSide(Theme.of(context)),
+        ),
+      ),
+      child: ListView.builder(
+        scrollDirection: Axis.horizontal,
+        controller: scrollController,
+        itemCount: groups.length + groups.numSpacers,
+        itemBuilder: (context, int i) {
+          if (i % 2 == 1) {
+            return const _ColumnGroupSpacer();
+          }
+
+          final group = groups[i ~/ 2];
+          final groupRange = group.range;
+          double groupWidth = 0.0;
+          for (int j = groupRange.begin as int; j < groupRange.end; j++) {
+            final columnWidth = columnWidths[j];
+            groupWidth += columnWidth;
+            if (j < groupRange.end - 1) {
+              groupWidth += columnSpacing;
+            }
+          }
+          return Container(
+            alignment: Alignment.center,
+            width: groupWidth,
+            child: group.title,
+          );
+        },
+      ),
+    );
+  }
+}
+
+class _ColumnGroupSpacer extends StatelessWidget {
+  const _ColumnGroupSpacer({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: const EdgeInsets.symmetric(
+        horizontal: (columnGroupSpacingWithPadding - columnGroupSpacing) / 2,
+      ),
+      child: Container(
+        width: columnGroupSpacing,
+        decoration: BoxDecoration(
+          gradient: LinearGradient(
+            colors: [
+              Colors.black,
+              Theme.of(context).focusColor,
+              Colors.black,
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/table/_table_row.dart b/packages/devtools_app/lib/src/shared/table/_table_row.dart
new file mode 100644
index 0000000..bd23857
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/table/_table_row.dart
@@ -0,0 +1,583 @@
+// 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.
+
+part of 'table.dart';
+
+enum _TableRowType {
+  data,
+  columnHeader,
+  columnGroupHeader,
+  filler,
+}
+
+enum _TableRowPartDisplayType {
+  column,
+  columnSpacer,
+  columnGroupSpacer,
+}
+
+/// Presents a [node] as a row in a table.
+///
+/// When the given [node] is null, this widget will instead present
+/// column headings.
+@visibleForTesting
+class TableRow<T> extends StatefulWidget {
+  /// Constructs a [TableRow] that presents the column values for
+  /// [node].
+  const TableRow({
+    Key? key,
+    required this.linkedScrollControllerGroup,
+    required this.node,
+    required this.columns,
+    required this.columnWidths,
+    required this.onPressed,
+    this.columnGroups,
+    this.backgroundColor,
+    this.expandableColumn,
+    this.isExpanded = false,
+    this.isExpandable = false,
+    this.isSelected = false,
+    this.isShown = true,
+    this.enableHoverHandling = false,
+    this.displayTreeGuidelines = false,
+    this.searchMatchesNotifier,
+    this.activeSearchMatchNotifier,
+  })  : sortColumn = null,
+        sortDirection = null,
+        secondarySortColumn = null,
+        onSortChanged = null,
+        _rowType = _TableRowType.data,
+        tall = false,
+        super(key: key);
+
+  /// Constructs a [TableRow] that is empty.
+  const TableRow.filler({
+    Key? key,
+    required this.linkedScrollControllerGroup,
+    required this.columns,
+    required this.columnWidths,
+    this.columnGroups,
+    this.backgroundColor,
+  })  : node = null,
+        isExpanded = false,
+        isExpandable = false,
+        isSelected = false,
+        onPressed = null,
+        expandableColumn = null,
+        isShown = true,
+        sortColumn = null,
+        sortDirection = null,
+        secondarySortColumn = null,
+        onSortChanged = null,
+        searchMatchesNotifier = null,
+        activeSearchMatchNotifier = null,
+        tall = false,
+        enableHoverHandling = false,
+        displayTreeGuidelines = false,
+        _rowType = _TableRowType.filler,
+        super(key: key);
+
+  /// Constructs a [TableRow] that presents the column titles instead
+  /// of any [node].
+  const TableRow.tableColumnHeader({
+    Key? key,
+    required this.linkedScrollControllerGroup,
+    required this.columns,
+    required this.columnWidths,
+    required this.columnGroups,
+    required this.sortColumn,
+    required this.sortDirection,
+    required this.onSortChanged,
+    this.secondarySortColumn,
+    this.onPressed,
+    this.tall = false,
+    this.backgroundColor,
+  })  : node = null,
+        isExpanded = false,
+        isExpandable = false,
+        isSelected = false,
+        expandableColumn = null,
+        isShown = true,
+        searchMatchesNotifier = null,
+        activeSearchMatchNotifier = null,
+        displayTreeGuidelines = false,
+        enableHoverHandling = false,
+        _rowType = _TableRowType.columnHeader,
+        super(key: key);
+
+  /// Constructs a [TableRow] that presents column group titles instead of any
+  /// [node].
+  const TableRow.tableColumnGroupHeader({
+    Key? key,
+    required this.linkedScrollControllerGroup,
+    required this.columnGroups,
+    required this.columnWidths,
+    required this.sortColumn,
+    required this.sortDirection,
+    required this.onSortChanged,
+    this.secondarySortColumn,
+    this.onPressed,
+    this.tall = false,
+    this.backgroundColor,
+  })  : node = null,
+        isExpanded = false,
+        isExpandable = false,
+        isSelected = false,
+        expandableColumn = null,
+        columns = const [],
+        isShown = true,
+        searchMatchesNotifier = null,
+        activeSearchMatchNotifier = null,
+        displayTreeGuidelines = false,
+        enableHoverHandling = false,
+        _rowType = _TableRowType.columnGroupHeader,
+        super(key: key);
+
+  final LinkedScrollControllerGroup linkedScrollControllerGroup;
+
+  final T? node;
+
+  final List<ColumnData<T>> columns;
+
+  final List<ColumnGroup>? columnGroups;
+
+  final ItemSelectedCallback<T>? onPressed;
+
+  final List<double> columnWidths;
+
+  final bool isSelected;
+
+  final _TableRowType _rowType;
+
+  final bool tall;
+
+  final bool enableHoverHandling;
+
+  /// Which column, if any, should show expansion affordances
+  /// and nested rows.
+  final ColumnData<T>? expandableColumn;
+
+  /// Whether or not this row is expanded.
+  ///
+  /// This dictates the orientation of the expansion arrow
+  /// that is drawn in the [expandableColumn].
+  ///
+  /// Only meaningful if [isExpanded] is true.
+  final bool isExpanded;
+
+  /// Whether or not this row can be expanded.
+  ///
+  /// This dictates whether an expansion arrow is
+  /// drawn in the [expandableColumn].
+  final bool isExpandable;
+
+  /// Whether or not this row is shown.
+  ///
+  /// When the value is toggled, this row will appear or disappear.
+  final bool isShown;
+
+  /// The background color of the row.
+  ///
+  /// If null, defaults to `Theme.of(context).canvasColor`.
+  final Color? backgroundColor;
+
+  final ColumnData<T>? sortColumn;
+
+  final SortDirection? sortDirection;
+
+  final ColumnData<T>? secondarySortColumn;
+
+  final Function(
+    ColumnData<T> column,
+    SortDirection direction, {
+    ColumnData<T>? secondarySortColumn,
+  })? onSortChanged;
+
+  final ValueListenable<List<T>>? searchMatchesNotifier;
+
+  final ValueListenable<T?>? activeSearchMatchNotifier;
+
+  final bool displayTreeGuidelines;
+
+  @override
+  State<TableRow<T>> createState() => _TableRowState<T>();
+}
+
+class _TableRowState<T> extends State<TableRow<T>>
+    with
+        TickerProviderStateMixin,
+        CollapsibleAnimationMixin,
+        AutoDisposeMixin,
+        SearchableMixin {
+  Key? contentKey;
+
+  late ScrollController scrollController;
+
+  bool isSearchMatch = false;
+
+  bool isActiveSearchMatch = false;
+
+  bool isHovering = false;
+
+  @override
+  void initState() {
+    super.initState();
+    contentKey = ValueKey(this);
+    scrollController = widget.linkedScrollControllerGroup.addAndGet();
+    _initSearchListeners();
+  }
+
+  @override
+  void didUpdateWidget(TableRow<T> oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    setExpanded(widget.isExpanded);
+    if (oldWidget.linkedScrollControllerGroup !=
+        widget.linkedScrollControllerGroup) {
+      scrollController.dispose();
+      scrollController = widget.linkedScrollControllerGroup.addAndGet();
+    }
+
+    cancelListeners();
+    _initSearchListeners();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    scrollController.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final node = widget.node;
+    final widgetOnPressed = widget.onPressed;
+
+    void Function()? onPressed;
+    if (node != null && widgetOnPressed != null) {
+      onPressed = () => widgetOnPressed(node);
+    }
+
+    final row = tableRowFor(
+      context,
+      onPressed: onPressed,
+    );
+
+    final box = SizedBox(
+      height: widget._rowType == _TableRowType.data
+          ? defaultRowHeight
+          : defaultHeaderHeight +
+              (widget.tall ? scaleByFontFactor(densePadding) : 0.0),
+      child: Material(
+        color: _searchAwareBackgroundColor(),
+        child: onPressed != null
+            ? InkWell(
+                canRequestFocus: false,
+                key: contentKey,
+                onTap: onPressed,
+                child: row,
+              )
+            : row,
+      ),
+    );
+    return box;
+  }
+
+  void _initSearchListeners() {
+    if (widget.searchMatchesNotifier != null) {
+      searchMatches = widget.searchMatchesNotifier!.value;
+      isSearchMatch = searchMatches.contains(widget.node);
+      addAutoDisposeListener(widget.searchMatchesNotifier, () {
+        final isPreviousMatch = searchMatches.contains(widget.node);
+        searchMatches = widget.searchMatchesNotifier!.value;
+        final isNewMatch = searchMatches.contains(widget.node);
+
+        // We only want to rebuild the row if it the match status has changed.
+        if (isPreviousMatch != isNewMatch) {
+          setState(() {
+            isSearchMatch = isNewMatch;
+          });
+        }
+      });
+    }
+
+    if (widget.activeSearchMatchNotifier != null) {
+      activeSearchMatch = widget.activeSearchMatchNotifier!.value;
+      isActiveSearchMatch = activeSearchMatch == widget.node;
+      addAutoDisposeListener(widget.activeSearchMatchNotifier, () {
+        final isPreviousActiveSearchMatch = activeSearchMatch == widget.node;
+        activeSearchMatch = widget.activeSearchMatchNotifier!.value;
+        final isNewActiveSearchMatch = activeSearchMatch == widget.node;
+
+        // We only want to rebuild the row if it the match status has changed.
+        if (isPreviousActiveSearchMatch != isNewActiveSearchMatch) {
+          setState(() {
+            isActiveSearchMatch = isNewActiveSearchMatch;
+          });
+        }
+      });
+    }
+  }
+
+  Color _searchAwareBackgroundColor() {
+    final colorScheme = Theme.of(context).colorScheme;
+    final backgroundColor = widget.backgroundColor ?? colorScheme.surface;
+    if (widget.isSelected) {
+      return colorScheme.selectedRowBackgroundColor;
+    }
+    final searchAwareBackgroundColor = isSearchMatch
+        ? Color.alphaBlend(
+            isActiveSearchMatch
+                ? activeSearchMatchColorOpaque
+                : searchMatchColorOpaque,
+            backgroundColor,
+          )
+        : backgroundColor;
+    return searchAwareBackgroundColor;
+  }
+
+  Alignment _alignmentFor(ColumnData<T> column) {
+    switch (column.alignment) {
+      case ColumnAlignment.center:
+        return Alignment.center;
+      case ColumnAlignment.right:
+        return Alignment.centerRight;
+      case ColumnAlignment.left:
+      default:
+        return Alignment.centerLeft;
+    }
+  }
+
+  /// Presents the content of this row.
+  Widget tableRowFor(BuildContext context, {VoidCallback? onPressed}) {
+    Widget columnFor(ColumnData<T> column, double columnWidth) {
+      Widget? content;
+      final theme = Theme.of(context);
+      final node = widget.node;
+      if (widget._rowType == _TableRowType.filler) {
+        content = const SizedBox.shrink();
+      } else if (widget._rowType == _TableRowType.columnHeader) {
+        Widget defaultHeaderRenderer() => _ColumnHeader(
+              column: column,
+              isSortColumn: column == widget.sortColumn,
+              secondarySortColumn: widget.secondarySortColumn,
+              sortDirection: widget.sortDirection!,
+              onSortChanged: widget.onSortChanged,
+            );
+
+        // ignore: avoid-unrelated-type-assertions, false positive.
+        if (column is ColumnHeaderRenderer) {
+          content = (column as ColumnHeaderRenderer)
+              .buildHeader(context, defaultHeaderRenderer);
+        }
+        // If ColumnHeaderRenderer.build returns null, fall back to the default
+        // rendering.
+        content ??= defaultHeaderRenderer();
+      } else if (node != null) {
+        // TODO(kenz): clean up and pull all this code into _ColumnDataRow
+        // widget class.
+        final padding = column.getNodeIndentPx(node);
+        assert(padding >= 0);
+        // ignore: avoid-unrelated-type-assertions, false positive.
+        if (column is ColumnRenderer) {
+          content = (column as ColumnRenderer).build(
+            context,
+            node,
+            isRowSelected: widget.isSelected,
+            isRowHovered: isHovering,
+            onPressed: onPressed,
+          );
+        }
+        // If ColumnRenderer.build returns null, fall back to the default
+        // rendering.
+        content ??= Text.rich(
+          TextSpan(
+            text: column.getDisplayValue(node),
+            children: [
+              if (column.getCaption(node) != null)
+                TextSpan(
+                  text: ' ${column.getCaption(node)}',
+                  style: const TextStyle(
+                    fontStyle: FontStyle.italic,
+                  ),
+                ),
+            ],
+            style: column.contentTextStyle(
+              context,
+              node,
+              isSelected: widget.isSelected,
+            ),
+          ),
+          maxLines: 1,
+          overflow: TextOverflow.ellipsis,
+          textAlign: column.contentTextAlignment,
+        );
+
+        final tooltip = column.getTooltip(node);
+        final richTooltip = column.getRichTooltip(node, context);
+        if (tooltip.isNotEmpty || richTooltip != null) {
+          content = DevToolsTooltip(
+            message: richTooltip == null ? tooltip : null,
+            richMessage: richTooltip,
+            waitDuration: tooltipWaitLong,
+            child: content,
+          );
+        }
+
+        if (column == widget.expandableColumn) {
+          final expandIndicator = widget.isExpandable
+              ? ValueListenableBuilder(
+                  valueListenable: expandController,
+                  builder: (context, _, __) {
+                    return RotationTransition(
+                      turns: expandArrowAnimation,
+                      child: Icon(
+                        Icons.expand_more,
+                        color: theme.colorScheme.onSurface,
+                        size: defaultIconSize,
+                      ),
+                    );
+                  },
+                )
+              : SizedBox(width: defaultIconSize, height: defaultIconSize);
+          content = Row(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              expandIndicator,
+              Expanded(child: content),
+            ],
+          );
+        }
+        content = Padding(
+          padding: EdgeInsets.only(left: padding),
+          child: ClipRect(
+            child: content,
+          ),
+        );
+      } else {
+        throw Exception(
+          'Expected a non-null node for this table column, but node == null.',
+        );
+      }
+
+      content = SizedBox(
+        width: columnWidth,
+        child: Align(
+          alignment: _alignmentFor(column),
+          child: content,
+        ),
+      );
+      if (widget.displayTreeGuidelines &&
+          node != null &&
+          node is TreeNode &&
+          column is TreeColumnData) {
+        content = CustomPaint(
+          painter: _RowGuidelinePainter(
+            node.level,
+            theme.colorScheme,
+          ),
+          child: content,
+        );
+      }
+      return DefaultTextStyle(
+        style: theme.regularTextStyle,
+        child: content,
+      );
+    }
+
+    if (widget._rowType == _TableRowType.columnGroupHeader) {
+      final groups = widget.columnGroups!;
+      return _ColumnGroupHeaderRow(
+        groups: groups,
+        columnWidths: widget.columnWidths,
+        scrollController: scrollController,
+      );
+    }
+
+    final rowDisplayParts = <_TableRowPartDisplayType>[];
+    final groups = widget.columnGroups;
+    if (groups != null && groups.isNotEmpty) {
+      for (int i = 0; i < groups.length; i++) {
+        final groupParts = List.generate(
+          groups[i].range.size as int,
+          (index) => _TableRowPartDisplayType.column,
+        ).joinWith(_TableRowPartDisplayType.columnSpacer);
+        rowDisplayParts.addAll(groupParts);
+        if (i < groups.length - 1) {
+          rowDisplayParts.add(_TableRowPartDisplayType.columnGroupSpacer);
+        }
+      }
+    } else {
+      final parts = List.generate(
+        widget.columns.length,
+        (_) => _TableRowPartDisplayType.column,
+      ).joinWith(_TableRowPartDisplayType.columnSpacer);
+      rowDisplayParts.addAll(parts);
+    }
+
+    // Maps the indices from [rowDisplayParts] to the corresponding index of
+    // each column in [widget.columns].
+    final columnIndexMap = <int, int>{};
+    // Add scope to guarantee [columnIndexTracker] is not used outside of this
+    // block.
+    {
+      var columnIndexTracker = 0;
+      for (int i = 0; i < rowDisplayParts.length; i++) {
+        final type = rowDisplayParts[i];
+        if (type == _TableRowPartDisplayType.column) {
+          columnIndexMap[i] = columnIndexTracker;
+          columnIndexTracker++;
+        }
+      }
+    }
+
+    Widget rowContent = Padding(
+      padding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
+      child: ListView.builder(
+        scrollDirection: Axis.horizontal,
+        controller: scrollController,
+        itemCount: widget.columns.length + widget.columns.numSpacers,
+        itemBuilder: (context, int i) {
+          final displayTypeForIndex = rowDisplayParts[i];
+          switch (displayTypeForIndex) {
+            case _TableRowPartDisplayType.column:
+              final index = columnIndexMap[i]!;
+              return columnFor(
+                widget.columns[index],
+                widget.columnWidths[index],
+              );
+            case _TableRowPartDisplayType.columnSpacer:
+              return const SizedBox(
+                width: columnSpacing,
+                child: VerticalDivider(width: columnSpacing),
+              );
+            case _TableRowPartDisplayType.columnGroupSpacer:
+              return const _ColumnGroupSpacer();
+          }
+        },
+      ),
+    );
+
+    if (widget.enableHoverHandling) {
+      rowContent = MouseRegion(
+        onEnter: (_) => setState(() => isHovering = true),
+        onExit: (_) => setState(() => isHovering = false),
+        child: rowContent,
+      );
+    }
+
+    if (widget._rowType == _TableRowType.columnHeader) {
+      return OutlineDecoration.onlyBottom(child: rowContent);
+    }
+    return rowContent;
+  }
+
+  @override
+  bool get isExpanded => widget.isExpanded;
+
+  @override
+  void onExpandChanged(bool expanded) {}
+
+  @override
+  bool shouldShow() => widget.isShown;
+}
diff --git a/packages/devtools_app/lib/src/shared/table/_tree_table.dart b/packages/devtools_app/lib/src/shared/table/_tree_table.dart
new file mode 100644
index 0000000..f8813ce
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/table/_tree_table.dart
@@ -0,0 +1,491 @@
+// 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.
+
+part of 'table.dart';
+
+enum TreeTableScrollKind {
+  up,
+  down,
+  parent,
+}
+
+/// A table that shows [TreeNode]s.
+///
+/// This takes in a collection of [dataRoots], and a collection of [ColumnData].
+/// It takes at most one [TreeColumnData].
+///
+/// The [ColumnData] gives this table information about how to size its
+/// columns, how to present each row of `data`, and which rows to show.
+///
+/// To lay the contents out, this table's [TreeTableState] creates a flattened
+/// list of the tree hierarchy. It then uses the nesting level of the
+/// deepest row in [dataRoots] to determine how wide to make the [treeColumn].
+///
+/// If [dataRoots.length] > 1, there are multiple trees in this tree table. In
+/// this case, tree table operations (expand all, collapse all, sort, etc.) will
+/// be applied to every tree.
+class TreeTable<T extends TreeNode<T>> extends StatefulWidget {
+  TreeTable({
+    Key? key,
+    required this.keyFactory,
+    required this.dataRoots,
+    required this.dataKey,
+    required this.columns,
+    required this.treeColumn,
+    required this.defaultSortColumn,
+    required this.defaultSortDirection,
+    this.secondarySortColumn,
+    this.autoExpandRoots = false,
+    this.preserveVerticalScrollPosition = false,
+    this.displayTreeGuidelines = false,
+    this.tallHeaders = false,
+    this.headerColor,
+    ValueNotifier<Selection<T?>>? selectionNotifier,
+  })  : selectionNotifier = selectionNotifier ??
+            ValueNotifier<Selection<T?>>(Selection.empty()),
+        assert(columns.contains(treeColumn)),
+        assert(columns.contains(defaultSortColumn)),
+        super(key: key);
+
+  /// Factory that creates keys for each row in this table.
+  final Key Function(T) keyFactory;
+
+  /// The tree structures of rows to show in this table.
+  ///
+  /// Each root in [dataRoots] will be a top-level entry in the table. Depending
+  /// on the expanded / collapsed state of each root and its children,
+  /// additional table rows will be present in the table.
+  final List<T> dataRoots;
+
+  /// Unique key for the data shown in this table.
+  ///
+  /// This key will be used to restore things like sort column, sort direction,
+  /// and scroll position for this table (when [preserveVerticalScrollPosition]
+  /// is true).
+  ///
+  /// We use [TableUiStateStore] to store [_TableUiState] by this key so that
+  /// we can save and restore this state without having to keep [State] or table
+  /// controller objects alive.
+  final String dataKey;
+
+  /// The columns to show in this table.
+  final List<ColumnData<T>> columns;
+
+  /// The column of the table to treat as expandable.
+  final TreeColumnData<T> treeColumn;
+
+  /// The default sort column for this table.
+  ///
+  /// This sort column is passed along to [TreeTableState.tableController],
+  /// which uses [defaultSortColumn] for the starting value in
+  /// [TableControllerBase.tableUiState].
+  final ColumnData<T> defaultSortColumn;
+
+  /// The default [SortDirection] for this table.
+  ///
+  /// This [SortDirection] is passed along to [TreeTableState.tableController],
+  /// which uses [defaultSortDirection] for the starting value in
+  /// [TableControllerBase.tableUiState].
+  final SortDirection defaultSortDirection;
+
+  /// The secondary sort column to be used in the sorting algorithm provided by
+  /// [TableControllerBase.sortDataAndNotify].
+  final ColumnData<T>? secondarySortColumn;
+
+  /// Stores the selected data item (the selected row) for this table.
+  ///
+  /// This notifier's value will be updated when a row of the table is selected.
+  final ValueNotifier<Selection<T?>> selectionNotifier;
+
+  /// Whether the data roots in this table should be automatically expanded.
+  final bool autoExpandRoots;
+
+  /// Whether the vertical scroll position for this table should be preserved for
+  /// each data set.
+  ///
+  /// This should be set to true if the table is not disposed and completely
+  /// rebuilt when changing from one data set to another.
+  final bool preserveVerticalScrollPosition;
+
+  /// Determines whether or not guidelines should be rendered in tree columns.
+  final bool displayTreeGuidelines;
+
+  /// Whether the table headers should be slightly taller than the table rows to
+  /// support multiline text.
+  final bool tallHeaders;
+
+  /// The background color of the header.
+  ///
+  /// If null, defaults to `Theme.of(context).canvasColor`.
+  final Color? headerColor;
+
+  @override
+  TreeTableState<T> createState() => TreeTableState<T>();
+}
+
+class TreeTableState<T extends TreeNode<T>> extends State<TreeTable<T>>
+    with TickerProviderStateMixin, AutoDisposeMixin {
+  FocusNode? get focusNode => _focusNode;
+  late FocusNode _focusNode;
+
+  TreeTableController<T> get tableController => _tableController!;
+  TreeTableController<T>? _tableController;
+
+  late List<T> _data;
+
+  VoidCallback? _selectionListener;
+
+  @override
+  void initState() {
+    super.initState();
+    _setUpTableController();
+
+    _data = tableController.tableData.value.data;
+    addAutoDisposeListener(tableController.tableData, () {
+      setState(() {
+        _data = tableController.tableData.value.data;
+      });
+    });
+
+    _initSelectionListener();
+
+    _focusNode = FocusNode(debugLabel: 'table');
+    autoDisposeFocusNode(_focusNode);
+  }
+
+  @override
+  void didUpdateWidget(TreeTable<T> oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (_tableConfigurationChanged(oldWidget, widget)) {
+      _setUpTableController();
+    } else {
+      _setUpTableController(reset: false);
+    }
+
+    if (oldWidget.selectionNotifier != widget.selectionNotifier) {
+      _initSelectionListener();
+    }
+  }
+
+  @override
+  void dispose() {
+    _tableController = null;
+    super.dispose();
+  }
+
+  /// Sets up the [tableController] for the property values in [widget].
+  ///
+  /// [reset] determines whether or not we should re-initialize
+  /// [_tableController], which should only happen when the core table
+  /// configuration (columns & column groups) has changed.
+  ///
+  /// See [_tableConfigurationChanged].
+  void _setUpTableController({bool reset = true}) {
+    final shouldResetController = reset || _tableController == null;
+    if (shouldResetController) {
+      _tableController = TreeTableController<T>(
+        columns: widget.columns,
+        defaultSortColumn: widget.defaultSortColumn,
+        defaultSortDirection: widget.defaultSortDirection,
+        secondarySortColumn: widget.secondarySortColumn,
+        treeColumn: widget.treeColumn,
+        autoExpandRoots: widget.autoExpandRoots,
+      );
+    }
+
+    if (widget.preserveVerticalScrollPosition) {
+      // Order matters - this must be called before [tableController.setData]
+      tableController.storeScrollPosition();
+    }
+
+    tableController.setData(widget.dataRoots, widget.dataKey);
+  }
+
+  /// Whether the core table configuration has changed, determined by checking
+  /// the equality of columns and column groups.
+  bool _tableConfigurationChanged(
+    TreeTable<T> oldWidget,
+    TreeTable<T> newWidget,
+  ) {
+    final columnsChanged = !collectionEquals(
+          oldWidget.columns.map((c) => c.title),
+          newWidget.columns.map((c) => c.title),
+        ) ||
+        oldWidget.treeColumn.title != newWidget.treeColumn.title;
+    return columnsChanged;
+  }
+
+  void _initSelectionListener() {
+    cancelListener(_selectionListener);
+    _selectionListener = () {
+      final node = widget.selectionNotifier.value.node;
+      if (node != null) {
+        setState(() {
+          expandParents(node.parent);
+        });
+      }
+    };
+    addAutoDisposeListener(widget.selectionNotifier, _selectionListener);
+  }
+
+  void expandParents(T? parent) {
+    if (parent == null) return;
+
+    if (parent.parent?.index != -1) {
+      expandParents(parent.parent);
+    }
+
+    if (!parent.isExpanded) {
+      _toggleNode(parent);
+    }
+  }
+
+  void _onItemPressed(T node, int nodeIndex) {
+    // Rebuilds the table whenever the tree structure has been updated.
+    widget.selectionNotifier.value = Selection(
+      node: node,
+      nodeIndex: nodeIndex,
+    );
+
+    _toggleNode(node);
+  }
+
+  void _toggleNode(T node) {
+    if (!node.isExpandable) {
+      node.leaf();
+      return;
+    }
+
+    setState(() {
+      if (node.isExpanded) {
+        node.collapse();
+      } else {
+        node.expand();
+      }
+    });
+
+    tableController.setDataAndNotify();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return DevToolsTable<T>(
+      tableController: tableController,
+      columnWidths: tableController.columnWidths!,
+      rowBuilder: _buildRow,
+      rowItemExtent: defaultRowHeight,
+      focusNode: _focusNode,
+      handleKeyEvent: _handleKeyEvent,
+      selectionNotifier: widget.selectionNotifier,
+      preserveVerticalScrollPosition: widget.preserveVerticalScrollPosition,
+      tallHeaders: widget.tallHeaders,
+      headerColor: widget.headerColor,
+    );
+  }
+
+  Widget _buildRow({
+    required BuildContext context,
+    required LinkedScrollControllerGroup linkedScrollControllerGroup,
+    required int index,
+    required List<double> columnWidths,
+    required bool isPinned,
+    required bool enableHoverHandling,
+  }) {
+    Widget rowForNode(T node) {
+      final selection = widget.selectionNotifier.value;
+      node.index = index;
+      return TableRow<T>(
+        key: widget.keyFactory(node),
+        linkedScrollControllerGroup: linkedScrollControllerGroup,
+        node: node,
+        onPressed: (item) => _onItemPressed(item, index),
+        backgroundColor: alternatingColorForIndex(
+          index,
+          Theme.of(context).colorScheme,
+        ),
+        columns: tableController.columns,
+        columnWidths: columnWidths,
+        expandableColumn: tableController.treeColumn,
+        isSelected: selection.node != null && selection.node == node,
+        isExpanded: node.isExpanded,
+        isExpandable: node.isExpandable,
+        isShown: node.shouldShow(),
+        displayTreeGuidelines: widget.displayTreeGuidelines,
+      );
+    }
+
+    return rowForNode(_data[index]);
+  }
+
+  KeyEventResult _handleKeyEvent(
+    KeyEvent event,
+    ScrollController scrollController,
+    BoxConstraints constraints,
+  ) {
+    if (!event.isKeyDownOrRepeat) return KeyEventResult.ignored;
+
+    // Exit early if we aren't handling the key
+    if (![
+      LogicalKeyboardKey.arrowDown,
+      LogicalKeyboardKey.arrowUp,
+      LogicalKeyboardKey.arrowLeft,
+      LogicalKeyboardKey.arrowRight,
+    ].contains(event.logicalKey)) return KeyEventResult.ignored;
+
+    // If there is no selected node, choose the first one.
+    if (widget.selectionNotifier.value.node == null) {
+      widget.selectionNotifier.value = Selection(
+        node: _data[0],
+        nodeIndex: 0,
+      );
+    }
+
+    final selection = widget.selectionNotifier.value;
+    assert(selection.node == _data[selection.nodeIndex!]);
+
+    if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
+      _moveSelection(
+        TreeTableScrollKind.down,
+        scrollController,
+        constraints.maxHeight,
+      );
+    } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
+      _moveSelection(
+        TreeTableScrollKind.up,
+        scrollController,
+        constraints.maxHeight,
+      );
+    } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+      // On left arrow collapse the row if it is expanded. If it is not, move
+      // selection to its parent.
+      if (selection.node!.isExpanded) {
+        _toggleNode(selection.node!);
+      } else {
+        _moveSelection(
+          TreeTableScrollKind.parent,
+          scrollController,
+          constraints.maxHeight,
+        );
+      }
+    } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
+      // On right arrow expand the row if possible, otherwise move selection down.
+      if (selection.node!.isExpandable && !selection.node!.isExpanded) {
+        _toggleNode(selection.node!);
+      } else {
+        _moveSelection(
+          TreeTableScrollKind.down,
+          scrollController,
+          constraints.maxHeight,
+        );
+      }
+    }
+
+    return KeyEventResult.handled;
+  }
+
+  void _moveSelection(
+    TreeTableScrollKind scrollKind,
+    ScrollController scrollController,
+    double viewportHeight,
+  ) {
+    // get the index of the first item fully visible in the viewport
+    final firstItemIndex = (scrollController.offset / defaultRowHeight).ceil();
+
+    // '-2' in the following calculations is needed because the header row
+    // occupies space in the viewport so we must subtract 1 for that, and we
+    // subtract 1 to account for the fact that a partial row could be displayed
+    // at the top and bottom of the view.
+    final minCompleteItemsInView =
+        max((viewportHeight / defaultRowHeight).floor() - 2, 0);
+    final lastItemIndex = firstItemIndex + minCompleteItemsInView - 1;
+    late int newSelectedNodeIndex;
+
+    final selectionValue = widget.selectionNotifier.value;
+    final selectedNodeIndex = selectionValue.nodeIndex;
+    switch (scrollKind) {
+      case TreeTableScrollKind.down:
+        newSelectedNodeIndex = min(selectedNodeIndex! + 1, _data.length - 1);
+        break;
+      case TreeTableScrollKind.up:
+        newSelectedNodeIndex = max(selectedNodeIndex! - 1, 0);
+        break;
+      case TreeTableScrollKind.parent:
+        newSelectedNodeIndex = selectionValue.node!.parent != null
+            ? max(_data.indexOf(selectionValue.node!.parent!), 0)
+            : 0;
+        break;
+    }
+
+    final newSelectedNode = _data[newSelectedNodeIndex];
+    final isBelowViewport = newSelectedNodeIndex > lastItemIndex;
+    final isAboveViewport = newSelectedNodeIndex < firstItemIndex;
+
+    if (isBelowViewport) {
+      // Scroll so selected row is the last full item displayed in the viewport.
+      // To do this we need to be showing the (minCompleteItemsInView + 1)
+      // previous item  at the top.
+      unawaited(
+        scrollController.animateTo(
+          (newSelectedNodeIndex - minCompleteItemsInView + 1) *
+              defaultRowHeight,
+          duration: defaultDuration,
+          curve: defaultCurve,
+        ),
+      );
+    } else if (isAboveViewport) {
+      unawaited(
+        scrollController.animateTo(
+          newSelectedNodeIndex * defaultRowHeight,
+          duration: defaultDuration,
+          curve: defaultCurve,
+        ),
+      );
+    }
+
+    // We do not need to scroll into view here because we have manually
+    // managed the scrolling in the above checks for `isBelowViewport` and
+    // `isAboveViewport`.
+    widget.selectionNotifier.value = Selection(
+      node: newSelectedNode,
+      nodeIndex: newSelectedNodeIndex,
+    );
+  }
+}
+
+/// A custom painter to draw guidelines between tree table nodes.
+class _RowGuidelinePainter extends CustomPainter {
+  _RowGuidelinePainter(this.level, this.colorScheme);
+
+  static const _treeGuidelineColors = [
+    Color(0xFF13B9FD),
+    Color(0xFF5BC43B),
+  ];
+
+  final int level;
+  final ColorScheme colorScheme;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    for (int i = 0; i < level; ++i) {
+      final currentX = i * TreeColumnData.treeToggleWidth + defaultIconSize / 2;
+      // Draw a vertical line for each tick identifying a connection between
+      // an ancestor of this node and some other node in the tree.
+      canvas.drawLine(
+        Offset(currentX, 0.0),
+        Offset(currentX, defaultRowHeight),
+        Paint()
+          ..color = _treeGuidelineColors[i % _treeGuidelineColors.length]
+          ..strokeWidth = 1.0,
+      );
+    }
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) {
+    if (oldDelegate is _RowGuidelinePainter) {
+      return oldDelegate.colorScheme.isLight != colorScheme.isLight;
+    }
+    return true;
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/table/column_widths.dart b/packages/devtools_app/lib/src/shared/table/column_widths.dart
index e445573..0039aff 100644
--- a/packages/devtools_app/lib/src/shared/table/column_widths.dart
+++ b/packages/devtools_app/lib/src/shared/table/column_widths.dart
@@ -6,7 +6,6 @@
 
 import 'package:devtools_app_shared/ui.dart';
 
-import '../common_widgets.dart';
 import '../primitives/trees.dart';
 import '../primitives/utils.dart';
 import 'table_controller.dart';
@@ -20,42 +19,25 @@
 const columnGroupSpacingWithPadding = columnGroupSpacing + 2 * defaultSpacing;
 const columnSpacing = defaultSpacing;
 
+/// The minimum width that can be used for variable width columns.
+final defaultVariableWidthColumnSize = scaleByFontFactor(1200.0);
+
 extension FlatColumnWidthExtension<T> on FlatTableController<T> {
   /// Calculates [FlatTable] column widths when the columns should be sized as
-  /// either 1) the [fixedWidthPx] specified by the column, or 2) the size of
-  /// the column's widest content.
+  /// either 1) the [fixedWidthPx] specified by the column, or
+  /// 2) [defaultVariableWidthColumnSize], the minimum width that can be used
+  /// for variable width columns.
   ///
   /// If the sum of the column widths exceeds the available screen space, the
   /// table will scroll horizontally.
-  List<double> computeColumnWidthsSizeToContent(List<T> data) {
-    final widths = <double>[];
-    for (var column in columns) {
-      var width = column.fixedWidthPx;
-      if (width == null) {
-        // Note: this is assuming that we're always using a monospace font in
-        // our tree tables. This will almost certainly cause overflows if we
-        // try to use non-monospace fonts.
-        //
-        // `characterWidth` was determined through trial and error to roughly
-        // approximate the width of a single character.
-        final characterWidth = assumedMonospaceCharacterWidth;
-        double longestWidth = 0;
-        for (var node in data) {
-          final width = column.getDisplayValue(node).length * characterWidth;
-          if (width > longestWidth) {
-            longestWidth = width;
-          }
-        }
-        // Ensure we account for the [minWidthPx] value and that we always
-        // display the full column title.
-        width = max(
-          max(longestWidth, column.minWidthPx ?? 0),
-          column.title.length * characterWidth,
-        );
-      }
-      widths.add(width);
-    }
-    return widths;
+  List<double> computeColumnWidthsSizeToContent() {
+    return columns
+        .map(
+          (c) =>
+              c.fixedWidthPx ??
+              (c.minWidthPx ?? defaultVariableWidthColumnSize),
+        )
+        .toList();
   }
 
   /// Calculates [FlatTable] column widths such that they will take up the
@@ -151,38 +133,23 @@
 
 extension TreeColumnWidthExtension<T extends TreeNode<T>>
     on TreeTableController<T> {
-  List<double> computeColumnWidths(List<T> flattenedList) {
-    final widths = <double>[];
-    for (var column in columns) {
-      double width;
-      if (column.fixedWidthPx != null) {
-        width = column.fixedWidthPx!;
-      } else {
-        // Note: this is assuming that we're always using a monospace font in
-        // our tree tables. This will almost certainly cause overflows if we
-        // try to use non-monospace fonts.
-        //
-        // `characterWidth` was determined through trial and error to roughly
-        // approximate the width of a single character.
-        final characterWidth = assumedMonospaceCharacterWidth;
-        double longestWidth = 0;
-        for (var node in flattenedList) {
-          final width = column.getNodeIndentPx(node) +
-              (column.getDisplayValue(node).length * characterWidth);
-          if (width > longestWidth) {
-            longestWidth = width;
-          }
-        }
-        // Add two characters to account for expansion caret and ensure we
-        // always display the full column title.
-        width = max(
-              max(longestWidth, column.minWidthPx ?? 0),
-              column.title.length * characterWidth,
-            ) +
-            characterWidth * 2;
-      }
-      widths.add(width);
-    }
-    return widths;
+  /// Calculates [TreeTable] column widths.
+  ///
+  /// Non-tree columns will be sized with their specified [fixedWidthPx]. The
+  /// tree column width will include space for the max indentation of the fully
+  /// expanded tree and [defaultVariableWidthColumnSize] for displaying the
+  /// tree column content.
+  ///
+  /// If the sum of the column widths exceeds the available screen space, the
+  /// table will scroll horizontally.
+  List<double> computeColumnWidths(int maxTableDepth) {
+    return columns
+        .map(
+          (c) =>
+              c.fixedWidthPx ??
+              maxTableDepth * TreeColumnData.treeToggleWidth +
+                  defaultVariableWidthColumnSize,
+        )
+        .toList();
   }
 }
diff --git a/packages/devtools_app/lib/src/shared/table/table.dart b/packages/devtools_app/lib/src/shared/table/table.dart
index 27ed3bc..19cf7a5 100644
--- a/packages/devtools_app/lib/src/shared/table/table.dart
+++ b/packages/devtools_app/lib/src/shared/table/table.dart
@@ -23,12 +23,14 @@
 import 'table_controller.dart';
 import 'table_data.dart';
 
+part '_flat_table.dart';
+part '_table_column.dart';
+part '_table_row.dart';
+part '_tree_table.dart';
+
 // TODO(devoncarew): We need to render the selected row with a different
 // background color.
 
-/// The height for rows in the table.
-double get defaultRowHeight => scaleByFontFactor(32.0);
-
 typedef IndexedScrollableWidgetBuilder = Widget Function({
   required BuildContext context,
   required LinkedScrollControllerGroup linkedScrollControllerGroup,
@@ -44,367 +46,6 @@
   BoxConstraints constraints,
 );
 
-enum ScrollKind {
-  up,
-  down,
-  parent,
-}
-
-/// A [FlatTable] widget that is searchable.
-///
-/// The table requires a [searchController], which is responsible for feeding
-/// information about search matches and the active search match to the table.
-///
-/// This table will automatically refresh search matches on the
-/// [searchController] after sort operations that are triggered from the table.
-class SearchableFlatTable<T extends SearchableDataMixin> extends FlatTable<T> {
-  SearchableFlatTable({
-    super.key,
-    required SearchControllerMixin<T> searchController,
-    required super.keyFactory,
-    required super.data,
-    required super.dataKey,
-    required super.columns,
-    required super.defaultSortColumn,
-    required super.defaultSortDirection,
-    super.secondarySortColumn,
-    super.sortOriginalData = false,
-    super.pinBehavior = FlatTablePinBehavior.none,
-    super.columnGroups,
-    super.autoScrollContent = false,
-    super.onItemSelected,
-    super.preserveVerticalScrollPosition = false,
-    super.includeColumnGroupHeaders = true,
-    super.sizeColumnsToFit = true,
-    super.selectionNotifier,
-  }) : super(
-          searchMatchesNotifier: searchController.searchMatches,
-          activeSearchMatchNotifier: searchController.activeSearchMatch,
-          onDataSorted: () => WidgetsBinding.instance.addPostFrameCallback((_) {
-            // This needs to be in a post frame callback so that the search
-            // matches are not updated in the middle of a table build.
-            searchController.refreshSearchMatches();
-          }),
-        );
-}
-
-/// A table that displays in a collection of [data], based on a collection of
-/// [ColumnData].
-///
-/// The [ColumnData] gives this table information about how to size its columns,
-/// and how to present each row of `data`.
-class FlatTable<T> extends StatefulWidget {
-  FlatTable({
-    Key? key,
-    required this.keyFactory,
-    required this.data,
-    required this.dataKey,
-    required this.columns,
-    this.columnGroups,
-    this.autoScrollContent = false,
-    this.onItemSelected,
-    required this.defaultSortColumn,
-    required this.defaultSortDirection,
-    this.onDataSorted,
-    this.sortOriginalData = false,
-    this.pinBehavior = FlatTablePinBehavior.none,
-    this.secondarySortColumn,
-    this.searchMatchesNotifier,
-    this.activeSearchMatchNotifier,
-    this.preserveVerticalScrollPosition = false,
-    this.includeColumnGroupHeaders = true,
-    this.tallHeaders = false,
-    this.sizeColumnsToFit = true,
-    this.headerColor,
-    this.fillWithEmptyRows = false,
-    this.enableHoverHandling = false,
-    ValueNotifier<T?>? selectionNotifier,
-  })  : selectionNotifier = selectionNotifier ?? ValueNotifier<T?>(null),
-        super(key: key);
-
-  /// List of columns to display.
-  ///
-  /// These [ColumnData] elements should be defined as static
-  /// OR if they cannot be defined as static,
-  /// they should not manage stateful data.
-  ///
-  /// [FlatTableState.didUpdateWidget] checks if the columns have
-  /// changed before re-initializing the table controller,
-  /// and the columns are compared by title only.
-  /// See also [FlatTableState. _tableConfigurationChanged].
-  final List<ColumnData<T>> columns;
-
-  final List<ColumnGroup>? columnGroups;
-
-  /// Whether the columns for this table should be sized so that the entire
-  /// table fits in view (e.g. so that there is no horizontal scrolling).
-  final bool sizeColumnsToFit;
-
-  // TODO(kenz): should we enable this behavior by default? Does it ever matter
-  // to preserve the order of the original data passed to a flat table?
-  /// Whether table sorting should sort the original data list instead of
-  /// creating a copy.
-  final bool sortOriginalData;
-
-  /// Determines if the headers for column groups should be rendered.
-  ///
-  /// If set to false and `columnGroups` is non-null and non-empty, only
-  /// the vertical dividing lines will be drawn for each column group boundary.
-  final bool includeColumnGroupHeaders;
-
-  /// Whether the table headers should be slightly taller than the table rows to
-  /// support multiline text.
-  final bool tallHeaders;
-
-  /// The background color of the header.
-  ///
-  /// If null, defaults to `Theme.of(context).canvasColor`.
-  final Color? headerColor;
-
-  /// Whether to fill the table with empty rows.
-  final bool fillWithEmptyRows;
-
-  /// Whether to enable hover handling.
-  final bool enableHoverHandling;
-
-  /// Data set to show as rows in this table.
-  final List<T> data;
-
-  /// Unique key for the data shown in this table.
-  ///
-  /// This key will be used to restore things like sort column, sort direction,
-  /// and scroll position for this table (when [preserveVerticalScrollPosition]
-  /// is true).
-  ///
-  /// We use [TableUiStateStore] to store [_TableUiState] by this key so that
-  /// we can save and restore this state without having to keep [State] or table
-  /// controller objects alive.
-  final String dataKey;
-
-  /// Auto-scrolling the table to keep new content visible.
-  final bool autoScrollContent;
-
-  /// Factory that creates keys for each row in this table.
-  final Key Function(T data) keyFactory;
-
-  /// Callback that, when non-null, will be called on each table row selection.
-  final ItemSelectedCallback<T?>? onItemSelected;
-
-  /// Determines how elements that request to be pinned are displayed.
-  ///
-  /// Defaults to [FlatTablePinBehavior.none], which disables pinnning.
-  final FlatTablePinBehavior pinBehavior;
-
-  /// The default sort column for this table.
-  ///
-  /// This sort column is passed along to [TreeTableState.tableController],
-  /// which uses [defaultSortColumn] for the starting value in
-  /// [TableControllerBase.tableUiState].
-  final ColumnData<T> defaultSortColumn;
-
-  /// The default [SortDirection] for this table.
-  ///
-  /// This [SortDirection] is passed along to [TreeTableState.tableController],
-  /// which uses [defaultSortDirection] for the starting value in
-  /// [TableControllerBase.tableUiState].
-  final SortDirection defaultSortDirection;
-
-  /// The secondary sort column to be used in the sorting algorithm provided by
-  /// [TableControllerBase.sortDataAndNotify].
-  final ColumnData<T>? secondarySortColumn;
-
-  /// Callback that will be called after each table sort operation.
-  final VoidCallback? onDataSorted;
-
-  /// Notifies with the list of data items that should be marked as search
-  /// matches.
-  final ValueListenable<List<T>>? searchMatchesNotifier;
-
-  /// Notifies with the data item that should be marked as the active search
-  /// match.
-  final ValueListenable<T?>? activeSearchMatchNotifier;
-
-  /// Stores the selected data item (the selected row) for this table.
-  ///
-  /// This notifier's value will be updated when a row of the table is selected.
-  final ValueNotifier<T?> selectionNotifier;
-
-  /// Whether the vertical scroll position for this table should be preserved for
-  /// each data set.
-  ///
-  /// This should be set to true if the table is not disposed and completely
-  /// rebuilt when changing from one data set to another.
-  final bool preserveVerticalScrollPosition;
-
-  @override
-  FlatTableState<T> createState() => FlatTableState<T>();
-}
-
-@visibleForTesting
-class FlatTableState<T> extends State<FlatTable<T>> with AutoDisposeMixin {
-  FlatTableController<T> get tableController => _tableController!;
-  FlatTableController<T>? _tableController;
-
-  @override
-  void initState() {
-    super.initState();
-    _setUpTableController();
-    addAutoDisposeListener(tableController.tableData);
-
-    if (tableController.pinBehavior != FlatTablePinBehavior.none &&
-        this is! State<FlatTable<PinnableListEntry>>) {
-      throw StateError('$T must implement PinnableListEntry');
-    }
-  }
-
-  @override
-  void didUpdateWidget(FlatTable<T> oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    if (_tableConfigurationChanged(oldWidget, widget)) {
-      _setUpTableController();
-    } else if (!collectionEquals(oldWidget.data, widget.data)) {
-      _setUpTableController(reset: false);
-    }
-  }
-
-  @override
-  void dispose() {
-    _tableController = null;
-    super.dispose();
-  }
-
-  /// Sets up the [tableController] for the property values in [widget].
-  ///
-  /// [reset] determines whether or not we should re-initialize
-  /// [_tableController], which should only happen when the core table
-  /// configuration (columns & column groups) has changed.
-  ///
-  /// See [_tableConfigurationChanged].
-  void _setUpTableController({bool reset = true}) {
-    final shouldResetController = reset || _tableController == null;
-    if (shouldResetController) {
-      _tableController = FlatTableController<T>(
-        columns: widget.columns,
-        defaultSortColumn: widget.defaultSortColumn,
-        defaultSortDirection: widget.defaultSortDirection,
-        secondarySortColumn: widget.secondarySortColumn,
-        columnGroups: widget.columnGroups,
-        includeColumnGroupHeaders: widget.includeColumnGroupHeaders,
-        pinBehavior: widget.pinBehavior,
-        sizeColumnsToFit: widget.sizeColumnsToFit,
-        sortOriginalData: widget.sortOriginalData,
-        onDataSorted: widget.onDataSorted,
-      );
-    }
-
-    if (widget.preserveVerticalScrollPosition) {
-      // Order matters - this must be called before [tableController.setData]
-      tableController.storeScrollPosition();
-    }
-
-    tableController
-      ..setData(widget.data, widget.dataKey)
-      ..pinBehavior = widget.pinBehavior;
-  }
-
-  /// Whether the core table configuration has changed, determined by checking
-  /// the equality of columns and column groups.
-  bool _tableConfigurationChanged(
-    FlatTable<T> oldWidget,
-    FlatTable<T> newWidget,
-  ) {
-    final columnsChanged = !collectionEquals(
-          oldWidget.columns.map((c) => c.title),
-          newWidget.columns.map((c) => c.title),
-        ) ||
-        !collectionEquals(
-          oldWidget.columnGroups?.map((c) => c.title),
-          newWidget.columnGroups?.map((c) => c.title),
-        );
-    return columnsChanged;
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    Widget buildTable(List<double> columnWidths) => _Table<T>(
-          tableController: tableController,
-          columnWidths: columnWidths,
-          autoScrollContent: widget.autoScrollContent,
-          rowBuilder: _buildRow,
-          activeSearchMatchNotifier: widget.activeSearchMatchNotifier,
-          rowItemExtent: defaultRowHeight,
-          preserveVerticalScrollPosition: widget.preserveVerticalScrollPosition,
-          tallHeaders: widget.tallHeaders,
-          headerColor: widget.headerColor,
-          fillWithEmptyRows: widget.fillWithEmptyRows,
-          enableHoverHandling: widget.enableHoverHandling,
-        );
-    if (widget.sizeColumnsToFit || tableController.columnWidths == null) {
-      return LayoutBuilder(
-        builder: (context, constraints) => buildTable(
-          tableController.computeColumnWidthsSizeToFit(
-            constraints.maxWidth,
-          ),
-        ),
-      );
-    }
-    return buildTable(tableController.columnWidths!);
-  }
-
-  Widget _buildRow({
-    required BuildContext context,
-    required LinkedScrollControllerGroup linkedScrollControllerGroup,
-    required int index,
-    required List<double> columnWidths,
-    required bool isPinned,
-    required bool enableHoverHandling,
-  }) {
-    final pinnedData = tableController.pinnedData;
-    final data = isPinned ? pinnedData : tableController.tableData.value.data;
-    if (index >= data.length) {
-      return TableRow<T>.filler(
-        linkedScrollControllerGroup: linkedScrollControllerGroup,
-        columns: tableController.columns,
-        columnGroups: tableController.columnGroups,
-        columnWidths: columnWidths,
-        backgroundColor: alternatingColorForIndex(
-          index,
-          Theme.of(context).colorScheme,
-        ),
-      );
-    }
-
-    final node = data[index];
-    return ValueListenableBuilder<T?>(
-      valueListenable: widget.selectionNotifier,
-      builder: (context, selected, _) {
-        return TableRow<T>(
-          key: widget.keyFactory(node),
-          linkedScrollControllerGroup: linkedScrollControllerGroup,
-          node: node,
-          onPressed: (T? selection) {
-            widget.selectionNotifier.value = selection;
-            if (widget.onItemSelected != null && selection != null) {
-              widget.onItemSelected!(selection);
-            }
-          },
-          columns: tableController.columns,
-          columnGroups: tableController.columnGroups,
-          columnWidths: columnWidths,
-          backgroundColor: alternatingColorForIndex(
-            index,
-            Theme.of(context).colorScheme,
-          ),
-          isSelected: node != null && node == selected,
-          searchMatchesNotifier: widget.searchMatchesNotifier,
-          activeSearchMatchNotifier: widget.activeSearchMatchNotifier,
-          enableHoverHandling: enableHoverHandling,
-        );
-      },
-    );
-  }
-}
-
 class Selection<T> {
   Selection({
     this.node,
@@ -426,447 +67,11 @@
   final bool scrollIntoView;
 }
 
-// TODO(https://github.com/flutter/devtools/issues/1657)
-
-/// A table that shows [TreeNode]s.
-///
-/// This takes in a collection of [dataRoots], and a collection of [ColumnData].
-/// It takes at most one [TreeColumnData].
-///
-/// The [ColumnData] gives this table information about how to size its
-/// columns, how to present each row of `data`, and which rows to show.
-///
-/// To lay the contents out, this table's [TreeTableState] creates a flattened
-/// list of the tree hierarchy. It then uses the nesting level of the
-/// deepest row in [dataRoots] to determine how wide to make the [treeColumn].
-///
-/// If [dataRoots.length] > 1, there are multiple trees in this tree table. In
-/// this case, tree table operations (expand all, collapse all, sort, etc.) will
-/// be applied to every tree.
-class TreeTable<T extends TreeNode<T>> extends StatefulWidget {
-  TreeTable({
-    Key? key,
-    required this.keyFactory,
-    required this.dataRoots,
-    required this.dataKey,
-    required this.columns,
-    required this.treeColumn,
-    required this.defaultSortColumn,
-    required this.defaultSortDirection,
-    this.secondarySortColumn,
-    this.autoExpandRoots = false,
-    this.preserveVerticalScrollPosition = false,
-    this.displayTreeGuidelines = false,
-    this.tallHeaders = false,
-    this.headerColor,
-    ValueNotifier<Selection<T?>>? selectionNotifier,
-  })  : selectionNotifier = selectionNotifier ??
-            ValueNotifier<Selection<T?>>(Selection.empty()),
-        assert(columns.contains(treeColumn)),
-        assert(columns.contains(defaultSortColumn)),
-        super(key: key);
-
-  /// Factory that creates keys for each row in this table.
-  final Key Function(T) keyFactory;
-
-  /// The tree structures of rows to show in this table.
-  ///
-  /// Each root in [dataRoots] will be a top-level entry in the table. Depending
-  /// on the expanded / collapsed state of each root and its children,
-  /// additional table rows will be present in the table.
-  final List<T> dataRoots;
-
-  /// Unique key for the data shown in this table.
-  ///
-  /// This key will be used to restore things like sort column, sort direction,
-  /// and scroll position for this table (when [preserveVerticalScrollPosition]
-  /// is true).
-  ///
-  /// We use [TableUiStateStore] to store [_TableUiState] by this key so that
-  /// we can save and restore this state without having to keep [State] or table
-  /// controller objects alive.
-  final String dataKey;
-
-  /// The columns to show in this table.
-  final List<ColumnData<T>> columns;
-
-  /// The column of the table to treat as expandable.
-  final TreeColumnData<T> treeColumn;
-
-  /// The default sort column for this table.
-  ///
-  /// This sort column is passed along to [TreeTableState.tableController],
-  /// which uses [defaultSortColumn] for the starting value in
-  /// [TableControllerBase.tableUiState].
-  final ColumnData<T> defaultSortColumn;
-
-  /// The default [SortDirection] for this table.
-  ///
-  /// This [SortDirection] is passed along to [TreeTableState.tableController],
-  /// which uses [defaultSortDirection] for the starting value in
-  /// [TableControllerBase.tableUiState].
-  final SortDirection defaultSortDirection;
-
-  /// The secondary sort column to be used in the sorting algorithm provided by
-  /// [TableControllerBase.sortDataAndNotify].
-  final ColumnData<T>? secondarySortColumn;
-
-  /// Stores the selected data item (the selected row) for this table.
-  ///
-  /// This notifier's value will be updated when a row of the table is selected.
-  final ValueNotifier<Selection<T?>> selectionNotifier;
-
-  /// Whether the data roots in this table should be automatically expanded.
-  final bool autoExpandRoots;
-
-  /// Whether the vertical scroll position for this table should be preserved for
-  /// each data set.
-  ///
-  /// This should be set to true if the table is not disposed and completely
-  /// rebuilt when changing from one data set to another.
-  final bool preserveVerticalScrollPosition;
-
-  /// Determines whether or not guidelines should be rendered in tree columns.
-  final bool displayTreeGuidelines;
-
-  /// Whether the table headers should be slightly taller than the table rows to
-  /// support multiline text.
-  final bool tallHeaders;
-
-  /// The background color of the header.
-  ///
-  /// If null, defaults to `Theme.of(context).canvasColor`.
-  final Color? headerColor;
-
-  @override
-  TreeTableState<T> createState() => TreeTableState<T>();
-}
-
-class TreeTableState<T extends TreeNode<T>> extends State<TreeTable<T>>
-    with TickerProviderStateMixin, AutoDisposeMixin {
-  FocusNode? get focusNode => _focusNode;
-  late FocusNode _focusNode;
-
-  TreeTableController<T> get tableController => _tableController!;
-  TreeTableController<T>? _tableController;
-
-  late List<T> _data;
-
-  VoidCallback? _selectionListener;
-
-  @override
-  void initState() {
-    super.initState();
-    _setUpTableController();
-
-    _data = tableController.tableData.value.data;
-    addAutoDisposeListener(tableController.tableData, () {
-      setState(() {
-        _data = tableController.tableData.value.data;
-      });
-    });
-
-    _initSelectionListener();
-
-    _focusNode = FocusNode(debugLabel: 'table');
-    autoDisposeFocusNode(_focusNode);
-  }
-
-  @override
-  void didUpdateWidget(TreeTable<T> oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    if (_tableConfigurationChanged(oldWidget, widget)) {
-      _setUpTableController();
-    } else {
-      _setUpTableController(reset: false);
-    }
-
-    if (oldWidget.selectionNotifier != widget.selectionNotifier) {
-      _initSelectionListener();
-    }
-  }
-
-  @override
-  void dispose() {
-    _tableController = null;
-    super.dispose();
-  }
-
-  /// Sets up the [tableController] for the property values in [widget].
-  ///
-  /// [reset] determines whether or not we should re-initialize
-  /// [_tableController], which should only happen when the core table
-  /// configuration (columns & column groups) has changed.
-  ///
-  /// See [_tableConfigurationChanged].
-  void _setUpTableController({bool reset = true}) {
-    final shouldResetController = reset || _tableController == null;
-    if (shouldResetController) {
-      _tableController = TreeTableController<T>(
-        columns: widget.columns,
-        defaultSortColumn: widget.defaultSortColumn,
-        defaultSortDirection: widget.defaultSortDirection,
-        secondarySortColumn: widget.secondarySortColumn,
-        treeColumn: widget.treeColumn,
-        autoExpandRoots: widget.autoExpandRoots,
-      );
-    }
-
-    if (widget.preserveVerticalScrollPosition) {
-      // Order matters - this must be called before [tableController.setData]
-      tableController.storeScrollPosition();
-    }
-
-    tableController.setData(widget.dataRoots, widget.dataKey);
-  }
-
-  /// Whether the core table configuration has changed, determined by checking
-  /// the equality of columns and column groups.
-  bool _tableConfigurationChanged(
-    TreeTable<T> oldWidget,
-    TreeTable<T> newWidget,
-  ) {
-    final columnsChanged = !collectionEquals(
-          oldWidget.columns.map((c) => c.title),
-          newWidget.columns.map((c) => c.title),
-        ) ||
-        oldWidget.treeColumn.title != newWidget.treeColumn.title;
-    return columnsChanged;
-  }
-
-  void _initSelectionListener() {
-    cancelListener(_selectionListener);
-    _selectionListener = () {
-      final node = widget.selectionNotifier.value.node;
-      if (node != null) {
-        setState(() {
-          expandParents(node.parent);
-        });
-      }
-    };
-    addAutoDisposeListener(widget.selectionNotifier, _selectionListener);
-  }
-
-  void expandParents(T? parent) {
-    if (parent == null) return;
-
-    if (parent.parent?.index != -1) {
-      expandParents(parent.parent);
-    }
-
-    if (!parent.isExpanded) {
-      _toggleNode(parent);
-    }
-  }
-
-  void _onItemPressed(T node, int nodeIndex) {
-    // Rebuilds the table whenever the tree structure has been updated.
-    widget.selectionNotifier.value = Selection(
-      node: node,
-      nodeIndex: nodeIndex,
-    );
-
-    _toggleNode(node);
-  }
-
-  void _toggleNode(T node) {
-    if (!node.isExpandable) {
-      node.leaf();
-      return;
-    }
-
-    setState(() {
-      if (node.isExpanded) {
-        node.collapse();
-      } else {
-        node.expand();
-      }
-    });
-
-    tableController.setDataAndNotify();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return _Table<T>(
-      tableController: tableController,
-      columnWidths: tableController.columnWidths!,
-      rowBuilder: _buildRow,
-      rowItemExtent: defaultRowHeight,
-      focusNode: _focusNode,
-      handleKeyEvent: _handleKeyEvent,
-      selectionNotifier: widget.selectionNotifier,
-      preserveVerticalScrollPosition: widget.preserveVerticalScrollPosition,
-      tallHeaders: widget.tallHeaders,
-      headerColor: widget.headerColor,
-    );
-  }
-
-  Widget _buildRow({
-    required BuildContext context,
-    required LinkedScrollControllerGroup linkedScrollControllerGroup,
-    required int index,
-    required List<double> columnWidths,
-    required bool isPinned,
-    required bool enableHoverHandling,
-  }) {
-    Widget rowForNode(T node) {
-      final selection = widget.selectionNotifier.value;
-      node.index = index;
-      return TableRow<T>(
-        key: widget.keyFactory(node),
-        linkedScrollControllerGroup: linkedScrollControllerGroup,
-        node: node,
-        onPressed: (item) => _onItemPressed(item, index),
-        backgroundColor: alternatingColorForIndex(
-          index,
-          Theme.of(context).colorScheme,
-        ),
-        columns: tableController.columns,
-        columnWidths: columnWidths,
-        expandableColumn: tableController.treeColumn,
-        isSelected: selection.node != null && selection.node == node,
-        isExpanded: node.isExpanded,
-        isExpandable: node.isExpandable,
-        isShown: node.shouldShow(),
-        displayTreeGuidelines: widget.displayTreeGuidelines,
-      );
-    }
-
-    return rowForNode(_data[index]);
-  }
-
-  KeyEventResult _handleKeyEvent(
-    KeyEvent event,
-    ScrollController scrollController,
-    BoxConstraints constraints,
-  ) {
-    if (!event.isKeyDownOrRepeat) return KeyEventResult.ignored;
-
-    // Exit early if we aren't handling the key
-    if (![
-      LogicalKeyboardKey.arrowDown,
-      LogicalKeyboardKey.arrowUp,
-      LogicalKeyboardKey.arrowLeft,
-      LogicalKeyboardKey.arrowRight,
-    ].contains(event.logicalKey)) return KeyEventResult.ignored;
-
-    // If there is no selected node, choose the first one.
-    if (widget.selectionNotifier.value.node == null) {
-      widget.selectionNotifier.value = Selection(
-        node: _data[0],
-        nodeIndex: 0,
-      );
-    }
-
-    final selection = widget.selectionNotifier.value;
-    assert(selection.node == _data[selection.nodeIndex!]);
-
-    if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
-      _moveSelection(ScrollKind.down, scrollController, constraints.maxHeight);
-    } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
-      _moveSelection(ScrollKind.up, scrollController, constraints.maxHeight);
-    } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
-      // On left arrow collapse the row if it is expanded. If it is not, move
-      // selection to its parent.
-      if (selection.node!.isExpanded) {
-        _toggleNode(selection.node!);
-      } else {
-        _moveSelection(
-          ScrollKind.parent,
-          scrollController,
-          constraints.maxHeight,
-        );
-      }
-    } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
-      // On right arrow expand the row if possible, otherwise move selection down.
-      if (selection.node!.isExpandable && !selection.node!.isExpanded) {
-        _toggleNode(selection.node!);
-      } else {
-        _moveSelection(
-          ScrollKind.down,
-          scrollController,
-          constraints.maxHeight,
-        );
-      }
-    }
-
-    return KeyEventResult.handled;
-  }
-
-  void _moveSelection(
-    ScrollKind scrollKind,
-    ScrollController scrollController,
-    double viewportHeight,
-  ) {
-    // get the index of the first item fully visible in the viewport
-    final firstItemIndex = (scrollController.offset / defaultRowHeight).ceil();
-
-    // '-2' in the following calculations is needed because the header row
-    // occupies space in the viewport so we must subtract 1 for that, and we
-    // subtract 1 to account for the fact that a partial row could be displayed
-    // at the top and bottom of the view.
-    final minCompleteItemsInView =
-        max((viewportHeight / defaultRowHeight).floor() - 2, 0);
-    final lastItemIndex = firstItemIndex + minCompleteItemsInView - 1;
-    late int newSelectedNodeIndex;
-
-    final selectionValue = widget.selectionNotifier.value;
-    final selectedNodeIndex = selectionValue.nodeIndex;
-    switch (scrollKind) {
-      case ScrollKind.down:
-        newSelectedNodeIndex = min(selectedNodeIndex! + 1, _data.length - 1);
-        break;
-      case ScrollKind.up:
-        newSelectedNodeIndex = max(selectedNodeIndex! - 1, 0);
-        break;
-      case ScrollKind.parent:
-        newSelectedNodeIndex = selectionValue.node!.parent != null
-            ? max(_data.indexOf(selectionValue.node!.parent!), 0)
-            : 0;
-        break;
-    }
-
-    final newSelectedNode = _data[newSelectedNodeIndex];
-    final isBelowViewport = newSelectedNodeIndex > lastItemIndex;
-    final isAboveViewport = newSelectedNodeIndex < firstItemIndex;
-
-    if (isBelowViewport) {
-      // Scroll so selected row is the last full item displayed in the viewport.
-      // To do this we need to be showing the (minCompleteItemsInView + 1)
-      // previous item  at the top.
-      unawaited(
-        scrollController.animateTo(
-          (newSelectedNodeIndex - minCompleteItemsInView + 1) *
-              defaultRowHeight,
-          duration: defaultDuration,
-          curve: defaultCurve,
-        ),
-      );
-    } else if (isAboveViewport) {
-      unawaited(
-        scrollController.animateTo(
-          newSelectedNodeIndex * defaultRowHeight,
-          duration: defaultDuration,
-          curve: defaultCurve,
-        ),
-      );
-    }
-
-    // We do not need to scroll into view here because we have manually
-    // managed the scrolling in the above checks for `isBelowViewport` and
-    // `isAboveViewport`.
-    widget.selectionNotifier.value = Selection(
-      node: newSelectedNode,
-      nodeIndex: newSelectedNodeIndex,
-    );
-  }
-}
-
 // TODO(kenz): https://github.com/flutter/devtools/issues/1522. The table code
 // needs to be refactored to support flexible column widths.
-class _Table<T> extends StatefulWidget {
-  const _Table({
+@visibleForTesting
+class DevToolsTable<T> extends StatefulWidget {
+  const DevToolsTable({
     Key? key,
     required this.tableController,
     required this.columnWidths,
@@ -900,16 +105,27 @@
   final bool enableHoverHandling;
 
   @override
-  _TableState<T> createState() => _TableState<T>();
+  DevToolsTableState<T> createState() => DevToolsTableState<T>();
 }
 
-class _TableState<T> extends State<_Table<T>> with AutoDisposeMixin {
+@visibleForTesting
+class DevToolsTableState<T> extends State<DevToolsTable<T>>
+    with AutoDisposeMixin {
   late LinkedScrollControllerGroup _linkedHorizontalScrollControllerGroup;
   late ScrollController scrollController;
   late ScrollController pinnedScrollController;
 
   late List<T> _data;
 
+  /// An adjusted copy of [widget.columnWidths] where any variable width columns
+  /// may be increased so that the sum of all column widths equals the available
+  /// screen space.
+  ///
+  /// This must be calculated where we have access to the Flutter view
+  /// constraints (e.g. the [LayoutBuilder] below).
+  @visibleForTesting
+  late List<double> adjustedColumnWidths;
+
   @override
   void initState() {
     super.initState();
@@ -925,10 +141,12 @@
     scrollController = widget.tableController.verticalScrollController!;
 
     pinnedScrollController = ScrollController();
+
+    adjustedColumnWidths = List.of(widget.columnWidths);
   }
 
   @override
-  void didUpdateWidget(covariant _Table<T> oldWidget) {
+  void didUpdateWidget(covariant DevToolsTable<T> oldWidget) {
     super.didUpdateWidget(oldWidget);
 
     final notifiersChanged = widget.tableController.tableData !=
@@ -940,6 +158,8 @@
       cancelListeners();
       _initDataAndAddListeners();
     }
+
+    adjustedColumnWidths = List.of(widget.columnWidths);
   }
 
   void _initDataAndAddListeners() {
@@ -1025,8 +245,8 @@
     super.dispose();
   }
 
-  /// The width of all columns in the table, with additional padding.
-  double get tableWidth {
+  /// The width of all columns in the table with additional padding.
+  double get _tableWidthForOriginalColumns {
     var tableWidth = 2 * defaultSpacing;
     final numColumnGroupSpacers =
         widget.tableController.columnGroups?.numSpacers ?? 0;
@@ -1040,6 +260,80 @@
     return tableWidth;
   }
 
+  /// Modifies [adjustedColumnWidths] so that any available view space greater
+  /// than [_tableWidthForOriginalColumns] is distributed evenly across variable
+  /// width columns.
+  void _adjustColumnWidthsForViewSize(double viewWidth) {
+    final extraSpace = viewWidth - _tableWidthForOriginalColumns;
+    if (extraSpace <= 0) {
+      adjustedColumnWidths = List.of(widget.columnWidths);
+      return;
+    }
+
+    final adjustedColumnWidthsByIndex = <int, double>{};
+
+    /// Helper method to evenly distribute [space] among the columns at
+    /// [columnIndices].
+    ///
+    /// This method stores the adjusted width values in
+    /// [adjustedColumnWidthsByIndex].
+    void evenlyDistributeColumnSizes(List<int> columnIndices, double space) {
+      final targetSize = space / columnIndices.length;
+
+      var largestColumnIndex = -1;
+      var largestColumnWidth = 0.0;
+      for (final index in columnIndices) {
+        final columnWidth = widget.columnWidths[index];
+        if (columnWidth >= largestColumnWidth) {
+          largestColumnIndex = index;
+          largestColumnWidth = columnWidth;
+        }
+      }
+      if (targetSize < largestColumnWidth) {
+        // We do not have enough extra space to evenly distribute to all
+        // columns. Remove the largest column and recurse.
+        adjustedColumnWidthsByIndex[largestColumnIndex] = largestColumnWidth;
+        final newColumnIndices = List.of(columnIndices)
+          ..remove(largestColumnIndex);
+        return evenlyDistributeColumnSizes(
+          newColumnIndices,
+          space - largestColumnWidth,
+        );
+      }
+
+      for (int i = 0; i < columnIndices.length; i++) {
+        final columnIndex = columnIndices[i];
+        adjustedColumnWidthsByIndex[columnIndex] = targetSize;
+      }
+    }
+
+    final variableWidthColumnIndices = <int>[];
+    var sumVariableWidthColumnSizes = 0.0;
+    for (int i = 0; i < widget.tableController.columns.length; i++) {
+      final column = widget.tableController.columns[i];
+      if (column.fixedWidthPx == null) {
+        variableWidthColumnIndices.add(i);
+        sumVariableWidthColumnSizes += widget.columnWidths[i];
+      }
+    }
+    final totalVariableWidthColumnSpace =
+        sumVariableWidthColumnSizes + extraSpace;
+
+    evenlyDistributeColumnSizes(
+      variableWidthColumnIndices,
+      totalVariableWidthColumnSpace,
+    );
+
+    adjustedColumnWidths.clear();
+    for (int i = 0; i < widget.columnWidths.length; i++) {
+      final originalWidth = widget.columnWidths[i];
+      final isVariableWidthColumn = variableWidthColumnIndices.contains(i);
+      adjustedColumnWidths.add(
+        isVariableWidthColumn ? adjustedColumnWidthsByIndex[i]! : originalWidth,
+      );
+    }
+  }
+
   double _pinnedDataHeight(BoxConstraints tableConstraints) => min(
         widget.rowItemExtent! * pinnedData.length,
         tableConstraints.maxHeight / 2,
@@ -1056,7 +350,7 @@
     var maxHeight = tableConstraints.maxHeight;
     final columnHeadersCount = showColumnGroupHeader ? 2 : 1;
     maxHeight -= columnHeadersCount *
-        (areaPaneHeaderHeight +
+        (defaultHeaderHeight +
             (widget.tallHeaders ? scaleByFontFactor(densePadding) : 0.0));
 
     if (pinnedData.isNotEmpty) {
@@ -1072,7 +366,7 @@
       context: context,
       linkedScrollControllerGroup: _linkedHorizontalScrollControllerGroup,
       index: index,
-      columnWidths: widget.columnWidths,
+      columnWidths: adjustedColumnWidths,
       isPinned: isPinned,
       enableHoverHandling: widget.enableHoverHandling,
     );
@@ -1105,12 +399,11 @@
     // TODO(kenz): add horizontal scrollbar.
     return LayoutBuilder(
       builder: (context, constraints) {
+        final viewWidth = constraints.maxWidth;
+        _adjustColumnWidthsForViewSize(viewWidth);
         return SelectionArea(
           child: SizedBox(
-            width: max(
-              constraints.widthConstraints().maxWidth,
-              tableWidth,
-            ),
+            width: max(viewWidth, _tableWidthForOriginalColumns),
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.stretch,
               children: [
@@ -1119,7 +412,7 @@
                     linkedScrollControllerGroup:
                         _linkedHorizontalScrollControllerGroup,
                     columnGroups: columnGroups,
-                    columnWidths: widget.columnWidths,
+                    columnWidths: adjustedColumnWidths,
                     sortColumn: sortColumn,
                     sortDirection: tableUiState.sortDirection,
                     secondarySortColumn:
@@ -1134,7 +427,7 @@
                       _linkedHorizontalScrollControllerGroup,
                   columns: widget.tableController.columns,
                   columnGroups: columnGroups,
-                  columnWidths: widget.columnWidths,
+                  columnWidths: adjustedColumnWidths,
                   sortColumn: sortColumn,
                   sortDirection: tableUiState.sortDirection,
                   secondarySortColumn:
@@ -1199,815 +492,3 @@
     );
   }
 }
-
-/// If a [ColumnData] implements this interface, it can override how that cell
-/// is rendered.
-abstract class ColumnRenderer<T> {
-  /// Render the given [data] to a [Widget].
-  ///
-  /// This method can return `null` to indicate that the default rendering
-  /// should be used instead.
-  /// `isRowHovered` is only used when `enableHoverHandling` is `true` on the table
-  /// that this column belongs to.
-  Widget? build(
-    BuildContext context,
-    T data, {
-    bool isRowSelected = false,
-    bool isRowHovered = false,
-    VoidCallback? onPressed,
-  });
-}
-
-/// If a [ColumnData] implements this interface, it can override how that column
-/// header is rendered.
-abstract class ColumnHeaderRenderer<T> {
-  /// Render the column header to a [Widget].
-  ///
-  /// This method can return `null` to indicate that the default rendering
-  /// should be used instead.
-  Widget? buildHeader(
-    BuildContext context,
-    Widget Function() defaultHeaderRenderer,
-  );
-}
-
-/// Presents a [node] as a row in a table.
-///
-/// When the given [node] is null, this widget will instead present
-/// column headings.
-@visibleForTesting
-class TableRow<T> extends StatefulWidget {
-  /// Constructs a [TableRow] that presents the column values for
-  /// [node].
-  const TableRow({
-    Key? key,
-    required this.linkedScrollControllerGroup,
-    required this.node,
-    required this.columns,
-    required this.columnWidths,
-    required this.onPressed,
-    this.columnGroups,
-    this.backgroundColor,
-    this.expandableColumn,
-    this.isExpanded = false,
-    this.isExpandable = false,
-    this.isSelected = false,
-    this.isShown = true,
-    this.enableHoverHandling = false,
-    this.displayTreeGuidelines = false,
-    this.searchMatchesNotifier,
-    this.activeSearchMatchNotifier,
-  })  : sortColumn = null,
-        sortDirection = null,
-        secondarySortColumn = null,
-        onSortChanged = null,
-        _rowType = _TableRowType.data,
-        tall = false,
-        super(key: key);
-
-  /// Constructs a [TableRow] that is empty.
-  const TableRow.filler({
-    Key? key,
-    required this.linkedScrollControllerGroup,
-    required this.columns,
-    required this.columnWidths,
-    this.columnGroups,
-    this.backgroundColor,
-  })  : node = null,
-        isExpanded = false,
-        isExpandable = false,
-        isSelected = false,
-        onPressed = null,
-        expandableColumn = null,
-        isShown = true,
-        sortColumn = null,
-        sortDirection = null,
-        secondarySortColumn = null,
-        onSortChanged = null,
-        searchMatchesNotifier = null,
-        activeSearchMatchNotifier = null,
-        tall = false,
-        enableHoverHandling = false,
-        displayTreeGuidelines = false,
-        _rowType = _TableRowType.filler,
-        super(key: key);
-
-  /// Constructs a [TableRow] that presents the column titles instead
-  /// of any [node].
-  const TableRow.tableColumnHeader({
-    Key? key,
-    required this.linkedScrollControllerGroup,
-    required this.columns,
-    required this.columnWidths,
-    required this.columnGroups,
-    required this.sortColumn,
-    required this.sortDirection,
-    required this.onSortChanged,
-    this.secondarySortColumn,
-    this.onPressed,
-    this.tall = false,
-    this.backgroundColor,
-  })  : node = null,
-        isExpanded = false,
-        isExpandable = false,
-        isSelected = false,
-        expandableColumn = null,
-        isShown = true,
-        searchMatchesNotifier = null,
-        activeSearchMatchNotifier = null,
-        displayTreeGuidelines = false,
-        enableHoverHandling = false,
-        _rowType = _TableRowType.columnHeader,
-        super(key: key);
-
-  /// Constructs a [TableRow] that presents column group titles instead of any
-  /// [node].
-  const TableRow.tableColumnGroupHeader({
-    Key? key,
-    required this.linkedScrollControllerGroup,
-    required this.columnGroups,
-    required this.columnWidths,
-    required this.sortColumn,
-    required this.sortDirection,
-    required this.onSortChanged,
-    this.secondarySortColumn,
-    this.onPressed,
-    this.tall = false,
-    this.backgroundColor,
-  })  : node = null,
-        isExpanded = false,
-        isExpandable = false,
-        isSelected = false,
-        expandableColumn = null,
-        columns = const [],
-        isShown = true,
-        searchMatchesNotifier = null,
-        activeSearchMatchNotifier = null,
-        displayTreeGuidelines = false,
-        enableHoverHandling = false,
-        _rowType = _TableRowType.columnGroupHeader,
-        super(key: key);
-
-  final LinkedScrollControllerGroup linkedScrollControllerGroup;
-
-  final T? node;
-
-  final List<ColumnData<T>> columns;
-
-  final List<ColumnGroup>? columnGroups;
-
-  final ItemSelectedCallback<T>? onPressed;
-
-  final List<double> columnWidths;
-
-  final bool isSelected;
-
-  final _TableRowType _rowType;
-
-  final bool tall;
-
-  final bool enableHoverHandling;
-
-  /// Which column, if any, should show expansion affordances
-  /// and nested rows.
-  final ColumnData<T>? expandableColumn;
-
-  /// Whether or not this row is expanded.
-  ///
-  /// This dictates the orientation of the expansion arrow
-  /// that is drawn in the [expandableColumn].
-  ///
-  /// Only meaningful if [isExpanded] is true.
-  final bool isExpanded;
-
-  /// Whether or not this row can be expanded.
-  ///
-  /// This dictates whether an expansion arrow is
-  /// drawn in the [expandableColumn].
-  final bool isExpandable;
-
-  /// Whether or not this row is shown.
-  ///
-  /// When the value is toggled, this row will appear or disappear.
-  final bool isShown;
-
-  /// The background color of the row.
-  ///
-  /// If null, defaults to `Theme.of(context).canvasColor`.
-  final Color? backgroundColor;
-
-  final ColumnData<T>? sortColumn;
-
-  final SortDirection? sortDirection;
-
-  final ColumnData<T>? secondarySortColumn;
-
-  final Function(
-    ColumnData<T> column,
-    SortDirection direction, {
-    ColumnData<T>? secondarySortColumn,
-  })? onSortChanged;
-
-  final ValueListenable<List<T>>? searchMatchesNotifier;
-
-  final ValueListenable<T?>? activeSearchMatchNotifier;
-
-  final bool displayTreeGuidelines;
-
-  @override
-  State<TableRow<T>> createState() => _TableRowState<T>();
-}
-
-class _TableRowState<T> extends State<TableRow<T>>
-    with
-        TickerProviderStateMixin,
-        CollapsibleAnimationMixin,
-        AutoDisposeMixin,
-        SearchableMixin {
-  Key? contentKey;
-
-  late ScrollController scrollController;
-
-  bool isSearchMatch = false;
-
-  bool isActiveSearchMatch = false;
-
-  bool isHovering = false;
-
-  @override
-  void initState() {
-    super.initState();
-    contentKey = ValueKey(this);
-    scrollController = widget.linkedScrollControllerGroup.addAndGet();
-    _initSearchListeners();
-  }
-
-  @override
-  void didUpdateWidget(TableRow<T> oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    setExpanded(widget.isExpanded);
-    if (oldWidget.linkedScrollControllerGroup !=
-        widget.linkedScrollControllerGroup) {
-      scrollController.dispose();
-      scrollController = widget.linkedScrollControllerGroup.addAndGet();
-    }
-
-    cancelListeners();
-    _initSearchListeners();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    scrollController.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final node = widget.node;
-    final widgetOnPressed = widget.onPressed;
-
-    Function()? onPressed;
-    if (node != null && widgetOnPressed != null) {
-      onPressed = () => widgetOnPressed(node);
-    }
-
-    final row = tableRowFor(
-      context,
-      onPressed: onPressed,
-    );
-
-    final box = SizedBox(
-      height: widget._rowType == _TableRowType.data
-          ? defaultRowHeight
-          : areaPaneHeaderHeight +
-              (widget.tall ? scaleByFontFactor(densePadding) : 0.0),
-      child: Material(
-        color: _searchAwareBackgroundColor(),
-        child: onPressed != null
-            ? InkWell(
-                canRequestFocus: false,
-                key: contentKey,
-                onTap: onPressed,
-                child: row,
-              )
-            : row,
-      ),
-    );
-    return box;
-  }
-
-  void _initSearchListeners() {
-    if (widget.searchMatchesNotifier != null) {
-      searchMatches = widget.searchMatchesNotifier!.value;
-      isSearchMatch = searchMatches.contains(widget.node);
-      addAutoDisposeListener(widget.searchMatchesNotifier, () {
-        final isPreviousMatch = searchMatches.contains(widget.node);
-        searchMatches = widget.searchMatchesNotifier!.value;
-        final isNewMatch = searchMatches.contains(widget.node);
-
-        // We only want to rebuild the row if it the match status has changed.
-        if (isPreviousMatch != isNewMatch) {
-          setState(() {
-            isSearchMatch = isNewMatch;
-          });
-        }
-      });
-    }
-
-    if (widget.activeSearchMatchNotifier != null) {
-      activeSearchMatch = widget.activeSearchMatchNotifier!.value;
-      isActiveSearchMatch = activeSearchMatch == widget.node;
-      addAutoDisposeListener(widget.activeSearchMatchNotifier, () {
-        final isPreviousActiveSearchMatch = activeSearchMatch == widget.node;
-        activeSearchMatch = widget.activeSearchMatchNotifier!.value;
-        final isNewActiveSearchMatch = activeSearchMatch == widget.node;
-
-        // We only want to rebuild the row if it the match status has changed.
-        if (isPreviousActiveSearchMatch != isNewActiveSearchMatch) {
-          setState(() {
-            isActiveSearchMatch = isNewActiveSearchMatch;
-          });
-        }
-      });
-    }
-  }
-
-  Color _searchAwareBackgroundColor() {
-    final colorScheme = Theme.of(context).colorScheme;
-    final backgroundColor = widget.backgroundColor ?? colorScheme.surface;
-    if (widget.isSelected) {
-      return colorScheme.selectedRowBackgroundColor;
-    }
-    final searchAwareBackgroundColor = isSearchMatch
-        ? Color.alphaBlend(
-            isActiveSearchMatch
-                ? activeSearchMatchColorOpaque
-                : searchMatchColorOpaque,
-            backgroundColor,
-          )
-        : backgroundColor;
-    return searchAwareBackgroundColor;
-  }
-
-  Alignment _alignmentFor(ColumnData<T> column) {
-    switch (column.alignment) {
-      case ColumnAlignment.center:
-        return Alignment.center;
-      case ColumnAlignment.right:
-        return Alignment.centerRight;
-      case ColumnAlignment.left:
-      default:
-        return Alignment.centerLeft;
-    }
-  }
-
-  /// Presents the content of this row.
-  Widget tableRowFor(BuildContext context, {VoidCallback? onPressed}) {
-    Widget columnFor(ColumnData<T> column, double columnWidth) {
-      Widget? content;
-      final theme = Theme.of(context);
-      final node = widget.node;
-      if (widget._rowType == _TableRowType.filler) {
-        content = const SizedBox.shrink();
-      } else if (widget._rowType == _TableRowType.columnHeader) {
-        Widget defaultHeaderRenderer() => _ColumnHeader(
-              column: column,
-              isSortColumn: column == widget.sortColumn,
-              secondarySortColumn: widget.secondarySortColumn,
-              sortDirection: widget.sortDirection!,
-              onSortChanged: widget.onSortChanged,
-            );
-
-        // ignore: avoid-unrelated-type-assertions, false positive.
-        if (column is ColumnHeaderRenderer) {
-          content = (column as ColumnHeaderRenderer)
-              .buildHeader(context, defaultHeaderRenderer);
-        }
-        // If ColumnHeaderRenderer.build returns null, fall back to the default
-        // rendering.
-        content ??= defaultHeaderRenderer();
-      } else if (node != null) {
-        // TODO(kenz): clean up and pull all this code into _ColumnDataRow
-        // widget class.
-        final padding = column.getNodeIndentPx(node);
-        assert(padding >= 0);
-        // ignore: avoid-unrelated-type-assertions, false positive.
-        if (column is ColumnRenderer) {
-          content = (column as ColumnRenderer).build(
-            context,
-            node,
-            isRowSelected: widget.isSelected,
-            isRowHovered: isHovering,
-            onPressed: onPressed,
-          );
-        }
-        // If ColumnRenderer.build returns null, fall back to the default
-        // rendering.
-        content ??= Text.rich(
-          TextSpan(
-            text: column.getDisplayValue(node),
-            children: [
-              if (column.getCaption(node) != null)
-                TextSpan(
-                  text: ' ${column.getCaption(node)}',
-                  style: const TextStyle(
-                    fontStyle: FontStyle.italic,
-                  ),
-                ),
-            ],
-            style: column.contentTextStyle(
-              context,
-              node,
-              isSelected: widget.isSelected,
-            ),
-          ),
-          maxLines: 1,
-          overflow: TextOverflow.ellipsis,
-          textAlign: column.contentTextAlignment,
-        );
-
-        final tooltip = column.getTooltip(node);
-        final richTooltip = column.getRichTooltip(node, context);
-        if (tooltip.isNotEmpty || richTooltip != null) {
-          content = DevToolsTooltip(
-            message: richTooltip == null ? tooltip : null,
-            richMessage: richTooltip,
-            waitDuration: tooltipWaitLong,
-            child: content,
-          );
-        }
-
-        if (column == widget.expandableColumn) {
-          final expandIndicator = widget.isExpandable
-              ? ValueListenableBuilder(
-                  valueListenable: expandController,
-                  builder: (context, _, __) {
-                    return RotationTransition(
-                      turns: expandArrowAnimation,
-                      child: Icon(
-                        Icons.expand_more,
-                        color: theme.colorScheme.onSurface,
-                        size: defaultIconSize,
-                      ),
-                    );
-                  },
-                )
-              : SizedBox(width: defaultIconSize, height: defaultIconSize);
-          content = Row(
-            mainAxisSize: MainAxisSize.min,
-            children: [
-              expandIndicator,
-              Expanded(child: content),
-            ],
-          );
-        }
-        content = Padding(
-          padding: EdgeInsets.only(left: padding),
-          child: ClipRect(
-            child: content,
-          ),
-        );
-      } else {
-        throw Exception(
-          'Expected a non-null node for this table column, but node == null.',
-        );
-      }
-
-      content = SizedBox(
-        width: columnWidth,
-        child: Align(
-          alignment: _alignmentFor(column),
-          child: content,
-        ),
-      );
-      if (widget.displayTreeGuidelines &&
-          node != null &&
-          node is TreeNode &&
-          column is TreeColumnData) {
-        content = CustomPaint(
-          painter: _RowGuidelinePainter(
-            node.level,
-            theme.colorScheme,
-          ),
-          child: content,
-        );
-      }
-      return content;
-    }
-
-    if (widget._rowType == _TableRowType.columnGroupHeader) {
-      final groups = widget.columnGroups!;
-      return _ColumnGroupHeaderRow(
-        groups: groups,
-        columnWidths: widget.columnWidths,
-        scrollController: scrollController,
-      );
-    }
-
-    final rowDisplayParts = <_TableRowPartDisplayType>[];
-    final groups = widget.columnGroups;
-    if (groups != null && groups.isNotEmpty) {
-      for (int i = 0; i < groups.length; i++) {
-        final groupParts = List.generate(
-          groups[i].range.size as int,
-          (index) => _TableRowPartDisplayType.column,
-        ).joinWith(_TableRowPartDisplayType.columnSpacer);
-        rowDisplayParts.addAll(groupParts);
-        if (i < groups.length - 1) {
-          rowDisplayParts.add(_TableRowPartDisplayType.columnGroupSpacer);
-        }
-      }
-    } else {
-      final parts = List.generate(
-        widget.columns.length,
-        (_) => _TableRowPartDisplayType.column,
-      ).joinWith(_TableRowPartDisplayType.columnSpacer);
-      rowDisplayParts.addAll(parts);
-    }
-
-    // Maps the indices from [rowDisplayParts] to the corresponding index of
-    // each column in [widget.columns].
-    final columnIndexMap = <int, int>{};
-    // Add scope to guarantee [columnIndexTracker] is not used outside of this
-    // block.
-    {
-      var columnIndexTracker = 0;
-      for (int i = 0; i < rowDisplayParts.length; i++) {
-        final type = rowDisplayParts[i];
-        if (type == _TableRowPartDisplayType.column) {
-          columnIndexMap[i] = columnIndexTracker;
-          columnIndexTracker++;
-        }
-      }
-    }
-
-    Widget rowContent = Padding(
-      padding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
-      child: ListView.builder(
-        scrollDirection: Axis.horizontal,
-        controller: scrollController,
-        itemCount: widget.columns.length + widget.columns.numSpacers,
-        itemBuilder: (context, int i) {
-          final displayTypeForIndex = rowDisplayParts[i];
-          switch (displayTypeForIndex) {
-            case _TableRowPartDisplayType.column:
-              final index = columnIndexMap[i]!;
-              return columnFor(
-                widget.columns[index],
-                widget.columnWidths[index],
-              );
-            case _TableRowPartDisplayType.columnSpacer:
-              return const SizedBox(
-                width: columnSpacing,
-                child: VerticalDivider(width: columnSpacing),
-              );
-            case _TableRowPartDisplayType.columnGroupSpacer:
-              return const _ColumnGroupSpacer();
-          }
-        },
-      ),
-    );
-
-    if (widget.enableHoverHandling) {
-      rowContent = MouseRegion(
-        onEnter: (_) => setState(() => isHovering = true),
-        onExit: (_) => setState(() => isHovering = false),
-        child: rowContent,
-      );
-    }
-
-    if (widget._rowType == _TableRowType.columnHeader) {
-      return OutlineDecoration.onlyBottom(child: rowContent);
-    }
-    return rowContent;
-  }
-
-  @override
-  bool get isExpanded => widget.isExpanded;
-
-  @override
-  void onExpandChanged(bool expanded) {}
-
-  @override
-  bool shouldShow() => widget.isShown;
-}
-
-class _RowGuidelinePainter extends CustomPainter {
-  _RowGuidelinePainter(this.level, this.colorScheme);
-
-  static const _treeGuidelineColors = [
-    Color(0xFF13B9FD),
-    Color(0xFF5BC43B),
-  ];
-
-  final int level;
-  final ColorScheme colorScheme;
-
-  @override
-  void paint(Canvas canvas, Size size) {
-    for (int i = 0; i < level; ++i) {
-      final currentX = i * TreeColumnData.treeToggleWidth + defaultIconSize / 2;
-      // Draw a vertical line for each tick identifying a connection between
-      // an ancestor of this node and some other node in the tree.
-      canvas.drawLine(
-        Offset(currentX, 0.0),
-        Offset(currentX, defaultRowHeight),
-        Paint()
-          ..color = _treeGuidelineColors[i % _treeGuidelineColors.length]
-          ..strokeWidth = 1.0,
-      );
-    }
-  }
-
-  @override
-  bool shouldRepaint(CustomPainter oldDelegate) {
-    if (oldDelegate is _RowGuidelinePainter) {
-      return oldDelegate.colorScheme.isLight != colorScheme.isLight;
-    }
-    return true;
-  }
-}
-
-class _ColumnHeader<T> extends StatelessWidget {
-  const _ColumnHeader({
-    Key? key,
-    required this.column,
-    required this.isSortColumn,
-    required this.sortDirection,
-    required this.onSortChanged,
-    this.secondarySortColumn,
-  }) : super(key: key);
-
-  final ColumnData<T> column;
-
-  final ColumnData<T>? secondarySortColumn;
-
-  final bool isSortColumn;
-
-  final SortDirection sortDirection;
-
-  final Function(
-    ColumnData<T> column,
-    SortDirection direction, {
-    ColumnData<T>? secondarySortColumn,
-  })? onSortChanged;
-
-  @override
-  Widget build(BuildContext context) {
-    late Widget content;
-    final title = Text(
-      column.title,
-      overflow: TextOverflow.ellipsis,
-      textAlign: column.headerAlignment,
-    );
-
-    final headerContent = Row(
-      mainAxisAlignment: column.mainAxisAlignment,
-      children: [
-        if (isSortColumn && column.supportsSorting) ...[
-          Icon(
-            sortDirection == SortDirection.ascending
-                ? Icons.expand_less
-                : Icons.expand_more,
-            size: defaultIconSize,
-          ),
-          const SizedBox(width: densePadding),
-        ],
-        Expanded(
-          child: column.titleTooltip != null
-              ? DevToolsTooltip(
-                  message: column.titleTooltip,
-                  padding: const EdgeInsets.all(denseSpacing),
-                  child: title,
-                )
-              : title,
-        ),
-      ],
-    );
-
-    content = column.includeHeader
-        ? InkWell(
-            canRequestFocus: false,
-            onTap: column.supportsSorting
-                ? () => _handleSortChange(
-                      column,
-                      secondarySortColumn: secondarySortColumn,
-                    )
-                : null,
-            child: headerContent,
-          )
-        : headerContent;
-    return content;
-  }
-
-  void _handleSortChange(
-    ColumnData<T> columnData, {
-    ColumnData<T>? secondarySortColumn,
-  }) {
-    SortDirection direction;
-    if (isSortColumn) {
-      direction = sortDirection.reverse();
-    } else if (columnData.numeric) {
-      direction = SortDirection.descending;
-    } else {
-      direction = SortDirection.ascending;
-    }
-    onSortChanged?.call(
-      columnData,
-      direction,
-      secondarySortColumn: secondarySortColumn,
-    );
-  }
-}
-
-class _ColumnGroupHeaderRow extends StatelessWidget {
-  const _ColumnGroupHeaderRow({
-    required this.groups,
-    required this.columnWidths,
-    required this.scrollController,
-    Key? key,
-  }) : super(key: key);
-
-  final List<ColumnGroup> groups;
-
-  final List<double> columnWidths;
-
-  final ScrollController scrollController;
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      padding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
-      decoration: BoxDecoration(
-        border: Border(
-          bottom: defaultBorderSide(Theme.of(context)),
-        ),
-      ),
-      child: ListView.builder(
-        scrollDirection: Axis.horizontal,
-        controller: scrollController,
-        itemCount: groups.length + groups.numSpacers,
-        itemBuilder: (context, int i) {
-          if (i % 2 == 1) {
-            return const _ColumnGroupSpacer();
-          }
-
-          final group = groups[i ~/ 2];
-          final groupRange = group.range;
-          double groupWidth = 0.0;
-          for (int j = groupRange.begin as int; j < groupRange.end; j++) {
-            final columnWidth = columnWidths[j];
-            groupWidth += columnWidth;
-            if (j < groupRange.end - 1) {
-              groupWidth += columnSpacing;
-            }
-          }
-          return Container(
-            alignment: Alignment.center,
-            width: groupWidth,
-            child: group.title,
-          );
-        },
-      ),
-    );
-  }
-}
-
-class _ColumnGroupSpacer extends StatelessWidget {
-  const _ColumnGroupSpacer({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      padding: const EdgeInsets.symmetric(
-        horizontal: (columnGroupSpacingWithPadding - columnGroupSpacing) / 2,
-      ),
-      child: Container(
-        width: columnGroupSpacing,
-        decoration: BoxDecoration(
-          gradient: LinearGradient(
-            colors: [
-              Colors.black,
-              Theme.of(context).focusColor,
-              Colors.black,
-            ],
-          ),
-        ),
-      ),
-    );
-  }
-}
-
-enum _TableRowType {
-  data,
-  columnHeader,
-  columnGroupHeader,
-  filler,
-}
-
-enum _TableRowPartDisplayType {
-  column,
-  columnSpacer,
-  columnGroupSpacer,
-}
diff --git a/packages/devtools_app/lib/src/shared/table/table_controller.dart b/packages/devtools_app/lib/src/shared/table/table_controller.dart
index b280756..9b6de6a 100644
--- a/packages/devtools_app/lib/src/shared/table/table_controller.dart
+++ b/packages/devtools_app/lib/src/shared/table/table_controller.dart
@@ -1,5 +1,7 @@
 import 'dart:collection';
+import 'dart:math';
 
+import 'package:collection/collection.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
@@ -39,7 +41,7 @@
 
   /// Determines how elements that request to be pinned are displayed.
   ///
-  /// Defaults to [FlatTablePinBehavior.none], which disables pinnning.
+  /// Defaults to [FlatTablePinBehavior.none], which disables pinning.
   FlatTablePinBehavior pinBehavior;
 
   /// Whether the columns for this table should be sized so that the entire
@@ -132,7 +134,7 @@
     }
 
     if (!sizeColumnsToFit) {
-      columnWidths = computeColumnWidthsSizeToContent(data);
+      columnWidths = computeColumnWidthsSizeToContent();
     }
     _tableData.value = TableData<T>(
       data: data,
@@ -164,6 +166,8 @@
 
   late List<T> dataRoots;
 
+  late int maxTableDepth;
+
   @override
   void setData(List<T> data, String key) {
     dataRoots = data;
@@ -172,6 +176,12 @@
         root.expand();
       }
     }
+    // TODO(kenz): instead of using the maximum tree depth, consider using the
+    // maximum depth of expanded nodes.
+    maxTableDepth = dataRoots.map((root) => root.depth).fold(
+          0,
+          (currentMaxDepth, nextDepth) => max(currentMaxDepth, nextDepth),
+        );
 
     // Look up the UI state for [key], and sort accordingly.
     final uiState = _tableUiStateForKey(key);
@@ -215,7 +225,7 @@
 
   void setDataAndNotify({String? dataKey}) {
     final dataFlatList = buildFlatList(dataRoots);
-    columnWidths = computeColumnWidths(dataFlatList);
+    columnWidths = computeColumnWidths(maxTableDepth);
 
     _tableData.value = TableData<T>(
       data: dataFlatList,
diff --git a/packages/devtools_app/lib/src/shared/table/table_data.dart b/packages/devtools_app/lib/src/shared/table/table_data.dart
index bd10358..c577a18 100644
--- a/packages/devtools_app/lib/src/shared/table/table_data.dart
+++ b/packages/devtools_app/lib/src/shared/table/table_data.dart
@@ -22,8 +22,8 @@
 abstract class ColumnData<T> {
   ColumnData(
     this.title, {
-    this.titleTooltip,
     required double this.fixedWidthPx,
+    this.titleTooltip,
     this.alignment = ColumnAlignment.left,
     this.headerAlignment = TextAlign.left,
   }) : minWidthPx = null;
@@ -43,6 +43,7 @@
   /// Width of the column expressed as a fixed number of pixels.
   final double? fixedWidthPx;
 
+  /// The minimum width that should be used for a variable width column.
   final double? minWidthPx;
 
   /// How much to indent the data object by.
@@ -97,7 +98,7 @@
   }) {
     final theme = Theme.of(context);
     final textColor = getTextColor(dataObject) ?? theme.colorScheme.onSurface;
-    return theme.fixedFontStyle.copyWith(color: textColor);
+    return theme.regularTextStyleWithColor(textColor);
   }
 
   @override
@@ -202,7 +203,7 @@
           fixedWidthPx: scaleByFontFactor(columnWidth),
         );
 
-  static const _defaultTimeColumnWidth = 165.0;
+  static const _defaultTimeColumnWidth = 120.0;
 
   Duration Function(T)? timeProvider;
 
@@ -260,3 +261,87 @@
   String _percentDisplay(T dataObject) =>
       percent(percentAsDoubleProvider(dataObject));
 }
+
+/// Column that, for each row, shows a memory value and the percentage that the
+/// memory value is of the total memory for this data set.
+///
+/// Both memory and percentage are provided through callbacks [sizeProvider] and
+/// [percentAsDoubleProvider], respectively.
+///
+/// When [percentageOnly] is true, the memory value will be omitted, and only the
+/// percentage will be displayed.
+abstract class SizeAndPercentageColumn<T> extends ColumnData<T> {
+  SizeAndPercentageColumn({
+    required String title,
+    required this.percentAsDoubleProvider,
+    this.sizeProvider,
+    this.tooltipProvider,
+    this.richTooltipProvider,
+    this.secondaryCompare,
+    this.percentageOnly = false,
+    double columnWidth = _defaultMemoryColumnWidth,
+    super.titleTooltip,
+  }) : super(
+          title,
+          fixedWidthPx: scaleByFontFactor(columnWidth),
+        );
+
+  static const _defaultMemoryColumnWidth =
+      TimeAndPercentageColumn._defaultTimeColumnWidth;
+
+  int Function(T)? sizeProvider;
+
+  double Function(T) percentAsDoubleProvider;
+
+  String Function(T)? tooltipProvider;
+
+  RichTooltipBuilder<T>? richTooltipProvider;
+
+  Comparable Function(T)? secondaryCompare;
+
+  final bool percentageOnly;
+
+  @override
+  bool get numeric => true;
+
+  @override
+  int compare(T a, T b) {
+    final int result = super.compare(a, b);
+    if (result == 0 && secondaryCompare != null) {
+      return secondaryCompare!(a).compareTo(secondaryCompare!(b));
+    }
+    return result;
+  }
+
+  @override
+  double getValue(T dataObject) => percentageOnly
+      ? percentAsDoubleProvider(dataObject)
+      : sizeProvider!(dataObject).toDouble();
+
+  @override
+  String getDisplayValue(T dataObject) {
+    if (percentageOnly) return _percentDisplay(dataObject);
+    return _memoryAndPercentage(dataObject);
+  }
+
+  @override
+  String getTooltip(T dataObject) {
+    if (tooltipProvider != null) {
+      return tooltipProvider!(dataObject);
+    }
+    if (percentageOnly && sizeProvider != null) {
+      return _memoryAndPercentage(dataObject);
+    }
+    return '';
+  }
+
+  @override
+  InlineSpan? getRichTooltip(T dataObject, BuildContext context) =>
+      richTooltipProvider?.call(dataObject, context);
+
+  String _memoryAndPercentage(T dataObject) =>
+      '${prettyPrintBytes(sizeProvider!(dataObject), includeUnit: true)} (${_percentDisplay(dataObject)})';
+
+  String _percentDisplay(T dataObject) =>
+      percent(percentAsDoubleProvider(dataObject));
+}
diff --git a/packages/devtools_app/lib/src/shared/ui/drop_down_button.dart b/packages/devtools_app/lib/src/shared/ui/drop_down_button.dart
index d931b18..4b492cd 100644
--- a/packages/devtools_app/lib/src/shared/ui/drop_down_button.dart
+++ b/packages/devtools_app/lib/src/shared/ui/drop_down_button.dart
@@ -64,7 +64,6 @@
         child: RoundedDropDownButton<T>(
           isDense: isDense,
           isExpanded: isExpanded,
-          style: Theme.of(context).textTheme.bodyMedium,
           value: value,
           items: items?.map((e) => e.item).toList(),
           onChanged: _onChanged,
diff --git a/packages/devtools_app/lib/src/shared/ui/filter.dart b/packages/devtools_app/lib/src/shared/ui/filter.dart
index 7c4a26a..0d72027 100644
--- a/packages/devtools_app/lib/src/shared/ui/filter.dart
+++ b/packages/devtools_app/lib/src/shared/ui/filter.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.
 
+import 'package:codicon/codicon.dart';
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 
@@ -30,6 +31,8 @@
 
   final filteredData = ListValueNotifier<T>([]);
 
+  final useRegExp = ValueNotifier<bool>(false);
+
   // TODO(kenz): replace [Filter] class with a record when available.
   ValueListenable<Filter<T>> get activeFilter => _activeFilter;
 
@@ -46,7 +49,11 @@
   }) {
     _activeFilter.value = Filter(
       queryFilter: query != null
-          ? QueryFilter.parse(query, args: _queryFilterArgs)
+          ? QueryFilter.parse(
+              query,
+              args: _queryFilterArgs,
+              useRegExp: useRegExp.value,
+            )
           : QueryFilter.empty(args: _queryFilterArgs),
       toggleFilters: toggleFilters ?? _toggleFilters,
     );
@@ -76,6 +83,7 @@
     return queryFilterActive || toggleFilterActive;
   }
 
+  // TODO(kenz): de-dupe the filtering logic in overrides of this method.
   // TODO(kenz): refactor this so that `filterData` returns the filtered data
   // and does not have side effects other than filtering data. Add a
   // `onFilterApplied` method here that can be overridden to apply those
@@ -96,9 +104,11 @@
     }
     final toggleFilterTag = suffixList.join(',');
     final queryFilterTag = activeFilter.queryFilter.query.toLowerCase();
-    return [toggleFilterTag, queryFilterTag]
-        .where((e) => e.isNotEmpty)
-        .join(filterTagSeparator);
+    return [
+      toggleFilterTag,
+      queryFilterTag,
+      if (queryFilterTag.isNotEmpty && useRegExp.value) 'regexp',
+    ].where((e) => e.isNotEmpty).join(filterTagSeparator);
   }
 
   void _resetToDefaultFilter() {
@@ -154,6 +164,8 @@
 class _FilterDialogState<T> extends State<FilterDialog<T>>
     with AutoDisposeMixin {
   late final TextEditingController queryTextFieldController;
+  late bool useRegExp;
+  late bool pendingUseRegExp;
 
   @override
   void initState() {
@@ -161,6 +173,11 @@
     queryTextFieldController = TextEditingController(
       text: widget.controller.activeFilter.value.queryFilter.query,
     );
+    useRegExp = widget.controller.useRegExp.value;
+    addAutoDisposeListener(widget.controller.useRegExp, () {
+      useRegExp = widget.controller.useRegExp.value;
+    });
+    pendingUseRegExp = useRegExp;
   }
 
   @override
@@ -185,6 +202,17 @@
               autofocus: true,
               labelText: 'Filter Query',
               controller: queryTextFieldController,
+              additionalSuffixActions: [
+                DevToolsToggleButton(
+                  icon: Codicons.regex,
+                  message: 'Use regular expressions',
+                  outlined: false,
+                  isSelected: pendingUseRegExp,
+                  onPressed: () => setState(() {
+                    pendingUseRegExp = !pendingUseRegExp;
+                  }),
+                ),
+              ],
             ),
             const SizedBox(height: defaultSpacing),
             if (widget.queryInstructions != null) ...[
@@ -201,12 +229,14 @@
   }
 
   void _applyFilterChanges() {
-    widget.controller.setActiveFilter(
-      query: widget.includeQueryFilter
-          ? queryTextFieldController.value.text
-          : null,
-      toggleFilters: widget.controller._toggleFilters,
-    );
+    widget.controller
+      ..useRegExp.value = pendingUseRegExp
+      ..setActiveFilter(
+        query: widget.includeQueryFilter
+            ? queryTextFieldController.value.text
+            : null,
+        toggleFilters: widget.controller._toggleFilters,
+      );
   }
 
   void _resetFilters() {
@@ -280,14 +310,14 @@
 class QueryFilter {
   const QueryFilter._({
     this.filterArguments = const <String, QueryFilterArgument>{},
-    this.substrings = const [],
+    this.substringExpressions = const <Pattern>[],
     this.isEmpty = false,
   });
 
   factory QueryFilter.empty({required Map<String, QueryFilterArgument> args}) {
     return QueryFilter._(
       filterArguments: args,
-      substrings: <String>[],
+      substringExpressions: <Pattern>[],
       isEmpty: true,
     );
   }
@@ -295,6 +325,7 @@
   factory QueryFilter.parse(
     String query, {
     required Map<String, QueryFilterArgument> args,
+    required bool useRegExp,
   }) {
     if (query.isEmpty) {
       return QueryFilter.empty(args: args);
@@ -306,7 +337,7 @@
     }
 
     final partsBySpace = query.split(' ');
-    final substrings = <String>[];
+    final substringExpressions = <Pattern>[];
     for (final part in partsBySpace) {
       final querySeparatorIndex = part.indexOf(':');
       if (querySeparatorIndex != -1) {
@@ -316,12 +347,19 @@
             if (arg.matchesKey(part)) {
               arg.isNegative =
                   part.startsWith(QueryFilterArgument.negativePrefix);
-              arg.values = value.split(QueryFilterArgument.valueSeparator);
+              final valueStrings =
+                  value.split(QueryFilterArgument.valueSeparator);
+              arg.values = useRegExp
+                  ? valueStrings
+                      .map((v) => RegExp(v, caseSensitive: false))
+                      .toList()
+                  : valueStrings;
             }
           }
         }
       } else {
-        substrings.add(part);
+        substringExpressions
+            .add(useRegExp ? RegExp(part, caseSensitive: false) : part);
       }
     }
 
@@ -332,33 +370,35 @@
         break;
       }
     }
-    if (!validArgumentFilter && substrings.isEmpty) {
+    if (!validArgumentFilter && substringExpressions.isEmpty) {
       return QueryFilter.empty(args: args);
     }
 
     return QueryFilter._(
       filterArguments: args,
-      substrings: substrings,
+      substringExpressions: substringExpressions,
     );
   }
 
   final Map<String, QueryFilterArgument> filterArguments;
 
-  final List<String> substrings;
+  final List<Pattern> substringExpressions;
 
   final bool isEmpty;
 
   String get query => isEmpty
       ? ''
       : [
-          ...substrings,
+          ...substringExpressions.toStringList(),
           for (final arg in filterArguments.values) arg.display,
         ].join(' ').trim();
 }
 
-class QueryFilterArgument {
+class QueryFilterArgument<T> {
   QueryFilterArgument({
     required this.keys,
+    required this.dataValueProvider,
+    required this.substringMatch,
     this.values = const [],
     this.isNegative = false,
   });
@@ -369,13 +409,19 @@
 
   final List<String> keys;
 
-  List<String> values;
+  final String? Function(T data) dataValueProvider;
+
+  final bool substringMatch;
+
+  List<Pattern> values;
 
   bool isNegative;
 
-  String get display => values.isNotEmpty
-      ? '${isNegative ? negativePrefix : ''}${keys.first}:${values.join(valueSeparator)}'
-      : '';
+  String get display {
+    if (values.isEmpty) return '';
+    return '${isNegative ? negativePrefix : ''}${keys.first}:'
+        '${values.toStringList().join(valueSeparator)}';
+  }
 
   bool matchesKey(String query) {
     for (final key in keys) {
@@ -384,11 +430,12 @@
     return false;
   }
 
-  bool matchesValue(String? dataValue, {bool substringMatch = false}) {
+  bool matchesValue(T data) {
     // If there are no specified filter values, consider [dataValue] to match
     // this filter.
     if (values.isEmpty) return true;
 
+    final dataValue = dataValueProvider(data);
     if (dataValue == null) {
       return isNegative;
     }
@@ -408,3 +455,11 @@
     isNegative = false;
   }
 }
+
+extension PatternListExtension on List<Pattern> {
+  List<String> toStringList() {
+    return safeFirst is RegExp
+        ? cast<RegExp>().map((v) => v.pattern).toList()
+        : cast<String>();
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/ui/hover.dart b/packages/devtools_app/lib/src/shared/ui/hover.dart
index 92e40d9..b05b1fe 100644
--- a/packages/devtools_app/lib/src/shared/ui/hover.dart
+++ b/packages/devtools_app/lib/src/shared/ui/hover.dart
@@ -506,7 +506,7 @@
   }
 
   Completer _timeoutCompleter(int timeout) {
-    final completer = Completer();
+    final completer = Completer<void>();
     Timer(Duration(milliseconds: timeout), () {
       completer.complete();
     });
diff --git a/packages/devtools_app/lib/src/shared/ui/icons.dart b/packages/devtools_app/lib/src/shared/ui/icons.dart
index b3e052a..c7c599b 100644
--- a/packages/devtools_app/lib/src/shared/ui/icons.dart
+++ b/packages/devtools_app/lib/src/shared/ui/icons.dart
@@ -75,25 +75,19 @@
   @override
   Widget build(BuildContext context) {
     return Container(
-      // Subtract 1 for a little bit of fixed padding
-      // around the icon relative to the default size.
-      // TODO(jacobr): consider switching this to padding.
-      width: defaultIconSize - 1,
-      height: defaultIconSize - 1,
+      width: defaultIconSize,
+      height: defaultIconSize,
       decoration: BoxDecoration(
         shape: BoxShape.circle,
         color: color,
       ),
       alignment: Alignment.center,
-      child: Padding(
-        padding: const EdgeInsets.only(top: 1),
-        child: Text(
-          text,
-          textAlign: TextAlign.center,
-          style: TextStyle(
-            fontSize: scaleByFontFactor(9.0),
-            color: textColor,
-          ),
+      child: Text(
+        text,
+        textAlign: TextAlign.center,
+        style: TextStyle(
+          fontSize: scaleByFontFactor(9.0),
+          color: textColor,
         ),
       ),
     );
diff --git a/packages/devtools_app/lib/src/shared/ui/search.dart b/packages/devtools_app/lib/src/shared/ui/search.dart
index 9b6a241..b2292a8 100644
--- a/packages/devtools_app/lib/src/shared/ui/search.dart
+++ b/packages/devtools_app/lib/src/shared/ui/search.dart
@@ -786,10 +786,10 @@
 }
 
 /// Callback when item in the drop-down list is selected.
-typedef SelectAutoComplete = Function(String selection);
+typedef SelectAutoComplete = void Function(String selection);
 
 /// Callback to handle highlighting item in the drop-down list.
-typedef HighlightAutoComplete = Function(
+typedef HighlightAutoComplete = void Function(
   AutoCompleteSearchControllerMixin controller,
   bool directionDown,
 );
@@ -1029,7 +1029,8 @@
 
   @override
   Widget build(BuildContext context) {
-    final textStyle = style ?? Theme.of(context).textTheme.bodyMedium;
+    final theme = Theme.of(context);
+    final textStyle = style ?? theme.regularTextStyle;
 
     final searchField = TextField(
       key: searchFieldKey,
@@ -1060,6 +1061,7 @@
             ),
             border: const OutlineInputBorder(),
             labelText: label,
+            labelStyle: theme.subtleTextStyle,
             // TODO(kenz): add the search icon to the search field.
             prefix: prefix != null
                 ? Row(
@@ -1209,7 +1211,7 @@
             if (widget.overlayXPositionBuilder != null) {
               widget.controller.xPosition = widget.overlayXPositionBuilder!(
                 value,
-                widget.style ?? Theme.of(context).textTheme.titleMedium,
+                widget.style ?? Theme.of(context).regularTextStyle,
               );
             }
           },
@@ -1387,7 +1389,7 @@
                     child: isSearchInProgress
                         ? SmallCircularProgressIndicator(
                             valueColor: AlwaysStoppedAnimation<Color?>(
-                              Theme.of(context).textTheme.bodyMedium!.color,
+                              Theme.of(context).regularTextStyle.color,
                             ),
                           )
                         : const SizedBox(),
@@ -1403,12 +1405,12 @@
                   ),
                 ),
                 inputDecorationSuffixButton(
-                  Icons.keyboard_arrow_up,
-                  numMatches > 1 ? controller.previousMatch : null,
+                  icon: Icons.keyboard_arrow_up,
+                  onPressed: numMatches > 1 ? controller.previousMatch : null,
                 ),
                 inputDecorationSuffixButton(
-                  Icons.keyboard_arrow_down,
-                  numMatches > 1 ? controller.nextMatch : null,
+                  icon: Icons.keyboard_arrow_down,
+                  onPressed: numMatches > 1 ? controller.nextMatch : null,
                 ),
                 if (onClose != null) closeSearchDropdownButton(onClose),
               ],
diff --git a/packages/devtools_app/lib/src/shared/ui/tab.dart b/packages/devtools_app/lib/src/shared/ui/tab.dart
index b4cb1c8..a0c0ed4 100644
--- a/packages/devtools_app/lib/src/shared/ui/tab.dart
+++ b/packages/devtools_app/lib/src/shared/ui/tab.dart
@@ -6,7 +6,6 @@
 import 'package:flutter/material.dart';
 
 import '../analytics/analytics.dart' as ga;
-import '../utils.dart';
 
 double get _tabHeight => scaleByFontFactor(46.0);
 double get _textAndIconTabHeight => scaleByFontFactor(72.0);
@@ -62,6 +61,14 @@
   final String gaId;
 
   final Widget? trailing;
+
+  @override
+  Widget build(BuildContext context) {
+    return DefaultTextStyle(
+      style: Theme.of(context).textTheme.titleSmall!,
+      child: super.build(context),
+    );
+  }
 }
 
 /// A combined [TabBar] and [TabBarView] implementation that tracks tab changes
@@ -70,7 +77,7 @@
 /// When using this widget, ensure that the [AnalyticsTabbedView] is not being
 /// rebuilt unnecessarily, as each call to [initState] and [didUpdateWidget]
 /// will send an event to analytics for the default selected tab.
-class AnalyticsTabbedView<T> extends StatefulWidget {
+class AnalyticsTabbedView extends StatefulWidget {
   AnalyticsTabbedView({
     Key? key,
     required this.tabs,
@@ -182,7 +189,7 @@
   Widget build(BuildContext context) {
     final tabBar = OutlineDecoration.onlyBottom(
       child: SizedBox(
-        height: defaultHeaderHeight(isDense: isDense()),
+        height: defaultHeaderHeight,
         child: Row(
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           children: [
diff --git a/packages/devtools_app/lib/src/shared/ui/utils.dart b/packages/devtools_app/lib/src/shared/ui/utils.dart
index 19ec857..28b3860 100644
--- a/packages/devtools_app/lib/src/shared/ui/utils.dart
+++ b/packages/devtools_app/lib/src/shared/ui/utils.dart
@@ -11,7 +11,6 @@
 
 import '../primitives/enum_utils.dart';
 import '../primitives/utils.dart';
-import '../table/table.dart';
 
 /// Returns a [TextSpan] that only includes the first [length] characters of
 /// [span].
diff --git a/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart b/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart
index d9f352e..e8699ab 100644
--- a/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart
+++ b/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart
@@ -200,13 +200,9 @@
           SizedBox(
             width: defaultSearchFieldWidth,
             height: defaultTextFieldHeight,
-            child: TextField(
+            child: DevToolsClearableTextField(
               controller: filterController,
-              decoration: const InputDecoration(
-                isDense: true,
-                border: OutlineInputBorder(),
-                labelText: 'Filter',
-              ),
+              labelText: 'Filter',
             ),
           ),
         ],
@@ -283,7 +279,8 @@
   _ValueColumn()
       : super(
           'Value',
-          fixedWidthPx: scaleByFontFactor(160),
+          fixedWidthPx: scaleByFontFactor(100),
+          headerAlignment: TextAlign.right,
           alignment: ColumnAlignment.right,
         );
 
diff --git a/packages/devtools_app/lib/src/shared/utils.dart b/packages/devtools_app/lib/src/shared/utils.dart
index 456d85d..c7549e1 100644
--- a/packages/devtools_app/lib/src/shared/utils.dart
+++ b/packages/devtools_app/lib/src/shared/utils.dart
@@ -33,10 +33,6 @@
   );
 }
 
-bool isDense() {
-  return preferences.denseModeEnabled.value || isEmbedded();
-}
-
 bool isEmbedded() => ideTheme.embed;
 
 extension VmExtension on VM {
@@ -84,7 +80,6 @@
       actions: [
         CopyToClipboardControl(
           dataProvider: () => description,
-          size: defaultIconSize,
         ),
       ],
     );
diff --git a/packages/devtools_app/lib/src/standalone_ui/api/impl/dart_tooling_api.dart b/packages/devtools_app/lib/src/standalone_ui/api/impl/dart_tooling_api.dart
index b54e951..cc356b5 100644
--- a/packages/devtools_app/lib/src/standalone_ui/api/impl/dart_tooling_api.dart
+++ b/packages/devtools_app/lib/src/standalone_ui/api/impl/dart_tooling_api.dart
@@ -17,6 +17,10 @@
 import '../vs_code_api.dart';
 import 'vs_code_api.dart';
 
+// TODO(https://github.com/flutter/devtools/issues/7055): migrate away from
+// postMessage and use the Dart Tooling Daemon to communicate between Dart
+// tooling surfaces.
+
 /// Whether to enable verbose logging for postMessage communication.
 ///
 /// This is useful for debugging when running inside VS Code.
@@ -41,7 +45,7 @@
     if (_enablePostMessageVerboseLogging) {
       setDevToolsLoggingLevel(verboseLoggingLevel);
     }
-    final postMessageController = StreamController();
+    final postMessageController = StreamController<Object?>();
     postMessageController.stream.listen((message) {
       // TODO(dantup): Using fine here doesn't work even though the
       // `setDevToolsLoggingLevel` call above seems like it should show finest
diff --git a/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart b/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart
index fab477d..667ceab 100644
--- a/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart
+++ b/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart
@@ -5,8 +5,11 @@
 import 'dart:async';
 
 import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_shared/devtools_extensions.dart';
 import 'package:flutter/material.dart';
 
+import '../../extensions/extension_screen.dart';
+import '../../extensions/extension_service.dart';
 import '../../shared/analytics/analytics.dart' as ga;
 import '../../shared/analytics/constants.dart' as gac;
 import '../../shared/constants.dart';
@@ -114,7 +117,7 @@
   }
 }
 
-class _DevToolsMenu extends StatelessWidget {
+class _DevToolsMenu extends StatefulWidget {
   const _DevToolsMenu({
     required this.api,
     required this.session,
@@ -136,29 +139,69 @@
   final bool supportsOpenExternal;
 
   @override
-  Widget build(BuildContext context) {
-    final normalDirection = Directionality.of(context);
-    final reversedDirection = normalDirection == TextDirection.ltr
-        ? TextDirection.rtl
-        : TextDirection.ltr;
+  State<_DevToolsMenu> createState() => _DevToolsMenuState();
+}
 
-    Widget devToolsScreenButton(ScreenMetaData screen) {
+class _DevToolsMenuState extends State<_DevToolsMenu> {
+  ExtensionService? _extensionServiceForSession;
+
+  @override
+  void initState() {
+    super.initState();
+    _initExtensions();
+  }
+
+  void _initExtensions() {
+    final sessionRootPath = widget.session.projectRootPath;
+    if (sessionRootPath != null) {
+      // This file path might be a Windows path but because this code runs in
+      // the web, Uri.file() will not handle it correctly.
+      //
+      // Since all paths are absolute, assume that if the path contains `\` and
+      // not `/` then it's Windows.
+      final isWindows =
+          sessionRootPath.contains(r'\') && !sessionRootPath.contains(r'/');
+      final fileUri = Uri.file(sessionRootPath, windows: isWindows);
+      assert(fileUri.isScheme('file'));
+      _extensionServiceForSession = ExtensionService(fixedAppRoot: fileUri);
+      unawaited(_extensionServiceForSession!.initialize());
+    }
+  }
+
+  @override
+  void didUpdateWidget(_DevToolsMenu oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.session != widget.session) {
+      _extensionServiceForSession?.dispose();
+      _initExtensions();
+    }
+  }
+
+  @override
+  void dispose() {
+    _extensionServiceForSession?.dispose();
+    _extensionServiceForSession = null;
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Widget devToolsButton(ScreenMetaData screen) {
       final title = screen.title ?? screen.id;
       String? disabledReason;
-      if (isRelease) {
+      if (widget.isRelease) {
         disabledReason = 'Not available in release mode';
-      } else if (screen.requiresFlutter && !isFlutter) {
+      } else if (screen.requiresFlutter && !widget.isFlutter) {
         disabledReason = 'Only available for Flutter applications';
-      } else if (screen.requiresDebugBuild && !isDebug) {
+      } else if (screen.requiresDebugBuild && !widget.isDebug) {
         disabledReason = 'Only available in debug mode';
-      } else if (screen.requiresDartVm && isWeb) {
+      } else if (screen.requiresDartVm && widget.isWeb) {
         disabledReason = 'Not available when running on the web';
       }
 
-      return DevToolsMenuItem(
-        title,
-        icon: screen.icon,
-        screenId: screen.id,
+      return DevToolsScreenMenuItem(
+        title: title,
+        icon: screen.icon!,
         disabledReason: disabledReason,
         onPressed: () {
           ga.select(
@@ -166,55 +209,80 @@
             gac.VsCodeFlutterSidebar.openDevToolsScreen(screen.id),
           );
           unawaited(
-            api.openDevToolsPage(session.id, page: screen.id),
+            widget.api.openDevToolsPage(
+              widget.session.id,
+              page: screen.id,
+            ),
           );
         },
-        textDirection: normalDirection,
       );
     }
 
-    return Directionality(
-      // Reverse the direction so the menu is anchored on the far side and
-      // expands in the opposite direction with the icons on the right.
-      textDirection: reversedDirection,
-      child: MenuAnchor(
-        style: const MenuStyle(
-          alignment: AlignmentDirectional.bottomStart,
-        ),
-        menuChildren: [
-          ...ScreenMetaData.values
-              .where(_shouldIncludeScreen)
-              .map(devToolsScreenButton),
-          if (supportsOpenExternal)
-            DevToolsMenuItem(
-              'Open in Browser',
-              icon: Icons.open_in_browser,
-              forceExternal: true,
-              textDirection: normalDirection,
-              onPressed: () {
-                ga.select(
-                  gac.VsCodeFlutterSidebar.id,
-                  gac.VsCodeFlutterSidebar.openDevToolsExternally.name,
-                );
-                unawaited(
-                  api.openDevToolsPage(session.id, forceExternal: true),
-                );
-              },
-            ),
-        ],
-        builder: (context, controller, child) => IconButton(
-          onPressed: () {
-            if (controller.isOpen) {
-              controller.close();
-            } else {
-              controller.open();
-            }
-          },
-          tooltip: 'DevTools',
-          icon: Icon(
-            Icons.construction,
-            size: actionsIconSize,
+    return MenuAnchor(
+      menuChildren: [
+        ...ScreenMetaData.values
+            .where(_shouldIncludeScreen)
+            .map(devToolsButton)
+            .toList(),
+        if (widget.supportsOpenExternal)
+          DevToolsScreenMenuItem(
+            title: 'Open in Browser',
+            icon: Icons.open_in_browser,
+            onPressed: () {
+              ga.select(
+                gac.VsCodeFlutterSidebar.id,
+                gac.VsCodeFlutterSidebar.openDevToolsExternally.name,
+              );
+              unawaited(
+                widget.api.openDevToolsPage(
+                  widget.session.id,
+                  forceExternal: true,
+                ),
+              );
+            },
           ),
+        if (_extensionServiceForSession != null)
+          ValueListenableBuilder(
+            valueListenable: _extensionServiceForSession!.visibleExtensions,
+            builder: (context, extensions, _) {
+              return extensions.isEmpty
+                  ? const SizedBox.shrink()
+                  : ExtensionScreenMenuItem(
+                      extensions: extensions,
+                      onPressed: (e) {
+                        ga.select(
+                          gac.VsCodeFlutterSidebar.id,
+                          gac.VsCodeFlutterSidebar.openDevToolsScreen(
+                            gac.DevToolsExtensionEvents.extensionScreenName(
+                              e,
+                            ),
+                          ),
+                        );
+                        unawaited(
+                          widget.api.openDevToolsPage(
+                            widget.session.id,
+                            page: e.screenId,
+                          ),
+                        );
+                      },
+                    );
+            },
+          ),
+      ],
+      builder: (context, controller, child) => IconButton(
+        onPressed: widget.isRelease
+            ? null
+            : () {
+                if (controller.isOpen) {
+                  controller.close();
+                } else {
+                  controller.open();
+                }
+              },
+        tooltip: 'DevTools',
+        icon: Icon(
+          Icons.construction,
+          size: actionsIconSize,
         ),
       ),
     );
@@ -237,34 +305,24 @@
   }
 }
 
-class DevToolsMenuItem extends StatelessWidget {
-  const DevToolsMenuItem(
-    this.title, {
+/// A context menu item for an individual DevTools screen.
+class DevToolsScreenMenuItem extends StatelessWidget {
+  const DevToolsScreenMenuItem({
     super.key,
-    this.icon,
-    this.screenId,
-    this.disabledReason,
-    this.forceExternal = false,
+    required this.title,
+    required this.icon,
     required this.onPressed,
-    required this.textDirection,
+    this.disabledReason,
   });
 
   final String title;
-  final IconData? icon;
-  final String? screenId;
-  final String? disabledReason;
-  final bool forceExternal;
-  final TextDirection textDirection;
+  final IconData icon;
   final VoidCallback onPressed;
+  final String? disabledReason;
 
   @override
   Widget build(BuildContext context) {
-    final textDirection = this.textDirection;
-    Widget text = Directionality(
-      textDirection: textDirection,
-      child: Text(title),
-    );
-
+    Widget text = Text(title);
     if (disabledReason != null) {
       text = Tooltip(
         preferBelow: false,
@@ -280,3 +338,36 @@
     );
   }
 }
+
+/// A context menu submenu button that contains the list of available extension
+/// screens.
+class ExtensionScreenMenuItem extends StatelessWidget {
+  const ExtensionScreenMenuItem({
+    super.key,
+    required this.extensions,
+    required this.onPressed,
+  });
+
+  static const _submenuOffsetDy = 8.0;
+
+  final List<DevToolsExtensionConfig> extensions;
+
+  final void Function(DevToolsExtensionConfig) onPressed;
+
+  @override
+  Widget build(BuildContext context) {
+    return SubmenuButton(
+      alignmentOffset: const Offset(0.0, _submenuOffsetDy),
+      menuChildren: extensions
+          .map(
+            (e) => DevToolsScreenMenuItem(
+              title: e.name,
+              icon: e.icon,
+              onPressed: () => onPressed(e),
+            ),
+          )
+          .toList(),
+      child: const Text('Extensions'),
+    );
+  }
+}
diff --git a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj
index 3f8a110..d4d2e74 100644
--- a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj
+++ b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj
@@ -27,6 +27,7 @@
 		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
 		33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
 		33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+		6EB27392C4B76064DC799088 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D23AEDD0AD6E37FFF22DBEE /* Pods_RunnerTests.framework */; };
 		E678665441E5C0F7F629BAD5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5062035DDDD18FB35E98D5B6 /* Pods_Runner.framework */; };
 /* End PBXBuildFile section */
 
@@ -62,6 +63,7 @@
 
 /* Begin PBXFileReference section */
 		11BB555C0F1767B9B5CB7CE0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		15CEECD69C043520CBE14C4D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
 		331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
 		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
@@ -78,7 +80,10 @@
 		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
 		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
 		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
+		34077518DBF7F7BDDD3C95B2 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
+		46DB21B4B3F0408F0D6E7924 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
 		5062035DDDD18FB35E98D5B6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		5D23AEDD0AD6E37FFF22DBEE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		68C587FFA5A0B8F46A0C5150 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
@@ -90,6 +95,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				6EB27392C4B76064DC799088 /* Pods_RunnerTests.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -185,6 +191,9 @@
 				11BB555C0F1767B9B5CB7CE0 /* Pods-Runner.debug.xcconfig */,
 				68C587FFA5A0B8F46A0C5150 /* Pods-Runner.release.xcconfig */,
 				A7CE48BF63861DD9F3A9FA2F /* Pods-Runner.profile.xcconfig */,
+				34077518DBF7F7BDDD3C95B2 /* Pods-RunnerTests.debug.xcconfig */,
+				15CEECD69C043520CBE14C4D /* Pods-RunnerTests.release.xcconfig */,
+				46DB21B4B3F0408F0D6E7924 /* Pods-RunnerTests.profile.xcconfig */,
 			);
 			name = Pods;
 			path = Pods;
@@ -194,6 +203,7 @@
 			isa = PBXGroup;
 			children = (
 				5062035DDDD18FB35E98D5B6 /* Pods_Runner.framework */,
+				5D23AEDD0AD6E37FFF22DBEE /* Pods_RunnerTests.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -205,6 +215,7 @@
 			isa = PBXNativeTarget;
 			buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
 			buildPhases = (
+				6CE159CAB6573682F9EBBEF1 /* [CP] Check Pods Manifest.lock */,
 				331C80D1294CF70F00263BE5 /* Sources */,
 				331C80D2294CF70F00263BE5 /* Frameworks */,
 				331C80D3294CF70F00263BE5 /* Resources */,
@@ -248,7 +259,7 @@
 			isa = PBXProject;
 			attributes = {
 				LastSwiftUpdateCheck = 0920;
-				LastUpgradeCheck = 1430;
+				LastUpgradeCheck = 1510;
 				ORGANIZATIONNAME = "";
 				TargetAttributes = {
 					331C80D4294CF70F00263BE5 = {
@@ -349,6 +360,28 @@
 			shellPath = /bin/sh;
 			shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
 		};
+		6CE159CAB6573682F9EBBEF1 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
 		DC1C8B6797A659BE5B59B986 /* [CP] Check Pods Manifest.lock */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
@@ -439,6 +472,7 @@
 /* Begin XCBuildConfiguration section */
 		331C80DB294CF71000263BE5 /* Debug */ = {
 			isa = XCBuildConfiguration;
+			baseConfigurationReference = 34077518DBF7F7BDDD3C95B2 /* Pods-RunnerTests.debug.xcconfig */;
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CURRENT_PROJECT_VERSION = 1;
@@ -453,6 +487,7 @@
 		};
 		331C80DC294CF71000263BE5 /* Release */ = {
 			isa = XCBuildConfiguration;
+			baseConfigurationReference = 15CEECD69C043520CBE14C4D /* Pods-RunnerTests.release.xcconfig */;
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CURRENT_PROJECT_VERSION = 1;
@@ -467,6 +502,7 @@
 		};
 		331C80DD294CF71000263BE5 /* Profile */ = {
 			isa = XCBuildConfiguration;
+			baseConfigurationReference = 46DB21B4B3F0408F0D6E7924 /* Pods-RunnerTests.profile.xcconfig */;
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CURRENT_PROJECT_VERSION = 1;
diff --git a/packages/devtools_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/devtools_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index f9e1a62..98cea03 100644
--- a/packages/devtools_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/devtools_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1430"
+   LastUpgradeVersion = "1510"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml
index dec0ff4..9f25bcc 100644
--- a/packages/devtools_app/pubspec.yaml
+++ b/packages/devtools_app/pubspec.yaml
@@ -4,7 +4,7 @@
 
 # Note: this version should only be updated by running tools/update_version.dart
 # that updates all versions of devtools packages (devtools_app, devtools_test).
-version: 2.31.1
+version: 2.33.0-dev.3
 
 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app
 
@@ -58,17 +58,17 @@
   string_scanner: ^1.1.0
   url_launcher: ^6.1.0
   url_launcher_web: ^2.0.6
-  vm_service: ^13.0.0
+  vm_service: ^14.0.0
   # TODO https://github.com/dart-lang/sdk/issues/52853 - unpin this version
   vm_snapshot_analysis: 0.7.2
-  web: ^0.4.0
+  web: ^0.4.1
   web_socket_channel: ^2.1.0
   # widget_icons: ^0.0.1
 
 dev_dependencies:
   args: ^2.4.2
   build_runner: ^2.3.3
-  devtools_test: 2.31.1
+  devtools_test: 2.33.0-dev.3
   fake_async: ^1.3.1
   flutter_driver:
     sdk: flutter
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index bf8401e..66e7426 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -1,35 +1,36 @@
 This is draft for future release notes, that are going to land on
 [the Flutter website](https://docs.flutter.dev/tools/devtools/release-notes).
 
-# DevTools 2.31.0 release notes
+# DevTools 2.33.0 release notes
 
-The 2.31.0 release of the Dart and Flutter DevTools
+The 2.33.0 release of the Dart and Flutter DevTools
 includes the following changes among other general improvements.
 To learn more about DevTools, check out the
 [DevTools overview](https://docs.flutter.dev/tools/devtools/overview).
 
 ## General updates
 
-* Added a new feature for deep link validation, supporting deep link web checks on Android. - [#6935](https://github.com/flutter/devtools/pull/6935)
-* Added the basic plumbing to allow connections to a Dart Tooling Daemon. - [#7009](https://github.com/flutter/devtools/pull/7009)
-* Made table text selectable [#6919](https://github.com/flutter/devtools/pull/6919)
+* Improved overall usability by making the DevTools UI more dense. This
+significantly improves the user experience when using DevTools embedded in
+an IDE. - [#7030](https://github.com/flutter/devtools/pull/7030)
+* Removed the "Dense mode" setting. - [#7086](https://github.com/flutter/devtools/pull/7086)
+* Added support for filtering with regular expressions in the Logging, Network, and CPU profiler
+pages - [#7027](https://github.com/flutter/devtools/pull/7027)
+* Add a DevTools server interaction for getting the DTD uri. - [#7054](https://github.com/flutter/devtools/pull/7054), [#7164](https://github.com/flutter/devtools/pull/7164)
+* Enabled expression evaluation with scope for the web, allowing evaluation of inspected widgets. - [#7144](https://github.com/flutter/devtools/pull/7144)
+* Update `package:vm_service` constraint to `^14.0.0`. - [#6953](https://github.com/flutter/devtools/pull/6953)
 
 ## Inspector updates
 
-* When done typing in the search field, the next selection is now automatically selected - [#6677](https://github.com/flutter/devtools/pull/6677)
-* Added link to package directory documentation, from the inspect settings dialog - [6825](https://github.com/flutter/devtools/pull/6825)
-* Fix bug where widgets owned by the Flutter framework were showing up in the widget tree view -
-[6857](https://github.com/flutter/devtools/pull/6857)
-* Only cache pub root directories added by the user - [6897](https://github.com/flutter/devtools/pull/6897)
-* Remove Flutter pub root if it was accidently cached - [6911](https://github.com/flutter/devtools/pull/6911)
+TODO: Remove this section if there are not any general updates.
 
 ## Performance updates
 
-* Changed raster layer preview background to a checkerboard. - [#6827](https://github.com/flutter/devtools/pull/6827)
+TODO: Remove this section if there are not any general updates.
 
 ## CPU profiler updates
 
-* Added hover cards to CPU profiler sampling rate selections. - [#7010](https://github.com/flutter/devtools/pull/7010)
+TODO: Remove this section if there are not any general updates.
 
 ## Memory updates
 
@@ -37,9 +38,7 @@
 
 ## Debugger updates
 
-* Highlight `extension type` as a declaration keyword,
-  highlight the `$` in identifier interpolation as part of the interpolation,
-  and properly highlight comments within type arguments. - [6837](https://github.com/flutter/devtools/pull/6837)
+TODO: Remove this section if there are not any general updates.
 
 ## Network profiler updates
 
@@ -47,7 +46,7 @@
 
 ## Logging updates
 
-* Added scrollbar to details pane. - [#6917](https://github.com/flutter/devtools/pull/6917)
+TODO: Remove this section if there are not any general updates.
 
 ## App size tool updates
 
@@ -55,13 +54,13 @@
 
 ## VS Code Sidebar updates
 
-* Fixed an issue that prevented the VS code sidebar from loading in recent beta/master builds. - [#6984](https://github.com/flutter/devtools/pull/6984)
+* Do not show DevTools release notes in the Flutter sidebar. - [#7166](https://github.com/flutter/devtools/pull/7166)
 
 ## DevTools Extension updates
 
-* Fixed a couple bugs preventing Dart server apps from connecting to DevTools extensions. - [#6982](https://github.com/flutter/devtools/pull/6982), [#6993](https://github.com/flutter/devtools/pull/6993)
+TODO: Remove this section if there are not any general updates.
 
 ## Full commit history
 
 To find a complete list of changes in this release, check out the
-[DevTools git log](https://github.com/flutter/devtools/tree/v2.31.0).
+[DevTools git log](https://github.com/flutter/devtools/tree/v2.33.0).
diff --git a/packages/devtools_app/release_notes/README.md b/packages/devtools_app/release_notes/README.md
index 1089543..921601d 100644
--- a/packages/devtools_app/release_notes/README.md
+++ b/packages/devtools_app/release_notes/README.md
@@ -26,7 +26,7 @@
 
   - The [NEXT_RELEASE_NOTES.md](NEXT_RELEASE_NOTES.md) file contains
     the running release notes for the current version.
-  - See an example [PR](https://github.com/flutter/website/pull/6791) for
+  - See an example [PR](https://github.com/flutter/website/pull/10113) for
     an example of how to add those to the Flutter website.
   - Make sure to update all image links with the `site_url`
     - e.g. `{{site.url}}/tools/devtools/release-notes/images-<VERSION>/<IMAGE_FILE>`
diff --git a/packages/devtools_app/test/cpu_profiler/cpu_profiler_controller_test.dart b/packages/devtools_app/test/cpu_profiler/cpu_profiler_controller_test.dart
index 0a45424..e38e8d3 100644
--- a/packages/devtools_app/test/cpu_profiler/cpu_profiler_controller_test.dart
+++ b/packages/devtools_app/test/cpu_profiler/cpu_profiler_controller_test.dart
@@ -51,6 +51,7 @@
       setGlobal(OfflineModeController, OfflineModeController());
       setGlobal(PreferencesController, PreferencesController());
       controller = CpuProfilerController();
+      controller.useRegExp.value = false;
     });
 
     Future<void> pullProfileAndSelectFrame() async {
@@ -180,6 +181,64 @@
       expect(filteredData.stackFrames.values.length, equals(9));
     });
 
+    test('filters data by regexp query filter', () async {
+      // [startMicros] and [extentMicros] are arbitrary for testing.
+      await controller.pullAndProcessProfile(
+        startMicros: 0,
+        extentMicros: 100,
+        processId: 'test',
+      );
+      final originalData = controller.cpuProfileStore.lookupProfile(
+        label: CpuProfilerController.userTagNone,
+      )!;
+      expect(
+        originalData.functionProfile.stackFrames.values.length,
+        equals(17),
+      );
+      controller.useRegExp.value = true;
+
+      // At this point, data is filtered by the default toggle filter values.
+      var filteredData = controller.dataNotifier.value!;
+      expect(filteredData.stackFrames.values.length, equals(12));
+
+      // [CpuProfilerController.filterData], which is triggered by the call to
+      // [setActiveFilter] via a listener callback, calls an unawaited future.
+      // We await a short delay here and below to ensure that that future
+      // completes.
+      controller.setActiveFilter(query: 'render');
+      await shortDelay();
+      filteredData = controller.dataNotifier.value!;
+      expect(filteredData.stackFrames.values.length, equals(9));
+
+      controller.setActiveFilter(query: 'render.*parent');
+      await shortDelay();
+      filteredData = controller.dataNotifier.value!;
+      expect(filteredData.stackFrames.values.length, equals(2));
+
+      controller.setActiveFilter(query: 'render.*paint');
+      await shortDelay();
+      filteredData = controller.dataNotifier.value!;
+      expect(filteredData.stackFrames.values.length, equals(6));
+
+      // Disable regexp filters and verify filter behavior changes.
+      controller.useRegExp.value = false;
+
+      controller.setActiveFilter(query: 'render');
+      await shortDelay();
+      filteredData = controller.dataNotifier.value!;
+      expect(filteredData.stackFrames.values.length, equals(9));
+
+      controller.setActiveFilter(query: 'render.*parent');
+      await shortDelay();
+      filteredData = controller.dataNotifier.value!;
+      expect(filteredData.stackFrames.values, isEmpty);
+
+      controller.setActiveFilter(query: 'render.*paint');
+      await shortDelay();
+      filteredData = controller.dataNotifier.value!;
+      expect(filteredData.stackFrames.values, isEmpty);
+    });
+
     test('selectCpuStackFrame', () async {
       final dataNotifierValue = controller.dataNotifier.value!;
 
diff --git a/packages/devtools_app/test/debugger/debugger_controller_test.dart b/packages/devtools_app/test/debugger/debugger_controller_test.dart
index 4ee750a..8e15d0c 100644
--- a/packages/devtools_app/test/debugger/debugger_controller_test.dart
+++ b/packages/devtools_app/test/debugger/debugger_controller_test.dart
@@ -124,7 +124,7 @@
     });
 
     test('starts empty', () {
-      expect(evalHistory.evalHistory, []);
+      expect(evalHistory.evalHistory, <Object?>[]);
       expect(evalHistory.currentText, null);
       expect(evalHistory.canNavigateDown, false);
       expect(evalHistory.canNavigateUp, false);
diff --git a/packages/devtools_app/test/debugger/debugger_evaluation_test.dart b/packages/devtools_app/test/debugger/debugger_evaluation_test.dart
index 125d987..b5f98e0 100644
--- a/packages/devtools_app/test/debugger/debugger_evaluation_test.dart
+++ b/packages/devtools_app/test/debugger/debugger_evaluation_test.dart
@@ -173,24 +173,24 @@
           await runMethodAndWaitForPause(
             'AnotherClass().pauseWithScopedVariablesMethod()',
           );
-          expect(
-            collectionEquals(
-              await autoCompleteResultsFor(
-                EditingParts(
-                  activeWord: '_',
-                  leftSide: '',
-                  rightSide: '',
-                ),
-                evalService,
+          await expectLater(
+            autoCompleteResultsFor(
+              EditingParts(
+                activeWord: '_',
+                leftSide: '',
+                rightSide: '',
               ),
-              [
-                '_privateField2',
-                '_privateField1',
-                '_PrivateClass',
-              ],
-              ordered: false,
+              evalService,
             ),
-            isTrue,
+            completion(
+              unorderedEquals(
+                [
+                  '_privateField2',
+                  '_privateField1',
+                  '_PrivateClass',
+                ],
+              ),
+            ),
           );
         },
         timeout: const Timeout.factor(8),
diff --git a/packages/devtools_app/test/debugger/debugger_screen_paused_test.dart b/packages/devtools_app/test/debugger/debugger_screen_paused_test.dart
index 39c231c..55f65f6 100644
--- a/packages/devtools_app/test/debugger/debugger_screen_paused_test.dart
+++ b/packages/devtools_app/test/debugger/debugger_screen_paused_test.dart
@@ -20,7 +20,7 @@
 import '../test_infra/utils/test_utils.dart';
 
 void main() {
-  const windowSize = Size(2500.0, 1500.0);
+  const windowSize = Size(2500.0, 1200.0);
 
   late FakeServiceConnectionManager fakeServiceConnection;
   late MockScriptManager scriptManager;
diff --git a/packages/devtools_app/test/debugger/program_explorer_test.dart b/packages/devtools_app/test/debugger/program_explorer_test.dart
index ad22a61..ea87b56 100644
--- a/packages/devtools_app/test/debugger/program_explorer_test.dart
+++ b/packages/devtools_app/test/debugger/program_explorer_test.dart
@@ -6,7 +6,6 @@
 import 'package:devtools_app/devtools_app.dart';
 import 'package:devtools_app/src/screens/debugger/debugger_model.dart';
 import 'package:devtools_app/src/screens/debugger/program_explorer.dart';
-import 'package:devtools_app/src/shared/flex_split_column.dart';
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_test/devtools_test.dart';
diff --git a/packages/devtools_app/test/http/curl_command_test.dart b/packages/devtools_app/test/http/curl_command_test.dart
index cc7e670..5b89a54 100644
--- a/packages/devtools_app/test/http/curl_command_test.dart
+++ b/packages/devtools_app/test/http/curl_command_test.dart
@@ -210,9 +210,8 @@
 
 class _TestDartIOHttpRequestData extends DartIOHttpRequestData {
   _TestDartIOHttpRequestData(
-    int timelineMicrosBase,
     this._request,
-  ) : super(timelineMicrosBase, _request);
+  ) : super(_request);
 
   final HttpProfileRequest _request;
 
@@ -244,16 +243,15 @@
   List<String>? cookies,
 }) {
   return _TestDartIOHttpRequestData(
-    0,
     HttpProfileRequest(
       id: '0',
       isolateId: '0',
       method: method,
       uri: uri,
       requestBody: requestBody,
-      responseBody: null,
-      startTime: 0,
-      endTime: 0,
+      events: [],
+      startTime: DateTime.fromMicrosecondsSinceEpoch(0),
+      endTime: DateTime.fromMicrosecondsSinceEpoch(0),
       response: HttpProfileResponseData(
         compressionState: '',
         connectionInfo: {},
@@ -264,9 +262,9 @@
         persistentConnection: false,
         reasonPhrase: '',
         redirects: [],
-        startTime: 0,
+        startTime: DateTime.fromMicrosecondsSinceEpoch(0),
         statusCode: 200,
-        endTime: 0,
+        endTime: DateTime.fromMicrosecondsSinceEpoch(0),
       ),
       request: HttpProfileRequestData.buildSuccessfulRequest(
         headers: headers ?? {},
@@ -275,9 +273,7 @@
         cookies: cookies ?? [],
         followRedirects: false,
         maxRedirects: 0,
-        method: method,
         persistentConnection: false,
-        events: [],
       ),
     ),
   );
diff --git a/packages/devtools_app/test/inspector/inspector_integration_test.dart b/packages/devtools_app/test/inspector/inspector_integration_test.dart
index 7666981..1fe3636 100644
--- a/packages/devtools_app/test/inspector/inspector_integration_test.dart
+++ b/packages/devtools_app/test/inspector/inspector_integration_test.dart
@@ -41,8 +41,6 @@
 
   setUp(() async {
     await env.setupEnvironment();
-    await storage.setValue('ui.denseMode', 'true');
-    preferences.toggleDenseMode(true);
   });
 
   tearDownAll(() async {
diff --git a/packages/devtools_app/test/inspector/layout_explorer/inspector_data_models_test.dart b/packages/devtools_app/test/inspector/layout_explorer/inspector_data_models_test.dart
index d253d6b..43c7b73 100644
--- a/packages/devtools_app/test/inspector/layout_explorer/inspector_data_models_test.dart
+++ b/packages/devtools_app/test/inspector/layout_explorer/inspector_data_models_test.dart
@@ -112,9 +112,7 @@
     group('childrenRenderProperties tests', () {
       const maxMainAxisDimension = 500.0;
 
-      // This is a false positive.
-      // ignore: avoid-unused-parameters
-      double maxSizeAvailable(Axis axis) => maxMainAxisDimension;
+      double maxSizeAvailable(Axis _) => maxMainAxisDimension;
 
       List<RenderProperties> childrenRenderProperties(
         FlexLayoutProperties properties,
diff --git a/packages/devtools_app/test/legacy_integration_tests/integration.dart b/packages/devtools_app/test/legacy_integration_tests/integration.dart
index 31b5d01..30b45b8 100644
--- a/packages/devtools_app/test/legacy_integration_tests/integration.dart
+++ b/packages/devtools_app/test/legacy_integration_tests/integration.dart
@@ -40,7 +40,7 @@
       waitForConnection
           ? tabInstance.onEvent
               .firstWhere((msg) => msg.event == 'app.devToolsReady')
-          : Future.value(),
+          : Future<void>.value(),
       tabInstance.getBrowserChannel(),
     ]);
   }
@@ -385,7 +385,7 @@
       }),
     );
 
-    await buildFinished.future.catchError((e) {
+    await buildFinished.future.catchError((Object? e) {
       fail('Build failed: $e');
     });
 
diff --git a/packages/devtools_app/test/logging/logging_controller_test.dart b/packages/devtools_app/test/logging/logging_controller_test.dart
index 68f84ca..a71d9dc 100644
--- a/packages/devtools_app/test/logging/logging_controller_test.dart
+++ b/packages/devtools_app/test/logging/logging_controller_test.dart
@@ -40,6 +40,10 @@
       );
     }
 
+    void addLogWithKind(String kind) {
+      controller.log(LogData(kind, jsonEncode({'foo': 'test_data'}), 0));
+    }
+
     setUp(() {
       setGlobal(
         ServiceConnectionManager,
@@ -52,7 +56,7 @@
     test('initial state', () {
       expect(controller.data, isEmpty);
       expect(controller.filteredData.value, isEmpty);
-      expect(controller.activeFilter.value.isEmpty, isTrue);
+      expect(controller.activeFilter.value.isEmpty, isFalse);
     });
 
     test('receives data', () {
@@ -82,6 +86,8 @@
       addStdoutData('abc');
       addStdoutData('def');
       addStdoutData('abc ghi');
+      addLogWithKind('Flutter.Navigation');
+      addLogWithKind('Flutter.Error');
       addGcData('gc1');
       addGcData('gc2');
 
@@ -89,11 +95,12 @@
       expect(controller.matchesForSearch('abc').length, equals(2));
       expect(controller.matchesForSearch('ghi').length, equals(1));
       expect(controller.matchesForSearch('abcd').length, equals(0));
+      expect(controller.matchesForSearch('Flutter*').length, equals(2));
       expect(controller.matchesForSearch('').length, equals(0));
 
       // Search by event kind.
       expect(controller.matchesForSearch('stdout').length, equals(3));
-      expect(controller.matchesForSearch('gc').length, equals(2));
+      expect(controller.matchesForSearch('flutter.*').length, equals(2));
 
       // Search with incorrect case.
       expect(controller.matchesForSearch('STDOUT').length, equals(3));
@@ -103,6 +110,8 @@
       addStdoutData('abc');
       addStdoutData('def');
       addStdoutData('abc ghi');
+      addLogWithKind('Flutter.Navigation');
+      addLogWithKind('Flutter.Error');
       addGcData('gc1');
       addGcData('gc2');
 
@@ -112,7 +121,7 @@
       expect(matches.length, equals(2));
       verifyIsSearchMatch(controller.filteredData.value, matches);
 
-      controller.search = 'gc';
+      controller.search = 'Flutter.';
       matches = controller.searchMatches.value;
       expect(matches.length, equals(2));
       verifyIsSearchMatch(controller.filteredData.value, matches);
@@ -122,47 +131,80 @@
       addStdoutData('abc');
       addStdoutData('def');
       addStdoutData('abc ghi');
+      addLogWithKind('Flutter.Navigation');
+      addLogWithKind('Flutter.Error');
+
+      // The following logs should all be filtered by default.
       addGcData('gc1');
       addGcData('gc2');
+      addLogWithKind('Flutter.FirstFrame');
+      addLogWithKind('Flutter.FrameworkInitialization');
+      addLogWithKind('Flutter.Frame');
+      addLogWithKind('Flutter.ImageSizesForFrame');
+      addLogWithKind('Flutter.ServiceExtensionStateChanged');
 
-      expect(controller.data, hasLength(5));
+      // At this point data is filtered by the default toggle filter values.
+      expect(controller.data, hasLength(12));
       expect(controller.filteredData.value, hasLength(5));
 
+      // Test query filters assuming default toggle filters are all enabled.
+      for (final filter in controller.activeFilter.value.toggleFilters) {
+        filter.enabled.value = true;
+      }
+
       controller.setActiveFilter(query: 'abc');
-      expect(controller.data, hasLength(5));
+      expect(controller.data, hasLength(12));
       expect(controller.filteredData.value, hasLength(2));
 
       controller.setActiveFilter(query: 'def');
-      expect(controller.data, hasLength(5));
+      expect(controller.data, hasLength(12));
       expect(controller.filteredData.value, hasLength(1));
 
-      controller.setActiveFilter(query: 'k:stdout abc def');
-      expect(controller.data, hasLength(5));
+      controller.setActiveFilter(query: 'abc def');
+      expect(controller.data, hasLength(12));
       expect(controller.filteredData.value, hasLength(3));
 
-      controller.setActiveFilter(query: 'kind:gc');
-      expect(controller.data, hasLength(5));
+      controller.setActiveFilter(query: 'k:stdout');
+      expect(controller.data, hasLength(12));
+      expect(controller.filteredData.value, hasLength(3));
+
+      controller.setActiveFilter(query: '-k:stdout');
+      expect(controller.data, hasLength(12));
       expect(controller.filteredData.value, hasLength(2));
 
       controller.setActiveFilter(query: 'k:stdout abc');
-      expect(controller.data, hasLength(5));
+      expect(controller.data, hasLength(12));
       expect(controller.filteredData.value, hasLength(2));
 
-      controller.setActiveFilter(query: '-k:gc');
-      expect(controller.data, hasLength(5));
-      expect(controller.filteredData.value, hasLength(3));
-
-      controller.setActiveFilter(query: '-k:gc,stdout');
-      expect(controller.data, hasLength(5));
-      expect(controller.filteredData.value, hasLength(0));
-
-      controller.setActiveFilter(query: 'k:gc,stdout,stdin,flutter.frame');
-      expect(controller.data, hasLength(5));
-      expect(controller.filteredData.value, hasLength(5));
+      controller.setActiveFilter(query: 'k:stdout,flutter.navigation');
+      expect(controller.data, hasLength(12));
+      expect(controller.filteredData.value, hasLength(4));
 
       controller.setActiveFilter();
-      expect(controller.data, hasLength(5));
+      expect(controller.data, hasLength(12));
       expect(controller.filteredData.value, hasLength(5));
+
+      // Test toggle filters.
+      final verboseFlutterFrameworkFilter =
+          controller.activeFilter.value.toggleFilters[0];
+      final verboseFlutterServiceFilter =
+          controller.activeFilter.value.toggleFilters[1];
+      final gcFilter = controller.activeFilter.value.toggleFilters[2];
+
+      verboseFlutterFrameworkFilter.enabled.value = false;
+      controller.setActiveFilter();
+      expect(controller.data, hasLength(12));
+      expect(controller.filteredData.value, hasLength(9));
+
+      verboseFlutterServiceFilter.enabled.value = false;
+      controller.setActiveFilter();
+      expect(controller.data, hasLength(12));
+      expect(controller.filteredData.value, hasLength(10));
+
+      gcFilter.enabled.value = false;
+      controller.setActiveFilter();
+      expect(controller.data, hasLength(12));
+      expect(controller.filteredData.value, hasLength(12));
     });
   });
 
diff --git a/packages/devtools_app/test/memory/chart/chart_test.dart b/packages/devtools_app/test/memory/chart/chart_test.dart
index ede78bb..f06c999 100644
--- a/packages/devtools_app/test/memory/chart/chart_test.dart
+++ b/packages/devtools_app/test/memory/chart/chart_test.dart
@@ -217,8 +217,6 @@
         expect(controller.canvasChartWidth, equals(2150.0));
 
         // Validate the Y axis before data added.
-        expect(controller.yScale.computedMin, equals(0.0));
-        expect(controller.yScale.computedMax, equals(0.0));
         expect(controller.yScale.labelTicks, equals(0.0));
         expect(controller.yScale.labelUnitExponent, 0.0);
         expect(controller.yScale.tickSpacing, equals(1.0));
@@ -289,8 +287,6 @@
           expect(controller.canvasChartWidth, equals(2150.0));
 
           // Validate the Y axis after data added to all traces.
-          expect(controller.yScale.computedMin, equals(0.0));
-          expect(controller.yScale.computedMax, equals(719576719.5767195));
           expect(controller.yScale.labelTicks, equals(10.0));
           expect(controller.yScale.labelUnitExponent, 8.0);
           expect(controller.yScale.tickSpacing, equals(5291005.291005291));
@@ -376,8 +372,6 @@
           expect(controller.canvasChartWidth, equals(2150.0));
 
           // Validate the Y axis after data added to all traces.
-          expect(controller.yScale.computedMin, equals(0.0));
-          expect(controller.yScale.computedMax, equals(719576719.5767195));
           expect(controller.yScale.labelTicks, equals(10.0));
           expect(controller.yScale.labelUnitExponent, 8.0);
           expect(controller.yScale.tickSpacing, equals(5291005.291005291));
@@ -420,8 +414,6 @@
           expect(controller.canvasChartWidth, equals(2149.312));
 
           // Validate the Y axis after data added to all traces.
-          expect(controller.yScale.computedMin, equals(0.0));
-          expect(controller.yScale.computedMax, equals(719576719.5767195));
           expect(controller.yScale.labelTicks, equals(10.0));
           expect(controller.yScale.labelUnitExponent, 8.0);
           expect(controller.yScale.tickSpacing, equals(5291005.291005291));
@@ -614,8 +606,6 @@
         expect(controller.canvasChartWidth, equals(2150.0));
 
         // Validate the Y axis before any data.
-        expect(controller.yScale.computedMin, equals(0.3707865168539326));
-        expect(controller.yScale.computedMax, equals(2.426966292134831));
         expect(controller.yScale.labelTicks, equals(3.0));
         expect(controller.yScale.labelUnitExponent, 0.0);
         expect(controller.yScale.tickSpacing, equals(0.033707865168539325));
@@ -655,12 +645,8 @@
           expect(controller.displayXLabels, true);
           expect(controller.canvasChartWidth, equals(2150.0));
 
-          // Validate the Y axis after data added to all traces.
-          expect(controller.yScale.computedMin, equals(0.0));
-
           // Rest of data is out of view because we're live view max is now 1.4
           // and only 2 labels visible.
-          expect(controller.yScale.computedMax, equals(2.426966292134831));
           expect(controller.yScale.labelTicks, equals(3.0));
 
           expect(controller.yScale.labelUnitExponent, 0.0);
@@ -769,8 +755,6 @@
           expect(controller.canvasChartWidth, equals(2150.0));
 
           // Validate the Y axis after data added to all traces.
-          expect(controller.yScale.computedMin, equals(0.0));
-          expect(controller.yScale.computedMax, equals(2.426966292134831));
           expect(controller.yScale.labelTicks, equals(3.0));
           expect(controller.yScale.labelUnitExponent, 0.0);
           expect(controller.yScale.tickSpacing, equals(0.033707865168539325));
@@ -826,8 +810,6 @@
           expect(controller.canvasChartWidth, equals(2149.312));
 
           // Validate the Y axis after data added to all traces.
-          expect(controller.yScale.computedMin, equals(0.0));
-          expect(controller.yScale.computedMax, equals(2.426966292134831));
           expect(controller.yScale.labelTicks, equals(3.0));
           expect(controller.yScale.labelUnitExponent, 0.0);
           expect(controller.yScale.tickSpacing, equals(0.033707865168539325));
diff --git a/packages/devtools_app/test/memory/diff/widgets/diff_pane_test.dart b/packages/devtools_app/test/memory/diff/widgets/diff_pane_test.dart
index 29cca07..9503357 100644
--- a/packages/devtools_app/test/memory/diff/widgets/diff_pane_test.dart
+++ b/packages/devtools_app/test/memory/diff/widgets/diff_pane_test.dart
@@ -109,7 +109,7 @@
         expect(snapshots.value.length, equals(1 + 3 - 1 + 1));
 
         // Clear all
-        await tester.tap(find.byTooltip('Clear all snapshots'));
+        await tester.tap(find.byTooltip('Delete all snapshots'));
         await tester.pumpAndSettle();
         await expectLater(
           find.byType(DiffPane),
diff --git a/packages/devtools_app/test/network/network_model_test.dart b/packages/devtools_app/test/network/network_model_test.dart
index 0217aa1..63cc38f 100644
--- a/packages/devtools_app/test/network/network_model_test.dart
+++ b/packages/devtools_app/test/network/network_model_test.dart
@@ -246,7 +246,7 @@
           'isRedirect': false,
           'persistentConnection': true,
           'reasonPhrase': 'OK',
-          'redirects': [],
+          'redirects': <Object?>[],
           'statusCode': 200,
         }),
         isTrue,
@@ -273,7 +273,7 @@
           'isRedirect': false,
           'persistentConnection': true,
           'reasonPhrase': 'Created',
-          'redirects': [],
+          'redirects': <Object?>[],
           'statusCode': 201,
         }),
         isTrue,
@@ -293,7 +293,7 @@
           'isRedirect': false,
           'persistentConnection': true,
           'reasonPhrase': 'OK',
-          'redirects': [],
+          'redirects': <Object?>[],
           'statusCode': 200,
         }),
         isTrue,
@@ -312,7 +312,7 @@
           'isRedirect': false,
           'persistentConnection': true,
           'reasonPhrase': 'OK',
-          'redirects': [],
+          'redirects': <Object?>[],
           'statusCode': 200,
         }),
         isTrue,
@@ -332,7 +332,7 @@
           'isRedirect': false,
           'persistentConnection': true,
           'reasonPhrase': 'Switching Protocols',
-          'redirects': [],
+          'redirects': <Object?>[],
           'statusCode': 101,
         }),
         isTrue,
@@ -358,19 +358,19 @@
       expect(httpGetWithError.requestHeaders, isNull);
       expect(
         collectionEquals(httpPost.requestHeaders, {
-          'transfer-encoding': [],
+          'transfer-encoding': <Object?>[],
         }),
         isTrue,
       );
       expect(
         collectionEquals(httpPut.requestHeaders, {
-          'transfer-encoding': [],
+          'transfer-encoding': <Object?>[],
         }),
         isTrue,
       );
       expect(
         collectionEquals(httpPatch.requestHeaders, {
-          'transfer-encoding': [],
+          'transfer-encoding': <Object?>[],
         }),
         isTrue,
       );
diff --git a/packages/devtools_app/test/network/network_profiler_test.dart b/packages/devtools_app/test/network/network_profiler_test.dart
index efb6b09..241bdc0 100644
--- a/packages/devtools_app/test/network/network_profiler_test.dart
+++ b/packages/devtools_app/test/network/network_profiler_test.dart
@@ -8,6 +8,7 @@
 import 'package:devtools_app/src/screens/network/network_request_inspector.dart';
 import 'package:devtools_app/src/screens/network/network_request_inspector_views.dart';
 import 'package:devtools_app/src/shared/http/http.dart';
+import 'package:devtools_app/src/shared/ui/tab.dart';
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_test/devtools_test.dart';
@@ -117,10 +118,7 @@
     void expectNoSelection() {
       expect(find.byType(NetworkRequestsTable), findsOneWidget);
       expect(find.byType(NetworkRequestInspector), findsOneWidget);
-      expect(
-        find.byKey(NetworkRequestInspector.noRequestSelectedKey),
-        findsOneWidget,
-      );
+      expect(find.text('No request selected'), findsOneWidget);
       expect(controller.selectedRequest.value, isNull);
     }
 
@@ -137,7 +135,12 @@
 
         Future<void> validateHeadersTab(DartIOHttpRequestData data) async {
           // Switch to headers tab.
-          await tester.tap(find.byKey(NetworkRequestInspector.headersTabKey));
+          await tester.tap(
+            find.descendant(
+              of: find.byType(DevToolsTab),
+              matching: find.text('Headers'),
+            ),
+          );
           await tester.pumpAndSettle();
 
           expect(find.byType(NetworkRequestOverviewView), findsNothing);
@@ -175,8 +178,12 @@
         Future<void> validateResponseTab(DartIOHttpRequestData data) async {
           if (data.responseBody != null) {
             // Switch to response tab.
-            await tester
-                .tap(find.byKey(NetworkRequestInspector.responseTabKey));
+            await tester.tap(
+              find.descendant(
+                of: find.byType(DevToolsTab),
+                matching: find.text('Response'),
+              ),
+            );
             await tester.pumpAndSettle();
 
             expect(find.byType(HttpResponseTrailingDropDown), findsOneWidget);
@@ -190,7 +197,12 @@
 
         Future<void> validateOverviewTab() async {
           // Switch to overview tab.
-          await tester.tap(find.byKey(NetworkRequestInspector.overviewTabKey));
+          await tester.tap(
+            find.descendant(
+              of: find.byType(DevToolsTab),
+              matching: find.text('Overview'),
+            ),
+          );
           await tester.pumpAndSettle();
 
           expect(find.byType(NetworkRequestOverviewView), findsOneWidget);
@@ -206,7 +218,12 @@
 
           if (hasCookies) {
             // Switch to cookies tab.
-            await tester.tap(find.byKey(NetworkRequestInspector.cookiesTabKey));
+            await tester.tap(
+              find.descendant(
+                of: find.byType(DevToolsTab),
+                matching: find.text('Cookies'),
+              ),
+            );
             await tester.pumpAndSettle();
 
             expect(find.byType(NetworkRequestOverviewView), findsNothing);
@@ -249,7 +266,10 @@
             // The cookies tab shouldn't be displayed if there are no cookies
             // associated with the request.
             expect(
-              find.byKey(NetworkRequestInspector.cookiesTabKey),
+              find.descendant(
+                of: find.byType(DevToolsTab),
+                matching: find.text('Cookies'),
+              ),
               findsNothing,
             );
           }
@@ -258,10 +278,7 @@
         for (final request in controller.requests.value.requests) {
           controller.selectedRequest.value = request;
           await tester.pumpAndSettle();
-          expect(
-            find.byKey(NetworkRequestInspector.noRequestSelectedKey),
-            findsNothing,
-          );
+          expect(find.text('No request selected'), findsNothing);
 
           final selection = controller.selectedRequest.value!;
           if (selection is DartIOHttpRequestData) {
@@ -303,10 +320,7 @@
         await tester.pumpAndSettle();
 
         expect(controller.selectedRequest.value, isNotNull);
-        expect(
-          find.byKey(NetworkRequestInspector.noRequestSelectedKey),
-          findsNothing,
-        );
+        expect(find.text('No request selected'), findsNothing);
       },
     );
 
diff --git a/packages/devtools_app/test/network/network_request_inspector_test.dart b/packages/devtools_app/test/network/network_request_inspector_test.dart
index f3d7a0e..d8e30af 100644
--- a/packages/devtools_app/test/network/network_request_inspector_test.dart
+++ b/packages/devtools_app/test/network/network_request_inspector_test.dart
@@ -40,7 +40,7 @@
             requests: [
               httpRequest!,
             ],
-            timestamp: 0,
+            timestamp: DateTime.fromMicrosecondsSinceEpoch(0),
           ),
         ),
       );
diff --git a/packages/devtools_app/test/network/utils/network_test_utils.dart b/packages/devtools_app/test/network/utils/network_test_utils.dart
index 33a2362..a04001f 100644
--- a/packages/devtools_app/test/network/utils/network_test_utils.dart
+++ b/packages/devtools_app/test/network/utils/network_test_utils.dart
@@ -26,6 +26,6 @@
       HttpProfileRequest.parse(httpWsHandshakeJson)!,
       HttpProfileRequest.parse(httpGetPendingJson)!,
     ],
-    timestamp: 0,
+    timestamp: DateTime.fromMicrosecondsSinceEpoch(0),
   );
 }
diff --git a/packages/devtools_app/test/performance/flutter_frames/flutter_frames_chart_test.dart b/packages/devtools_app/test/performance/flutter_frames/flutter_frames_chart_test.dart
index ba9e4e8..18ccd4b 100644
--- a/packages/devtools_app/test/performance/flutter_frames/flutter_frames_chart_test.dart
+++ b/packages/devtools_app/test/performance/flutter_frames/flutter_frames_chart_test.dart
@@ -131,7 +131,7 @@
 
     group('starting scroll position', () {
       const totalNumFrames = 50;
-      const totalFramesInView = 14;
+      const totalFramesInView = 15;
 
       setUp(() {
         var number = 0;
diff --git a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer.png b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer.png
index 30b85d3..edc3ee4 100644
--- a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer.png
+++ b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer.png
Binary files differ
diff --git a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_icons_only.png b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_icons_only.png
index 3a6433d..07fbdf5 100644
--- a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_icons_only.png
+++ b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_icons_only.png
Binary files differ
diff --git a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_with_shader_compilation.png b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_with_shader_compilation.png
index 1e78c13..073afc1 100644
--- a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_with_shader_compilation.png
+++ b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_with_shader_compilation.png
Binary files differ
diff --git a/packages/devtools_app/test/performance/performance_model_test.dart b/packages/devtools_app/test/performance/performance_model_test.dart
index 8fdf5af..dc6c924 100644
--- a/packages/devtools_app/test/performance/performance_model_test.dart
+++ b/packages/devtools_app/test/performance/performance_model_test.dart
@@ -47,12 +47,12 @@
         performanceData.toJson(),
         equals({
           PerformanceData.selectedFrameIdKey: null,
-          PerformanceData.flutterFramesKey: [],
+          PerformanceData.flutterFramesKey: <Object?>[],
           PerformanceData.displayRefreshRateKey: 60,
-          PerformanceData.traceEventsKey: [],
-          PerformanceData.selectedEventKey: {},
-          PerformanceData.cpuProfileKey: {},
-          PerformanceData.rasterStatsKey: {},
+          PerformanceData.traceEventsKey: <Object?>[],
+          PerformanceData.selectedEventKey: <Object?, Object?>{},
+          PerformanceData.cpuProfileKey: <Object?, Object?>{},
+          PerformanceData.rasterStatsKey: <Object?, Object?>{},
           PerformanceData.rebuildCountModelKey: null,
         }),
       );
diff --git a/packages/devtools_app/test/performance/raster_stats/goldens/raster_stats_changed_selection.png b/packages/devtools_app/test/performance/raster_stats/goldens/raster_stats_changed_selection.png
index aa086db..6528ce2 100644
--- a/packages/devtools_app/test/performance/raster_stats/goldens/raster_stats_changed_selection.png
+++ b/packages/devtools_app/test/performance/raster_stats/goldens/raster_stats_changed_selection.png
Binary files differ
diff --git a/packages/devtools_app/test/performance/raster_stats/goldens/raster_stats_with_data.png b/packages/devtools_app/test/performance/raster_stats/goldens/raster_stats_with_data.png
index 2a5f3cb..6c9a868 100644
--- a/packages/devtools_app/test/performance/raster_stats/goldens/raster_stats_with_data.png
+++ b/packages/devtools_app/test/performance/raster_stats/goldens/raster_stats_with_data.png
Binary files differ
diff --git a/packages/devtools_app/test/performance/timeline_events/legacy/legacy_event_processor_test.dart b/packages/devtools_app/test/performance/timeline_events/legacy/legacy_event_processor_test.dart
index 838572e..b4c3f03 100644
--- a/packages/devtools_app/test/performance/timeline_events/legacy/legacy_event_processor_test.dart
+++ b/packages/devtools_app/test/performance/timeline_events/legacy/legacy_event_processor_test.dart
@@ -135,7 +135,7 @@
         'pid': 94955,
         'ts': 118039650802,
         'ph': 'B',
-        'args': {},
+        'args': <String, Object?>{},
       });
       final animatorBeginFrameEvent = testTraceEventWrapper({
         'name': 'Animator::BeginFrame',
@@ -144,7 +144,7 @@
         'pid': 94955,
         'ts': 118039650802,
         'ph': 'B',
-        'args': {},
+        'args': <String, Object?>{},
       });
       traceEvents = [
         vsyncEvent,
diff --git a/packages/devtools_app/test/performance/timeline_events/perfetto/perfetto_event_processor_test.dart b/packages/devtools_app/test/performance/timeline_events/perfetto/perfetto_event_processor_test.dart
index 833c21a..29186a3 100644
--- a/packages/devtools_app/test/performance/timeline_events/perfetto/perfetto_event_processor_test.dart
+++ b/packages/devtools_app/test/performance/timeline_events/perfetto/perfetto_event_processor_test.dart
@@ -158,7 +158,7 @@
     'pid': 94955,
     'ts': 200,
     'ph': 'E',
-    'args': {},
+    'args': <String, Object?>{},
   }),
   testTraceEventWrapper({
     'name': 'Animator::BeginFrame',
@@ -176,7 +176,7 @@
     'pid': 94955,
     'ts': 400,
     'ph': 'E',
-    'args': {},
+    'args': <String, Object?>{},
   }),
   testTraceEventWrapper({
     'name': 'Animator::BeginFrame',
@@ -194,7 +194,7 @@
     'pid': 94955,
     'ts': 600,
     'ph': 'E',
-    'args': {},
+    'args': <String, Object?>{},
   }),
   testTraceEventWrapper({
     'name': 'GPURasterizer::Draw',
@@ -215,7 +215,7 @@
     'pid': 94955,
     'ts': 250,
     'ph': 'E',
-    'args': {},
+    'args': <String, Object?>{},
   }),
   testTraceEventWrapper({
     'name': 'GPURasterizer::Draw',
@@ -236,7 +236,7 @@
     'pid': 94955,
     'ts': 450,
     'ph': 'E',
-    'args': {},
+    'args': <String, Object?>{},
   }),
   testTraceEventWrapper({
     'name': 'GPURasterizer::Draw',
@@ -257,6 +257,6 @@
     'pid': 94955,
     'ts': 650,
     'ph': 'E',
-    'args': {},
+    'args': <String, Object?>{},
   }),
 ];
diff --git a/packages/devtools_app/test/primitives/utils_test.dart b/packages/devtools_app/test/primitives/utils_test.dart
index 8c64b5e..6e84319 100644
--- a/packages/devtools_app/test/primitives/utils_test.dart
+++ b/packages/devtools_app/test/primitives/utils_test.dart
@@ -1170,6 +1170,13 @@
           isFalse,
         );
       });
+
+      test('allIndicesWhere', () {
+        final list = [1, 2, 1, 2, 3, 4];
+        expect(list.allIndicesWhere((element) => element.isEven), [1, 3, 5]);
+        expect(list.allIndicesWhere((element) => element.isOdd), [0, 2, 4]);
+        expect(list.allIndicesWhere((element) => element + 2 == 3), [0, 2]);
+      });
     });
 
     group('SetExtension', () {
@@ -1264,6 +1271,24 @@
         expect(str.caseInsensitiveEquals(null), isFalse);
         expect(''.caseInsensitiveEquals(''), isTrue);
         expect(''.caseInsensitiveEquals(null), isFalse);
+
+        // Complete match.
+        expect(
+          str.caseInsensitiveEquals(RegExp('h.*o.*', caseSensitive: false)),
+          isTrue,
+        );
+        // Incomplete match.
+        expect(
+          str.caseInsensitiveEquals(RegExp('h.*o', caseSensitive: false)),
+          isFalse,
+        );
+        // No match.
+        expect(
+          str.caseInsensitiveEquals(
+            RegExp('hello.* this does not match', caseSensitive: false),
+          ),
+          isFalse,
+        );
       });
 
       test('caseInsensitiveAllMatches', () {
@@ -1361,7 +1386,10 @@
           subtractor: elementSubtractor,
         );
 
-        expect(const SetEquality().equals(result.keys.toSet(), {1, 2}), true);
+        expect(
+          const SetEquality<int>().equals(result.keys.toSet(), {1, 2}),
+          true,
+        );
         expect(
           result[1],
           equals(_SubtractionResult(subtract: 'subtract', from: 1.0)),
@@ -1386,7 +1414,7 @@
           subtractor: elementSubtractor,
         );
 
-        expect(const SetEquality().equals(result.keys.toSet(), {1}), true);
+        expect(const SetEquality<int>().equals(result.keys.toSet(), {1}), true);
         expect(
           result[1],
           equals(_SubtractionResult(subtract: null, from: 1.0)),
@@ -1407,7 +1435,7 @@
           subtractor: elementSubtractor,
         );
 
-        expect(const SetEquality().equals(result.keys.toSet(), {1}), true);
+        expect(const SetEquality<int>().equals(result.keys.toSet(), {1}), true);
         expect(
           result[1],
           equals(_SubtractionResult(subtract: 'subtract', from: null)),
@@ -1418,7 +1446,7 @@
 
   group('joinWithTrailing', () {
     test('joins no items', () {
-      expect([].joinWithTrailing(':'), equals(''));
+      expect(<String>[].joinWithTrailing(':'), equals(''));
     });
     test(' joins 1 item', () {
       expect(['A'].joinWithTrailing(':'), equals('A:'));
diff --git a/packages/devtools_app/test/shared/eval_integration_test.dart b/packages/devtools_app/test/shared/eval_integration_test.dart
index fe80515..0c8052f 100644
--- a/packages/devtools_app/test/shared/eval_integration_test.dart
+++ b/packages/devtools_app/test/shared/eval_integration_test.dart
@@ -49,7 +49,7 @@
 
     group('asyncEval', () {
       test(
-        'supports expresions that do not start with the await keyword',
+        'supports expressions that do not start with the await keyword',
         () async {
           await env.setupEnvironment();
 
@@ -118,7 +118,7 @@
                 (_) => throw Exception(
                   'The FutureFailedException was not thrown as expected.',
                 ),
-                onError: (err) => err,
+                onError: (Object? err) => err,
               );
 
           expect(
diff --git a/packages/devtools_app/test/shared/filter_test.dart b/packages/devtools_app/test/shared/filter_test.dart
index d406998..295a724 100644
--- a/packages/devtools_app/test/shared/filter_test.dart
+++ b/packages/devtools_app/test/shared/filter_test.dart
@@ -23,7 +23,7 @@
 
     setUp(() {
       controller = _TestController(_sampleData);
-      expect(controller.data.length, equals(7));
+      expect(controller.data.length, _sampleData.length);
       expect(controller.filteredData.value, isEmpty);
       verifyBaseFilterState();
     });
@@ -37,7 +37,9 @@
 
       expect(
         controller.filteredData.value.toString(),
-        equals('[1-FooBar-foobar, 3-Baz-foobar, 5-Basset Hound-dog]'),
+        equals(
+          '[1-FooBar-foobar, 3-Baz-foobar, 5-Basset Hound-dog, 7-Shepherd\'s pie-food, 9-Meal bar-food]',
+        ),
       );
     });
 
@@ -46,6 +48,8 @@
       for (final toggleFilter in controller.activeFilter.value.toggleFilters) {
         toggleFilter.enabled.value = false;
       }
+      expect(controller.useRegExp.value, isFalse);
+
       controller.setActiveFilter();
       expect(
         controller.filteredData.value.toString(),
@@ -57,7 +61,7 @@
       expect(
         controller.filteredData.value.toString(),
         equals(
-          '[1-FooBar-foobar, 2-Bar-foobar, 3-Baz-foobar, 5-Basset Hound-dog]',
+          '[1-FooBar-foobar, 2-Bar-foobar, 3-Baz-foobar, 5-Basset Hound-dog, 9-Meal bar-food]',
         ),
       );
 
@@ -88,7 +92,7 @@
       expect(
         controller.filteredData.value.toString(),
         equals(
-          '[1-FooBar-foobar, 2-Bar-foobar, 4-Shepherd-dog, 5-Basset Hound-dog]',
+          '[1-FooBar-foobar, 2-Bar-foobar, 4-Shepherd-dog, 5-Basset Hound-dog, 7-Shepherd\'s pie-food, 8-Orange-food]',
         ),
       );
 
@@ -111,6 +115,60 @@
       );
     });
 
+    test('filterData applies regexp query filters when enabled', () {
+      // Disable all toggle filters.
+      for (final toggleFilter in controller.activeFilter.value.toggleFilters) {
+        toggleFilter.enabled.value = false;
+      }
+      controller.useRegExp.value = true;
+
+      controller.setActiveFilter();
+      expect(
+        controller.filteredData.value.toString(),
+        _sampleData.toString(),
+      );
+
+      // Regexp filter argument match.
+      controller.setActiveFilter(query: 'cat:foo.*');
+      expect(
+        controller.filteredData.value.toString(),
+        equals(
+          '[0-Foo-foobar, 1-FooBar-foobar, 2-Bar-foobar, 3-Baz-foobar, 7-Shepherd\'s pie-food, 8-Orange-food, 9-Meal bar-food]',
+        ),
+      );
+      controller.setActiveFilter(query: '-cat:foo.*');
+      expect(
+        controller.filteredData.value.toString(),
+        equals(
+          '[4-Shepherd-dog, 5-Basset Hound-dog, 6-Husky-dog]',
+        ),
+      );
+      // Regexp substring match.
+      controller.setActiveFilter(query: '.*bar');
+      expect(
+        controller.filteredData.value.toString(),
+        equals(
+          '[1-FooBar-foobar, 2-Bar-foobar, 9-Meal bar-food]',
+        ),
+      );
+
+      // Disable regexp filters and verify filter behavior changes.
+      controller.useRegExp.value = false;
+
+      // Regexp filter argument match.
+      controller.setActiveFilter(query: 'cat:foo.*');
+      expect(controller.filteredData.value, isEmpty);
+      controller.setActiveFilter(query: '-cat:foo.*');
+      expect(
+        controller.filteredData.value.toString(),
+        _sampleData.toString(),
+      );
+
+      // Regexp substring match.
+      controller.setActiveFilter(query: '.*bar');
+      expect(controller.filteredData.value, isEmpty);
+    });
+
     test('isFilterActive', () {
       controller.toggleFilters[0].enabled.value = true;
       controller.toggleFilters[1].enabled.value = false;
@@ -145,6 +203,12 @@
       controller.setActiveFilter(query: 'Ba cat:foobar');
       expect(controller.activeFilterTag(), 'ba cat:foobar');
 
+      // Query filter with regular expressions enabled.
+      controller.useRegExp.value = true;
+      controller.setActiveFilter(query: 'Ba cat:foobar');
+      expect(controller.activeFilterTag(), 'ba cat:foobar-#-regexp');
+      controller.useRegExp.value = false;
+
       // Only toggle filter active and no query filters.
       controller.toggleFilters[0].enabled.value = true;
       controller.setActiveFilter();
@@ -221,7 +285,11 @@
 
   @override
   Map<String, QueryFilterArgument> createQueryFilterArgs() => {
-        categoryFilterId: QueryFilterArgument(keys: ['cat', 'c']),
+        categoryFilterId: QueryFilterArgument<_TestDataClass>(
+          keys: ['cat', 'c'],
+          dataValueProvider: (data) => data.category,
+          substringMatch: false,
+        ),
       };
 
   @override
@@ -235,26 +303,23 @@
     }
     bool filterCallback(_TestDataClass element) {
       // First filter by the toggle filters.
-      final toggleFilters = filter.toggleFilters;
-      for (final toggleFilter in toggleFilters) {
-        if (toggleFilter.enabled.value) {
-          if (!toggleFilter.includeCallback(element)) return false;
-        }
-      }
+      final filteredOutByToggleFilters = filter.toggleFilters.any(
+        (toggleFilter) =>
+            toggleFilter.enabled.value &&
+            !toggleFilter.includeCallback(element),
+      );
+      if (filteredOutByToggleFilters) return false;
 
       final queryFilter = filter.queryFilter;
       if (!queryFilter.isEmpty) {
-        // Match the query argument to [_TestDataClass.category].
-        final categoryArg =
-            filter.queryFilter.filterArguments[categoryFilterId];
-        if (categoryArg != null &&
-            !categoryArg.matchesValue(element.category)) {
-          return false;
-        }
+        final filteredOutByQueryFilterArgument = queryFilter
+            .filterArguments.values
+            .any((argument) => !argument.matchesValue(element));
+        if (filteredOutByQueryFilterArgument) return false;
 
         // Match substrings to [_TestDataClass.label].
-        if (queryFilter.substrings.isNotEmpty) {
-          for (final substring in queryFilter.substrings) {
+        if (queryFilter.substringExpressions.isNotEmpty) {
+          for (final substring in queryFilter.substringExpressions) {
             bool matches(String? stringToMatch) {
               if (stringToMatch?.caseInsensitiveContains(substring) == true) {
                 return true;
@@ -298,4 +363,7 @@
   _TestDataClass(4, 'Shepherd', 'dog'),
   _TestDataClass(5, 'Basset Hound', 'dog'),
   _TestDataClass(6, 'Husky', 'dog'),
+  _TestDataClass(7, 'Shepherd\'s pie', 'food'),
+  _TestDataClass(8, 'Orange', 'food'),
+  _TestDataClass(9, 'Meal bar', 'food'),
 ];
diff --git a/packages/devtools_app/test/shared/flame_chart_test.dart b/packages/devtools_app/test/shared/flame_chart_test.dart
index 4c901de..ed919ab 100644
--- a/packages/devtools_app/test/shared/flame_chart_test.dart
+++ b/packages/devtools_app/test/shared/flame_chart_test.dart
@@ -731,7 +731,7 @@
               activeSearchMatch: false,
               hovered: hovered,
               zoom: zoom,
-              colorScheme: Theme.of(context).colorScheme,
+              theme: Theme.of(context),
             ),
           ),
         ),
@@ -756,7 +756,7 @@
                     activeSearchMatch: false,
                     hovered: hovered,
                     zoom: defaultZoom,
-                    colorScheme: Theme.of(context).colorScheme,
+                    theme: Theme.of(context),
                   );
                 },
               ),
diff --git a/packages/devtools_app/test/shared/home_screen_test.dart b/packages/devtools_app/test/shared/home_screen_test.dart
index 0fd42d6..880b0c6 100644
--- a/packages/devtools_app/test/shared/home_screen_test.dart
+++ b/packages/devtools_app/test/shared/home_screen_test.dart
@@ -32,7 +32,7 @@
         // Build our app and trigger a frame.
         await tester.pumpWidget(wrap(const HomeScreenBody()));
         expect(find.byType(ConnectionSection), findsOneWidget);
-        expect(find.byType(ConnectDialog), findsOneWidget);
+        expect(find.byType(ConnectInput), findsOneWidget);
         expect(find.byType(ConnectToNewAppButton), findsNothing);
         expect(find.byType(ViewVmFlagsButton), findsNothing);
         expect(find.byType(SampleDataDropDownButton), findsNothing);
@@ -99,7 +99,7 @@
         // Build our app and trigger a frame.
         await tester.pumpWidget(wrap(const HomeScreenBody()));
         expect(find.byType(ConnectionSection), findsOneWidget);
-        expect(find.byType(ConnectDialog), findsNothing);
+        expect(find.byType(ConnectInput), findsNothing);
         expect(find.byType(ConnectToNewAppButton), findsOneWidget);
         expect(find.byType(ViewVmFlagsButton), findsOneWidget);
         expect(find.byType(SampleDataDropDownButton), findsNothing);
diff --git a/packages/devtools_app/test/shared/preferences_controller_test.dart b/packages/devtools_app/test/shared/preferences_controller_test.dart
index 7c9e168..b87e7d4 100644
--- a/packages/devtools_app/test/shared/preferences_controller_test.dart
+++ b/packages/devtools_app/test/shared/preferences_controller_test.dart
@@ -32,7 +32,6 @@
 
     test('has value', () {
       expect(controller.darkModeTheme.value, isNotNull);
-      expect(controller.denseModeEnabled.value, isNotNull);
     });
 
     test('toggleDarkModeTheme', () {
@@ -61,19 +60,6 @@
       expect(valueChanged, isTrue);
       expect(controller.vmDeveloperModeEnabled.value, isNot(originalValue));
     });
-
-    test('toggleDenseMode', () {
-      bool valueChanged = false;
-      final originalValue = controller.denseModeEnabled.value;
-
-      controller.denseModeEnabled.addListener(() {
-        valueChanged = true;
-      });
-
-      controller.toggleDenseMode(!controller.denseModeEnabled.value);
-      expect(valueChanged, isTrue);
-      expect(controller.denseModeEnabled.value, isNot(originalValue));
-    });
   });
 
   // TODO(https://github.com/flutter/devtools/issues/4342): Add more tests.
diff --git a/packages/devtools_app/test/shared/release_notes_test.dart b/packages/devtools_app/test/shared/release_notes_test.dart
index 62f8038..c93557b 100644
--- a/packages/devtools_app/test/shared/release_notes_test.dart
+++ b/packages/devtools_app/test/shared/release_notes_test.dart
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_shared/devtools_shared.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -10,6 +12,7 @@
   group('$ReleaseNotesController', () {
     late ReleaseNotesController controller;
     setUp(() {
+      setGlobal(IdeTheme, IdeTheme());
       debugTestReleaseNotes = true;
       controller = ReleaseNotesController();
     });
diff --git a/packages/devtools_app/test/shared/syntax_highlighter_test.dart b/packages/devtools_app/test/shared/syntax_highlighter_test.dart
index 5e4c0f2..833bb25 100644
--- a/packages/devtools_app/test/shared/syntax_highlighter_test.dart
+++ b/packages/devtools_app/test/shared/syntax_highlighter_test.dart
@@ -171,7 +171,7 @@
       'Syntax Highlighting (${useDarkTheme ? 'Dark' : 'Light'} Theme)',
       () {
         Widget buildSyntaxHighlightingTestContext(
-          Function(BuildContext) callback,
+          void Function(BuildContext) callback,
         ) {
           return MaterialApp.router(
             theme: themeFor(
@@ -324,8 +324,6 @@
                   );
 
                   expect(children[18].toPlainText(), '\n');
-
-                  return Container();
                 },
               ),
             );
@@ -556,7 +554,6 @@
                         color: colorCallback(Theme.of(context).colorScheme),
                       ),
                     );
-                    return Container();
                   },
                 ),
               );
diff --git a/packages/devtools_app/test/shared/table_test.dart b/packages/devtools_app/test/shared/table_test.dart
index 0689f4f..e009581 100644
--- a/packages/devtools_app/test/shared/table_test.dart
+++ b/packages/devtools_app/test/shared/table_test.dart
@@ -11,7 +11,7 @@
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_test/devtools_test.dart';
 import 'package:devtools_test/helpers.dart';
-import 'package:flutter/material.dart' hide TableRow;
+import 'package:flutter/material.dart' hide TableRow, Table;
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -42,6 +42,8 @@
   bool get numeric => true;
 }
 
+const windowSize = Size(4000.0, 500.0);
+
 void main() {
   setUp(() {
     setGlobal(ServiceConnectionManager, FakeServiceConnectionManager());
@@ -68,29 +70,33 @@
       ];
     });
 
-    testWidgets('displays with simple content', (WidgetTester tester) async {
-      final table = FlatTable<TestData>(
-        columns: [flatNameColumn],
-        data: [TestData('empty', 0)],
-        dataKey: 'test-data',
-        keyFactory: (d) => Key(d.name),
-        defaultSortColumn: flatNameColumn,
-        defaultSortDirection: SortDirection.ascending,
-      );
-      await tester.pumpWidget(wrap(table));
-      expect(find.byWidget(table), findsOneWidget);
-      expect(find.text('FlatName'), findsOneWidget);
+    testWidgetsWithWindowSize(
+      'displays with simple content',
+      windowSize,
+      (WidgetTester tester) async {
+        final table = FlatTable<TestData>(
+          columns: [flatNameColumn],
+          data: [TestData('empty', 0)],
+          dataKey: 'test-data',
+          keyFactory: (d) => Key(d.name),
+          defaultSortColumn: flatNameColumn,
+          defaultSortDirection: SortDirection.ascending,
+        );
+        await tester.pumpWidget(wrap(table));
+        expect(find.byWidget(table), findsOneWidget);
+        expect(find.text('FlatName'), findsOneWidget);
 
-      final FlatTableState state = tester.state(find.byWidget(table));
-      final columnWidths =
-          state.tableController.computeColumnWidthsSizeToFit(1000);
-      expect(columnWidths.length, 1);
-      expect(columnWidths.first, 300);
-      expect(find.byKey(const Key('empty')), findsOneWidget);
-    });
-
-    testWidgets(
+        final FlatTableState state = tester.state(find.byWidget(table));
+        final columnWidths =
+            state.tableController.computeColumnWidthsSizeToFit(1000);
+        expect(columnWidths.length, 1);
+        expect(columnWidths.first, 300);
+        expect(find.byKey(const Key('empty')), findsOneWidget);
+      },
+    );
+    testWidgetsWithWindowSize(
       'displays with simple content size to content',
+      windowSize,
       (WidgetTester tester) async {
         final table = FlatTable<TestData>(
           columns: [flatNameColumn],
@@ -116,7 +122,7 @@
 
     testWidgetsWithWindowSize(
       'displays with full content',
-      const Size(800.0, 1200.0),
+      windowSize,
       (WidgetTester tester) async {
         final table = FlatTable<TestData>(
           columns: [
@@ -151,7 +157,7 @@
 
     testWidgetsWithWindowSize(
       'displays with column groups',
-      const Size(800.0, 1200.0),
+      windowSize,
       (WidgetTester tester) async {
         final table = FlatTable<TestData>(
           columns: [
@@ -198,15 +204,15 @@
     );
 
     testWidgets('starts with sorted data', (WidgetTester tester) async {
-      expect(flatData[0].name, equals('Foo'));
-      expect(flatData[1].name, equals('Bar'));
-      expect(flatData[2].name, equals('Baz'));
-      expect(flatData[3].name, equals('Qux'));
-      expect(flatData[4].name, equals('Snap'));
-      expect(flatData[5].name, equals('Crackle'));
-      expect(flatData[6].name, equals('Pop'));
-      expect(flatData[7].name, equals('Baz'));
-      expect(flatData[8].name, equals('Qux'));
+      expect(flatData[0].name, 'Foo');
+      expect(flatData[1].name, 'Bar');
+      expect(flatData[2].name, 'Baz');
+      expect(flatData[3].name, 'Qux');
+      expect(flatData[4].name, 'Snap');
+      expect(flatData[5].name, 'Crackle');
+      expect(flatData[6].name, 'Pop');
+      expect(flatData[7].name, 'Baz');
+      expect(flatData[8].name, 'Qux');
       final table = FlatTable<TestData>(
         columns: [
           flatNameColumn,
@@ -221,15 +227,15 @@
       await tester.pumpWidget(wrap(table));
       final FlatTableState<TestData> state = tester.state(find.byWidget(table));
       final data = state.tableController.tableData.value.data;
-      expect(data[0].name, equals('Bar'));
-      expect(data[1].name, equals('Baz'));
-      expect(data[2].name, equals('Baz'));
-      expect(data[3].name, equals('Crackle'));
-      expect(data[4].name, equals('Foo'));
-      expect(data[5].name, equals('Pop'));
-      expect(data[6].name, equals('Qux'));
-      expect(data[7].name, equals('Qux'));
-      expect(data[8].name, equals('Snap'));
+      expect(data[0].name, 'Bar');
+      expect(data[1].name, 'Baz');
+      expect(data[2].name, 'Baz');
+      expect(data[3].name, 'Crackle');
+      expect(data[4].name, 'Foo');
+      expect(data[5].name, 'Pop');
+      expect(data[6].name, 'Qux');
+      expect(data[7].name, 'Qux');
+      expect(data[8].name, 'Snap');
     });
 
     testWidgets('sorts data by column', (WidgetTester tester) async {
@@ -248,15 +254,15 @@
       final FlatTableState<TestData> state = tester.state(find.byWidget(table));
       {
         final data = state.tableController.tableData.value.data;
-        expect(data[0].name, equals('Bar'));
-        expect(data[1].name, equals('Baz'));
-        expect(data[2].name, equals('Baz'));
-        expect(data[3].name, equals('Crackle'));
-        expect(data[4].name, equals('Foo'));
-        expect(data[5].name, equals('Pop'));
-        expect(data[6].name, equals('Qux'));
-        expect(data[7].name, equals('Qux'));
-        expect(data[8].name, equals('Snap'));
+        expect(data[0].name, 'Bar');
+        expect(data[1].name, 'Baz');
+        expect(data[2].name, 'Baz');
+        expect(data[3].name, 'Crackle');
+        expect(data[4].name, 'Foo');
+        expect(data[5].name, 'Pop');
+        expect(data[6].name, 'Qux');
+        expect(data[7].name, 'Qux');
+        expect(data[8].name, 'Snap');
       }
       // Reverse the sort direction.
       await tester.tap(find.text('FlatName'));
@@ -264,15 +270,15 @@
 
       {
         final data = state.tableController.tableData.value.data;
-        expect(data[8].name, equals('Bar'));
-        expect(data[7].name, equals('Baz'));
-        expect(data[6].name, equals('Baz'));
-        expect(data[5].name, equals('Crackle'));
-        expect(data[4].name, equals('Foo'));
-        expect(data[3].name, equals('Pop'));
-        expect(data[2].name, equals('Qux'));
-        expect(data[1].name, equals('Qux'));
-        expect(data[0].name, equals('Snap'));
+        expect(data[8].name, 'Bar');
+        expect(data[7].name, 'Baz');
+        expect(data[6].name, 'Baz');
+        expect(data[5].name, 'Crackle');
+        expect(data[4].name, 'Foo');
+        expect(data[3].name, 'Pop');
+        expect(data[2].name, 'Qux');
+        expect(data[1].name, 'Qux');
+        expect(data[0].name, 'Snap');
       }
 
       // Change the sort column.
@@ -281,15 +287,15 @@
 
       {
         final data = state.tableController.tableData.value.data;
-        expect(data[0].name, equals('Foo'));
-        expect(data[1].name, equals('Bar'));
-        expect(data[2].name, equals('Baz'));
-        expect(data[3].name, equals('Qux'));
-        expect(data[4].name, equals('Snap'));
-        expect(data[5].name, equals('Crackle'));
-        expect(data[6].name, equals('Pop'));
-        expect(data[7].name, equals('Baz'));
-        expect(data[8].name, equals('Qux'));
+        expect(data[0].name, 'Foo');
+        expect(data[1].name, 'Bar');
+        expect(data[2].name, 'Baz');
+        expect(data[3].name, 'Qux');
+        expect(data[4].name, 'Snap');
+        expect(data[5].name, 'Crackle');
+        expect(data[6].name, 'Pop');
+        expect(data[7].name, 'Baz');
+        expect(data[8].name, 'Qux');
       }
     });
 
@@ -316,15 +322,15 @@
             tester.state(find.byWidget(table));
         {
           final data = state.tableController.tableData.value.data;
-          expect(data[0].name, equals('Bar'));
-          expect(data[1].name, equals('Baz'));
-          expect(data[2].name, equals('Baz'));
-          expect(data[3].name, equals('Crackle'));
-          expect(data[4].name, equals('Foo'));
-          expect(data[5].name, equals('Pop'));
-          expect(data[6].name, equals('Qux'));
-          expect(data[7].name, equals('Qux'));
-          expect(data[8].name, equals('Snap'));
+          expect(data[0].name, 'Bar');
+          expect(data[1].name, 'Baz');
+          expect(data[2].name, 'Baz');
+          expect(data[3].name, 'Crackle');
+          expect(data[4].name, 'Foo');
+          expect(data[5].name, 'Pop');
+          expect(data[6].name, 'Qux');
+          expect(data[7].name, 'Qux');
+          expect(data[8].name, 'Snap');
         }
 
         // Attempt to reverse the sort direction.
@@ -333,15 +339,15 @@
 
         {
           final data = state.tableController.tableData.value.data;
-          expect(data[0].name, equals('Bar'));
-          expect(data[1].name, equals('Baz'));
-          expect(data[2].name, equals('Baz'));
-          expect(data[3].name, equals('Crackle'));
-          expect(data[4].name, equals('Foo'));
-          expect(data[5].name, equals('Pop'));
-          expect(data[6].name, equals('Qux'));
-          expect(data[7].name, equals('Qux'));
-          expect(data[8].name, equals('Snap'));
+          expect(data[0].name, 'Bar');
+          expect(data[1].name, 'Baz');
+          expect(data[2].name, 'Baz');
+          expect(data[3].name, 'Crackle');
+          expect(data[4].name, 'Foo');
+          expect(data[5].name, 'Pop');
+          expect(data[6].name, 'Qux');
+          expect(data[7].name, 'Qux');
+          expect(data[8].name, 'Snap');
         }
 
         // Attempt to change the sort column.
@@ -349,15 +355,15 @@
         await tester.pumpAndSettle();
         {
           final data = state.tableController.tableData.value.data;
-          expect(data[0].name, equals('Bar'));
-          expect(data[1].name, equals('Baz'));
-          expect(data[2].name, equals('Baz'));
-          expect(data[3].name, equals('Crackle'));
-          expect(data[4].name, equals('Foo'));
-          expect(data[5].name, equals('Pop'));
-          expect(data[6].name, equals('Qux'));
-          expect(data[7].name, equals('Qux'));
-          expect(data[8].name, equals('Snap'));
+          expect(data[0].name, 'Bar');
+          expect(data[1].name, 'Baz');
+          expect(data[2].name, 'Baz');
+          expect(data[3].name, 'Crackle');
+          expect(data[4].name, 'Foo');
+          expect(data[5].name, 'Pop');
+          expect(data[6].name, 'Qux');
+          expect(data[7].name, 'Qux');
+          expect(data[8].name, 'Snap');
         }
       },
     );
@@ -393,15 +399,15 @@
             tester.state(find.byWidget(table));
         {
           final data = state.tableController.tableData.value.data;
-          expect(data[0].name, equals('Foo'));
-          expect(data[1].name, equals('1 Bar'));
-          expect(data[2].name, equals('# Baz'));
-          expect(data[3].name, equals('Qux'));
-          expect(data[4].name, equals('Bang'));
-          expect(data[5].name, equals('Crackle'));
-          expect(data[6].name, equals('Pop'));
-          expect(data[7].name, equals('Snap'));
-          expect(data[8].name, equals('Qux'));
+          expect(data[0].name, 'Foo');
+          expect(data[1].name, '1 Bar');
+          expect(data[2].name, '# Baz');
+          expect(data[3].name, 'Qux');
+          expect(data[4].name, 'Bang');
+          expect(data[5].name, 'Crackle');
+          expect(data[6].name, 'Pop');
+          expect(data[7].name, 'Snap');
+          expect(data[8].name, 'Qux');
         }
 
         // Reverse the sort direction.
@@ -410,15 +416,15 @@
 
         {
           final data = state.tableController.tableData.value.data;
-          expect(data[8].name, equals('Foo'));
-          expect(data[7].name, equals('1 Bar'));
-          expect(data[6].name, equals('# Baz'));
-          expect(data[5].name, equals('Qux'));
-          expect(data[4].name, equals('Bang'));
-          expect(data[3].name, equals('Crackle'));
-          expect(data[2].name, equals('Pop'));
-          expect(data[1].name, equals('Snap'));
-          expect(data[0].name, equals('Qux'));
+          expect(data[8].name, 'Foo');
+          expect(data[7].name, '1 Bar');
+          expect(data[6].name, '# Baz');
+          expect(data[5].name, 'Qux');
+          expect(data[4].name, 'Bang');
+          expect(data[3].name, 'Crackle');
+          expect(data[2].name, 'Pop');
+          expect(data[1].name, 'Snap');
+          expect(data[0].name, 'Qux');
         }
         // Change the sort column.
         await tester.tap(find.text('FlatName'));
@@ -426,19 +432,20 @@
 
         {
           final data = state.tableController.tableData.value.data;
-          expect(data[0].name, equals('# Baz'));
-          expect(data[1].name, equals('1 Bar'));
-          expect(data[2].name, equals('Bang'));
-          expect(data[3].name, equals('Crackle'));
-          expect(data[4].name, equals('Foo'));
-          expect(data[5].name, equals('Pop'));
-          expect(data[6].name, equals('Qux'));
-          expect(data[7].name, equals('Qux'));
-          expect(data[8].name, equals('Snap'));
+          expect(data[0].name, '# Baz');
+          expect(data[1].name, '1 Bar');
+          expect(data[2].name, 'Bang');
+          expect(data[3].name, 'Crackle');
+          expect(data[4].name, 'Foo');
+          expect(data[5].name, 'Pop');
+          expect(data[6].name, 'Qux');
+          expect(data[7].name, 'Qux');
+          expect(data[8].name, 'Snap');
         }
       },
     );
 
+    // TODO(jacobr): add a golden image tests for column width tests.
     testWidgets('displays with many columns', (WidgetTester tester) async {
       final table = FlatTable<TestData>(
         columns: [
@@ -463,291 +470,517 @@
         ),
       );
       expect(find.byWidget(table), findsOneWidget);
-      // TODO(jacobr): add a golden image test.
     });
 
-    testWidgets('displays with wide column', (WidgetTester tester) async {
-      final table = FlatTable<TestData>(
-        columns: [
-          flatNameColumn,
-          _NumberColumn(),
-          _WideColumn(),
-        ],
-        data: flatData,
-        dataKey: 'test-data',
-        keyFactory: (data) => Key(data.name),
-        defaultSortColumn: flatNameColumn,
-        defaultSortDirection: SortDirection.ascending,
-      );
-      await tester.pumpWidget(
-        wrap(
-          SizedBox(
-            width: 800.0,
-            height: 200.0,
-            child: table,
+    group('column width calculation', () {
+      Future<void> pumpTable(
+        WidgetTester tester,
+        List<ColumnData<TestData>> columns, {
+        required bool sizeColumnsToFit,
+        required ValueNotifier<Size> viewSize,
+      }) async {
+        await tester.pumpWidget(
+          wrap(
+            ValueListenableBuilder<Size>(
+              valueListenable: viewSize,
+              builder: (context, size, _) {
+                return Center(
+                  child: SizedBox(
+                    width: size.width,
+                    height: size.height,
+                    child: FlatTable<TestData>(
+                      columns: columns,
+                      data: flatData,
+                      dataKey: 'test-data',
+                      keyFactory: (data) => Key(data.name),
+                      defaultSortColumn: flatNameColumn,
+                      defaultSortDirection: SortDirection.ascending,
+                      sizeColumnsToFit: sizeColumnsToFit,
+                    ),
+                  ),
+                );
+              },
+            ),
           ),
-        ),
-      );
-      expect(find.byWidget(table), findsOneWidget);
-      {
-        final FlatTableState<TestData> state =
-            tester.state(find.byWidget(table));
-        final columnWidths =
-            state.tableController.computeColumnWidthsSizeToFit(800.0);
-        expect(columnWidths.length, equals(3));
-        expect(columnWidths[0], equals(300.0));
-        expect(columnWidths[1], equals(400.0));
-        expect(columnWidths[2], equals(36.0));
+        );
       }
 
-      // TODO(jacobr): add a golden image test.
+      group('size to fit', () {
+        testWidgetsWithWindowSize(
+          'single wide column',
+          windowSize,
+          (WidgetTester tester) async {
+            final viewSize = ValueNotifier<Size>(windowSize);
+            final columns = [
+              flatNameColumn,
+              _NumberColumn(),
+              _WideColumn(),
+            ];
+            await pumpTable(
+              tester,
+              columns,
+              sizeColumnsToFit: true,
+              viewSize: viewSize,
+            );
 
-      await tester.pumpWidget(
-        wrap(
-          SizedBox(
-            width: 200.0,
-            height: 200.0,
-            child: table,
-          ),
-        ),
-      );
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 3);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 400.0); // Fixed width column.
+              expect(columnWidths[2], 3252.0); // Variable width column.
 
-      {
-        final FlatTableState<TestData> state =
-            tester.state(find.byWidget(table));
-        final columnWidths =
-            state.tableController.computeColumnWidthsSizeToFit(200.0);
-        expect(columnWidths.length, equals(3));
-        expect(columnWidths[0], equals(300.0)); // Fixed width column.
-        expect(columnWidths[1], equals(400.0)); // Fixed width column.
-        expect(columnWidths[2], equals(0.0)); // Variable width column.
-      }
+              expect(adjustedColumnWidths.length, 3);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 400.0);
+              expect(adjustedColumnWidths[2], 3252.0);
+            }
 
-      // TODO(jacobr): add a golden image test.
+            viewSize.value = const Size(800.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 3);
+              expect(columnWidths[0], 300.0);
+              expect(columnWidths[1], 400.0);
+              expect(columnWidths[2], 52.0);
+
+              expect(adjustedColumnWidths.length, 3);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 400.0);
+              expect(adjustedColumnWidths[2], 52.0);
+            }
+
+            viewSize.value = const Size(200.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 3);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 400.0); // Fixed width column.
+              expect(columnWidths[2], 0.0); // Variable width column.
+
+              expect(adjustedColumnWidths.length, 3);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 400.0);
+              expect(adjustedColumnWidths[2], 0.0);
+            }
+          },
+        );
+
+        testWidgetsWithWindowSize(
+          'multiple wide columns',
+          windowSize,
+          (WidgetTester tester) async {
+            final viewSize = ValueNotifier<Size>(windowSize);
+            final columns = [
+              flatNameColumn,
+              _WideMinWidthColumn(),
+              _NumberColumn(),
+              _WideColumn(),
+            ];
+            await pumpTable(
+              tester,
+              columns,
+              sizeColumnsToFit: true,
+              viewSize: viewSize,
+            );
+
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 4);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 1620.0); // Min width wide column
+              expect(columnWidths[2], 400.0); // Fixed width column.
+              expect(columnWidths[3], 1620.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 4);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 1620.0);
+              expect(adjustedColumnWidths[2], 400.0);
+              expect(adjustedColumnWidths[3], 1620.0);
+            }
+
+            viewSize.value = const Size(1000.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 4);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 120.0); // Min width wide column
+              expect(columnWidths[2], 400.0); // Fixed width column.
+              expect(columnWidths[3], 120.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 4);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 120.0);
+              expect(adjustedColumnWidths[2], 400.0);
+              expect(adjustedColumnWidths[3], 120.0);
+            }
+
+            viewSize.value = const Size(200.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 4);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 100.0); // Min width wide column
+              expect(columnWidths[2], 400.0); // Fixed width column.
+              expect(columnWidths[3], 0.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 4);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 100.0);
+              expect(adjustedColumnWidths[2], 400.0);
+              expect(adjustedColumnWidths[3], 0.0);
+            }
+          },
+        );
+
+        testWidgetsWithWindowSize(
+          'multiple min width wide columns',
+          windowSize,
+          (WidgetTester tester) async {
+            final viewSize = ValueNotifier<Size>(windowSize);
+            final columns = [
+              flatNameColumn,
+              _WideMinWidthColumn(),
+              _VeryWideMinWidthColumn(),
+              _NumberColumn(),
+              _WideColumn(),
+            ];
+            await pumpTable(
+              tester,
+              columns,
+              sizeColumnsToFit: true,
+              viewSize: viewSize,
+            );
+
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 5);
+              expect(columnWidths[0], 300.0); // Fixed width column
+              expect(columnWidths[1], 1076.0); // Min width wide column
+              // Very wide min width wide column
+              expect(columnWidths[2], 1076.0);
+              expect(columnWidths[3], 400.0); // Fixed width column.
+              expect(columnWidths[4], 1076.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 5);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 1076.0);
+              expect(adjustedColumnWidths[2], 1076.0);
+              expect(adjustedColumnWidths[3], 400.0);
+              expect(adjustedColumnWidths[4], 1076.0);
+            }
+
+            viewSize.value = const Size(1501.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 5);
+              expect(columnWidths[0], 300.0); // Fixed width column
+              expect(columnWidths[1], 243.0); // Min width wide column
+              // Very wide min width wide column
+              expect(columnWidths[2], 243.0);
+              expect(columnWidths[3], 400.0); // Fixed width column.
+              expect(columnWidths[4], 243.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 5);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 243.0);
+              expect(adjustedColumnWidths[2], 243.0);
+              expect(adjustedColumnWidths[3], 400.0);
+              expect(adjustedColumnWidths[4], 243.0);
+            }
+
+            viewSize.value = const Size(1200.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 5);
+              expect(columnWidths[0], 300.0); // Fixed width column
+              expect(columnWidths[1], 134.0); // Min width wide column
+              // Very wide min width wide column
+              expect(columnWidths[2], 160.0);
+              expect(columnWidths[3], 400.0); // Fixed width column.
+              expect(columnWidths[4], 134.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 5);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 134.0);
+              expect(adjustedColumnWidths[2], 160.0);
+              expect(adjustedColumnWidths[3], 400.0);
+              expect(adjustedColumnWidths[4], 134.0);
+            }
+
+            viewSize.value = const Size(1000.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 5);
+              expect(columnWidths[0], 300.0); // Fixed width column
+              expect(columnWidths[1], 100.0); // Min width wide column
+              // Very wide min width wide column
+              expect(columnWidths[2], 160.0);
+              expect(columnWidths[3], 400.0); // Fixed width column.
+              expect(columnWidths[4], 0.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 5);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 100.0);
+              expect(adjustedColumnWidths[2], 160.0);
+              expect(adjustedColumnWidths[3], 400.0);
+              expect(adjustedColumnWidths[4], 0.0);
+            }
+          },
+        );
+      });
+
+      group('size to content', () {
+        testWidgetsWithWindowSize(
+          'single wide column',
+          windowSize,
+          (WidgetTester tester) async {
+            final viewSize = ValueNotifier<Size>(windowSize);
+            final columns = [
+              flatNameColumn,
+              _NumberColumn(),
+              _WideColumn(),
+            ];
+            await pumpTable(
+              tester,
+              columns,
+              sizeColumnsToFit: false,
+              viewSize: viewSize,
+            );
+
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 3);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 400.0); // Fixed width column.
+              expect(columnWidths[2], 1200.0); // Variable width column.
+
+              expect(adjustedColumnWidths.length, 3);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 400.0);
+              expect(adjustedColumnWidths[2], 3252.0);
+            }
+
+            viewSize.value = const Size(800.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 3);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 400.0); // Fixed width column.
+              expect(columnWidths[2], 1200.0); // Variable width column.
+
+              expect(adjustedColumnWidths.length, 3);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 400.0);
+              expect(adjustedColumnWidths[2], 1200.0);
+            }
+
+            viewSize.value = const Size(200.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 3);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 400.0); // Fixed width column.
+              expect(columnWidths[2], 1200.0); // Variable width column.
+
+              expect(adjustedColumnWidths.length, 3);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 400.0);
+              expect(adjustedColumnWidths[2], 1200.0);
+            }
+          },
+        );
+
+        testWidgetsWithWindowSize(
+          'multiple wide columns',
+          windowSize,
+          (WidgetTester tester) async {
+            final viewSize = ValueNotifier<Size>(windowSize);
+            final columns = [
+              flatNameColumn,
+              _WideMinWidthColumn(),
+              _NumberColumn(),
+              _WideColumn(),
+            ];
+            await pumpTable(
+              tester,
+              columns,
+              sizeColumnsToFit: false,
+              viewSize: viewSize,
+            );
+
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 4);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 100.0); // Min width wide column
+              expect(columnWidths[2], 400.0); // Fixed width column.
+              expect(columnWidths[3], 1200.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 4);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 1620.0);
+              expect(adjustedColumnWidths[2], 400.0);
+              expect(adjustedColumnWidths[3], 1620.0);
+            }
+
+            viewSize.value = const Size(1000.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 4);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 100.0); // Min width wide column
+              expect(columnWidths[2], 400.0); // Fixed width column.
+              expect(columnWidths[3], 1200.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 4);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 100.0);
+              expect(adjustedColumnWidths[2], 400.0);
+              expect(adjustedColumnWidths[3], 1200.0);
+            }
+
+            viewSize.value = const Size(200.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 4);
+              expect(columnWidths[0], 300.0); // Fixed width column.
+              expect(columnWidths[1], 100.0); // Min width wide column
+              expect(columnWidths[2], 400.0); // Fixed width column.
+              expect(columnWidths[3], 1200.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 4);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 100.0);
+              expect(adjustedColumnWidths[2], 400.0);
+              expect(adjustedColumnWidths[3], 1200.0);
+            }
+          },
+        );
+
+        testWidgetsWithWindowSize(
+          'multiple min width wide columns',
+          windowSize,
+          (WidgetTester tester) async {
+            final viewSize = ValueNotifier<Size>(windowSize);
+            final columns = [
+              flatNameColumn,
+              _WideMinWidthColumn(),
+              _VeryWideMinWidthColumn(),
+              _NumberColumn(),
+              _WideColumn(),
+            ];
+            await pumpTable(
+              tester,
+              columns,
+              sizeColumnsToFit: false,
+              viewSize: viewSize,
+            );
+
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths.length, 5);
+              expect(columnWidths[0], 300.0); // Fixed width column
+              expect(columnWidths[1], 100.0); // Min width wide column
+              expect(columnWidths[2], 160.0); // Very wide min width wide column
+              expect(columnWidths[3], 400.0); // Fixed width column.
+              expect(columnWidths[4], 1200.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 5);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 1014.0);
+              expect(adjustedColumnWidths[2], 1014.0);
+              expect(adjustedColumnWidths[3], 400.0);
+              expect(adjustedColumnWidths[4], 1200.0);
+            }
+
+            viewSize.value = const Size(1200.0, 200.0);
+            await tester.pumpAndSettle();
+            {
+              final DevToolsTableState<TestData> tableState =
+                  tester.state(find.byType(DevToolsTable<TestData>));
+              final columnWidths = tableState.widget.columnWidths;
+              final adjustedColumnWidths = tableState.adjustedColumnWidths;
+              expect(columnWidths[0], 300.0); // Fixed width column
+              expect(columnWidths[1], 100.0); // Min width wide column
+              expect(columnWidths[2], 160.0); // Very wide min width wide column
+              expect(columnWidths[3], 400.0); // Fixed width column.
+              expect(columnWidths[4], 1200.0); // Variable width wide column.
+
+              expect(adjustedColumnWidths.length, 5);
+              expect(adjustedColumnWidths[0], 300.0);
+              expect(adjustedColumnWidths[1], 100.0);
+              expect(adjustedColumnWidths[2], 160.0);
+              expect(adjustedColumnWidths[3], 400.0);
+              expect(adjustedColumnWidths[4], 1200.0);
+            }
+          },
+        );
+      });
     });
 
-    testWidgets(
-      'displays with wide column size to content',
-      (WidgetTester tester) async {
-        final table = FlatTable<TestData>(
-          columns: [
-            flatNameColumn,
-            _NumberColumn(),
-            _WideColumn(),
-          ],
-          data: flatData,
-          dataKey: 'test-data',
-          keyFactory: (data) => Key(data.name),
-          defaultSortColumn: flatNameColumn,
-          defaultSortDirection: SortDirection.ascending,
-          sizeColumnsToFit: false,
-        );
-        await tester.pumpWidget(
-          wrap(
-            SizedBox(
-              width: 800.0,
-              height: 200.0,
-              child: table,
-            ),
-          ),
-        );
-        expect(find.byWidget(table), findsOneWidget);
-        {
-          final FlatTableState<TestData> state =
-              tester.state(find.byWidget(table));
-          expect(state.tableController.columnWidths, isNotNull);
-          final columnWidths = state.tableController.columnWidths!;
-          expect(columnWidths.length, equals(3));
-          expect(columnWidths[0], equals(300.0));
-          expect(columnWidths[1], equals(400.0));
-          expect(columnWidths[2], equals(369.0));
-        }
-
-        // TODO(jacobr): add a golden image test.
-
-        await tester.pumpWidget(
-          wrap(
-            SizedBox(
-              width: 200.0,
-              height: 200.0,
-              child: table,
-            ),
-          ),
-        );
-
-        {
-          final FlatTableState<TestData> state =
-              tester.state(find.byWidget(table));
-          expect(state.tableController.columnWidths, isNotNull);
-          final columnWidths = state.tableController.columnWidths!;
-          expect(columnWidths.length, equals(3));
-          expect(columnWidths[0], equals(300.0)); // Fixed width column.
-          expect(columnWidths[1], equals(400.0)); // Fixed width column.
-          expect(columnWidths[2], equals(369.0)); // Variable width column.
-        }
-
-        // TODO(jacobr): add a golden image test.
-      },
-    );
-
-    testWidgets(
-      'displays with multiple wide columns',
-      (WidgetTester tester) async {
-        final table = FlatTable<TestData>(
-          columns: [
-            flatNameColumn,
-            _WideMinWidthColumn(),
-            _NumberColumn(),
-            _WideColumn(),
-          ],
-          data: flatData,
-          dataKey: 'test-data',
-          keyFactory: (data) => Key(data.name),
-          defaultSortColumn: flatNameColumn,
-          defaultSortDirection: SortDirection.ascending,
-        );
-        await tester.pumpWidget(
-          wrap(
-            SizedBox(
-              width: 1000.0,
-              height: 200.0,
-              child: table,
-            ),
-          ),
-        );
-        expect(find.byWidget(table), findsOneWidget);
-        {
-          final FlatTableState<TestData> state =
-              tester.state(find.byWidget(table));
-          final columnWidths =
-              state.tableController.computeColumnWidthsSizeToFit(1000.0);
-          expect(columnWidths.length, equals(4));
-          expect(columnWidths[0], equals(300.0)); // Fixed width column.
-          expect(columnWidths[1], equals(110.0)); // Min width wide column
-          expect(columnWidths[2], equals(400.0)); // Fixed width column.
-          expect(columnWidths[3], equals(110.0)); // Variable width wide column.
-        }
-
-        await tester.pumpWidget(
-          wrap(
-            SizedBox(
-              width: 200.0,
-              height: 200.0,
-              child: table,
-            ),
-          ),
-        );
-        {
-          final FlatTableState<TestData> state =
-              tester.state(find.byWidget(table));
-          final columnWidths =
-              state.tableController.computeColumnWidthsSizeToFit(200.0);
-          expect(columnWidths.length, equals(4));
-          expect(columnWidths[0], equals(300.0)); // Fixed width column.
-          expect(columnWidths[1], equals(100.0)); // Min width wide column
-          expect(columnWidths[2], equals(400.0)); // Fixed width column.
-          expect(columnWidths[3], equals(0.0)); // Variable width wide column.
-        }
-      },
-    );
-
-    testWidgets(
-      'displays with multiple min width wide columns',
-      (WidgetTester tester) async {
-        final table = FlatTable<TestData>(
-          columns: [
-            flatNameColumn,
-            _WideMinWidthColumn(),
-            _VeryWideMinWidthColumn(),
-            _NumberColumn(),
-            _WideColumn(),
-          ],
-          data: flatData,
-          dataKey: 'test-data',
-          keyFactory: (data) => Key(data.name),
-          defaultSortColumn: flatNameColumn,
-          defaultSortDirection: SortDirection.ascending,
-        );
-        await tester.pumpWidget(
-          wrap(
-            SizedBox(
-              width: 1501.0,
-              height: 200.0,
-              child: table,
-            ),
-          ),
-        );
-        expect(find.byWidget(table), findsOneWidget);
-        {
-          final FlatTableState<TestData> state =
-              tester.state(find.byWidget(table));
-          final columnWidths =
-              state.tableController.computeColumnWidthsSizeToFit(1501.0);
-          expect(columnWidths.length, equals(5));
-          expect(columnWidths[0], equals(300.0)); // Fixed width column.
-          expect(columnWidths[1], equals(235.0)); // Min width wide column
-          expect(
-            columnWidths[2],
-            equals(235.0),
-          ); // Very wide min width wide column
-          expect(columnWidths[3], equals(400.0)); // Fixed width column.
-          expect(columnWidths[4], equals(235.0)); // Variable width wide column.
-        }
-
-        await tester.pumpWidget(
-          wrap(
-            SizedBox(
-              width: 1200.0,
-              height: 200.0,
-              child: table,
-            ),
-          ),
-        );
-        expect(find.byWidget(table), findsOneWidget);
-        {
-          final FlatTableState<TestData> state =
-              tester.state(find.byWidget(table));
-          final columnWidths =
-              state.tableController.computeColumnWidthsSizeToFit(1200.0);
-          expect(columnWidths.length, equals(5));
-          expect(columnWidths[0], equals(300.0)); // Fixed width column.
-          expect(columnWidths[1], equals(122.0)); // Min width wide column
-          expect(
-            columnWidths[2],
-            equals(160.0),
-          ); // Very wide min width wide column
-          expect(columnWidths[3], equals(400.0)); // Fixed width column.
-          expect(columnWidths[4], equals(122.0)); // Variable width wide column.
-        }
-
-        await tester.pumpWidget(
-          wrap(
-            SizedBox(
-              width: 1000.0,
-              height: 200.0,
-              child: table,
-            ),
-          ),
-        );
-        expect(find.byWidget(table), findsOneWidget);
-        {
-          final FlatTableState<TestData> state =
-              tester.state(find.byWidget(table));
-          final columnWidths =
-              state.tableController.computeColumnWidthsSizeToFit(1000.0);
-          expect(columnWidths.length, equals(5));
-          expect(columnWidths[0], equals(300.0)); // Fixed width column.
-          expect(columnWidths[1], equals(100.0)); // Min width wide column
-          expect(
-            columnWidths[2],
-            equals(160.0),
-          ); // Very wide min width wide column
-          expect(columnWidths[3], equals(400.0)); // Fixed width column.
-          expect(columnWidths[4], equals(0.0)); // Variable width wide column.
-        }
-      },
-    );
-
     testWidgets('can select an item', (WidgetTester tester) async {
       TestData? selected;
       final testData = TestData('empty', 0);
@@ -1187,14 +1420,14 @@
     );
 
     testWidgets('starts with sorted data', (WidgetTester tester) async {
-      expect(tree1.children[0].name, equals('Bar'));
-      expect(tree1.children[0].children[0].name, equals('Baz'));
-      expect(tree1.children[0].children[1].name, equals('Qux'));
-      expect(tree1.children[0].children[2].name, equals('Snap'));
-      expect(tree1.children[0].children[3].name, equals('Crackle'));
-      expect(tree1.children[0].children[4].name, equals('Pop'));
-      expect(tree1.children[1].name, equals('Baz'));
-      expect(tree1.children[2].name, equals('Qux'));
+      expect(tree1.children[0].name, 'Bar');
+      expect(tree1.children[0].children[0].name, 'Baz');
+      expect(tree1.children[0].children[1].name, 'Qux');
+      expect(tree1.children[0].children[2].name, 'Snap');
+      expect(tree1.children[0].children[3].name, 'Crackle');
+      expect(tree1.children[0].children[4].name, 'Pop');
+      expect(tree1.children[1].name, 'Baz');
+      expect(tree1.children[2].name, 'Qux');
       final table = TreeTable<TestData>(
         columns: [
           _NumberColumn(),
@@ -1210,66 +1443,71 @@
       await tester.pumpWidget(wrap(table));
       final TreeTableState<TestData> state = tester.state(find.byWidget(table));
       final tree = state.tableController.dataRoots[0];
-      expect(tree.children[0].name, equals('Bar'));
-      expect(tree.children[0].children[0].name, equals('Baz'));
-      expect(tree.children[0].children[1].name, equals('Crackle'));
-      expect(tree.children[0].children[2].name, equals('Pop'));
-      expect(tree.children[0].children[3].name, equals('Qux'));
-      expect(tree.children[0].children[4].name, equals('Snap'));
-      expect(tree.children[1].name, equals('Baz'));
-      expect(tree.children[2].name, equals('Qux'));
+      expect(tree.children[0].name, 'Bar');
+      expect(tree.children[0].children[0].name, 'Baz');
+      expect(tree.children[0].children[1].name, 'Crackle');
+      expect(tree.children[0].children[2].name, 'Pop');
+      expect(tree.children[0].children[3].name, 'Qux');
+      expect(tree.children[0].children[4].name, 'Snap');
+      expect(tree.children[1].name, 'Baz');
+      expect(tree.children[2].name, 'Qux');
     });
 
-    testWidgets('sorts data by column', (WidgetTester tester) async {
-      final table = TreeTable<TestData>(
-        columns: [
-          _NumberColumn(),
-          treeColumn,
-        ],
-        dataRoots: [tree1],
-        dataKey: 'test-data',
-        treeColumn: treeColumn,
-        keyFactory: (d) => Key(d.name),
-        defaultSortColumn: treeColumn,
-        defaultSortDirection: SortDirection.ascending,
-      );
-      await tester.pumpWidget(wrap(table));
-      final TreeTableState<TestData> state = tester.state(find.byWidget(table));
-      expect(state.tableController.columnWidths![0], equals(400));
-      expect(state.tableController.columnWidths![1], equals(81));
-      final tree = state.tableController.dataRoots[0];
-      expect(tree.children[0].name, equals('Bar'));
-      expect(tree.children[0].children[0].name, equals('Baz'));
-      expect(tree.children[0].children[1].name, equals('Crackle'));
-      expect(tree.children[0].children[2].name, equals('Pop'));
-      expect(tree.children[0].children[4].name, equals('Snap'));
-      expect(tree.children[1].name, equals('Baz'));
-      expect(tree.children[2].name, equals('Qux'));
+    testWidgetsWithWindowSize(
+      'sorts data by column',
+      windowSize,
+      (WidgetTester tester) async {
+        final table = TreeTable<TestData>(
+          columns: [
+            _NumberColumn(),
+            treeColumn,
+          ],
+          dataRoots: [tree1],
+          dataKey: 'test-data',
+          treeColumn: treeColumn,
+          keyFactory: (d) => Key(d.name),
+          defaultSortColumn: treeColumn,
+          defaultSortDirection: SortDirection.ascending,
+        );
+        await tester.pumpWidget(wrap(table));
+        final TreeTableState<TestData> state =
+            tester.state(find.byWidget(table));
+        expect(state.tableController.columnWidths![0], 400);
+        expect(state.tableController.columnWidths![1], 1242.0);
+        final tree = state.tableController.dataRoots[0];
+        expect(tree.children[0].name, 'Bar');
+        expect(tree.children[0].children[0].name, 'Baz');
+        expect(tree.children[0].children[1].name, 'Crackle');
+        expect(tree.children[0].children[2].name, 'Pop');
+        expect(tree.children[0].children[4].name, 'Snap');
+        expect(tree.children[1].name, 'Baz');
+        expect(tree.children[2].name, 'Qux');
 
-      // Reverse the sort direction.
-      await tester.tap(find.text('Name'));
-      await tester.pumpAndSettle();
-      expect(tree.children[2].name, equals('Bar'));
-      expect(tree.children[2].children[4].name, equals('Baz'));
-      expect(tree.children[2].children[3].name, equals('Crackle'));
-      expect(tree.children[2].children[2].name, equals('Pop'));
-      expect(tree.children[2].children[1].name, equals('Qux'));
-      expect(tree.children[2].children[0].name, equals('Snap'));
-      expect(tree.children[1].name, equals('Baz'));
-      expect(tree.children[0].name, equals('Qux'));
+        // Reverse the sort direction.
+        await tester.tap(find.text('Name'));
+        await tester.pumpAndSettle();
+        expect(tree.children[2].name, 'Bar');
+        expect(tree.children[2].children[4].name, 'Baz');
+        expect(tree.children[2].children[3].name, 'Crackle');
+        expect(tree.children[2].children[2].name, 'Pop');
+        expect(tree.children[2].children[1].name, 'Qux');
+        expect(tree.children[2].children[0].name, 'Snap');
+        expect(tree.children[1].name, 'Baz');
+        expect(tree.children[0].name, 'Qux');
 
-      // Change the sort column.
-      await tester.tap(find.text('Number'));
-      await tester.pumpAndSettle();
-      expect(tree.children[0].name, equals('Bar'));
-      expect(tree.children[0].children[0].name, equals('Baz'));
-      expect(tree.children[0].children[1].name, equals('Qux'));
-      expect(tree.children[0].children[2].name, equals('Snap'));
-      expect(tree.children[0].children[3].name, equals('Pop'));
-      expect(tree.children[0].children[4].name, equals('Crackle'));
-      expect(tree.children[1].name, equals('Qux'));
-      expect(tree.children[2].name, equals('Baz'));
-    });
+        // Change the sort column.
+        await tester.tap(find.text('Number'));
+        await tester.pumpAndSettle();
+        expect(tree.children[0].name, 'Bar');
+        expect(tree.children[0].children[0].name, 'Baz');
+        expect(tree.children[0].children[1].name, 'Qux');
+        expect(tree.children[0].children[2].name, 'Snap');
+        expect(tree.children[0].children[3].name, 'Pop');
+        expect(tree.children[0].children[4].name, 'Crackle');
+        expect(tree.children[1].name, 'Qux');
+        expect(tree.children[2].name, 'Baz');
+      },
+    );
 
     group('keyboard navigation', () {
       late TestData data;
@@ -1307,7 +1545,7 @@
           state.focusNode!.requestFocus();
           await tester.pumpAndSettle();
 
-          expect(state.widget.selectionNotifier.value.node, equals(null));
+          expect(state.widget.selectionNotifier.value.node, null);
 
           // the root is selected by default when there is no selection. Pressing
           // arrowDown should take us to the first child, Bar
@@ -1315,13 +1553,13 @@
           await tester.pumpAndSettle();
           expect(
             state.widget.selectionNotifier.value.node,
-            equals(data.children[0]),
+            data.children[0],
           );
 
           await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
           await tester.pumpAndSettle();
 
-          expect(state.widget.selectionNotifier.value.node, equals(data.root));
+          expect(state.widget.selectionNotifier.value.node, data.root);
         },
       );
 
@@ -1340,7 +1578,7 @@
           await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
           await tester.pumpAndSettle();
 
-          expect(state.widget.selectionNotifier.value.node, equals(data.root));
+          expect(state.widget.selectionNotifier.value.node, data.root);
           expect(
             state.widget.selectionNotifier.value.node!.isExpanded,
             isFalse,
@@ -1354,14 +1592,14 @@
 
           expect(
             state.widget.selectionNotifier.value.node,
-            equals(data.root.children[1]),
+            data.root.children[1],
           );
 
           // Back to parent
           await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
           await tester.pumpAndSettle();
 
-          expect(state.widget.selectionNotifier.value.node, equals(data.root));
+          expect(state.widget.selectionNotifier.value.node, data.root);
           expect(state.widget.selectionNotifier.value.node!.isExpanded, isTrue);
         },
       );
@@ -1423,12 +1661,12 @@
         final TableRow snapRow = tester.widget(snapFinder);
         TableRow crackleRow = tester.widget(crackleFinder);
 
-        expect(fooRow.backgroundColor!.value, equals(color1Value));
-        expect(barRow.backgroundColor!.value, equals(color2Value));
-        expect(bazRow.backgroundColor!.value, equals(color1Value));
-        expect(quxRow.backgroundColor!.value, equals(color2Value));
-        expect(snapRow.backgroundColor!.value, equals(color1Value));
-        expect(crackleRow.backgroundColor!.value, equals(color2Value));
+        expect(fooRow.backgroundColor!.value, color1Value);
+        expect(barRow.backgroundColor!.value, color2Value);
+        expect(bazRow.backgroundColor!.value, color1Value);
+        expect(quxRow.backgroundColor!.value, color2Value);
+        expect(snapRow.backgroundColor!.value, color1Value);
+        expect(crackleRow.backgroundColor!.value, color2Value);
 
         await tester.tap(barFinder);
         await tester.pumpAndSettle();
@@ -1442,12 +1680,12 @@
         barRow = tester.widget(barFinder);
         crackleRow = tester.widget(crackleFinder);
 
-        expect(fooRow.backgroundColor!.value, equals(color1Value));
+        expect(fooRow.backgroundColor!.value, color1Value);
         // [barRow] has the rowSelected color after being tapped.
-        expect(barRow.backgroundColor!.value, equals(rowSelectedColorValue));
+        expect(barRow.backgroundColor!.value, rowSelectedColorValue);
         // [crackleRow] has a different background color after collapsing previous
         // row (Bar).
-        expect(crackleRow.backgroundColor!.value, equals(color1Value));
+        expect(crackleRow.backgroundColor!.value, color1Value);
       },
     );
 
diff --git a/packages/devtools_app/test/standalone_ui/vs_code/debug_sessions_test.dart b/packages/devtools_app/test/standalone_ui/vs_code/debug_sessions_test.dart
new file mode 100644
index 0000000..45a9646
--- /dev/null
+++ b/packages/devtools_app/test/standalone_ui/vs_code/debug_sessions_test.dart
@@ -0,0 +1,274 @@
+// Copyright 2024 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:io';
+
+import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/shared/constants.dart';
+import 'package:devtools_app/src/standalone_ui/api/impl/vs_code_api.dart';
+import 'package:devtools_app/src/standalone_ui/vs_code/debug_sessions.dart';
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:devtools_test/devtools_test.dart';
+import 'package:devtools_test/helpers.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../test_infra/test_data/dart_tooling_api/mock_api.dart';
+
+void main() {
+  const windowSize = Size(2000.0, 2000.0);
+
+  late MockVsCodeApi mockVsCodeApi;
+  late final Map<String, VsCodeDevice> deviceMap;
+
+  setUpAll(() {
+    // Set test mode so that the debug list of extensions will be used.
+    setTestMode();
+
+    final devices = stubbedDevices.map((d) => MapEntry(d.id, d));
+    deviceMap = {for (final d in devices) d.key: d.value};
+  });
+
+  setUp(() {
+    mockVsCodeApi = MockVsCodeApi();
+    when(mockVsCodeApi.capabilities).thenReturn(
+      VsCodeCapabilitiesImpl({
+        'executeCommand': true,
+        'selectDevice': true,
+        'openDevToolsPage': true,
+        'openDevToolsExternally': true,
+        'hotReload': true,
+        'hotRestart': true,
+      }),
+    );
+    setGlobal(IdeTheme, IdeTheme());
+    setGlobal(PreferencesController, PreferencesController());
+  });
+
+  Future<void> pumpDebugSessions(WidgetTester tester) async {
+    await tester.pumpWidget(
+      wrap(
+        DebugSessions(
+          api: mockVsCodeApi,
+          sessions: _debugSessions,
+          deviceMap: deviceMap,
+        ),
+      ),
+    );
+  }
+
+  group('$DebugSessions', () {
+    Finder iconButtonFinder(IconData icon, {required int index}) {
+      return find
+          .byWidgetPredicate(
+            (widget) =>
+                widget.runtimeType == IconButton &&
+                ((widget as IconButton).icon as Icon).icon == icon,
+          )
+          .at(index);
+    }
+
+    void verifyDebugSessionState(
+      WidgetTester tester, {
+      required int debugSessionIndex,
+      required String sessionDisplayText,
+      required bool hotButtonsEnabled,
+      required bool devtoolsButtonEnabled,
+    }) {
+      expect(find.text(sessionDisplayText), findsOneWidget);
+
+      final hotReloadButtonFinder =
+          iconButtonFinder(hotReloadIcon, index: debugSessionIndex);
+      final hotRestartButtonFinder =
+          iconButtonFinder(hotRestartIcon, index: debugSessionIndex);
+      final devtoolsButtonFinder =
+          iconButtonFinder(Icons.construction, index: debugSessionIndex);
+      expect(hotReloadButtonFinder, findsOneWidget);
+      expect(hotRestartButtonFinder, findsOneWidget);
+      expect(devtoolsButtonFinder, findsOneWidget);
+
+      final hotReloadButton =
+          tester.widget(hotReloadButtonFinder) as IconButton;
+      final hotRestartButton =
+          tester.widget(hotRestartButtonFinder) as IconButton;
+      final devtoolsMenuButton =
+          tester.widget(devtoolsButtonFinder) as IconButton;
+      expect(
+        hotReloadButton.onPressed,
+        hotButtonsEnabled ? isNotNull : isNull,
+      );
+      expect(
+        hotRestartButton.onPressed,
+        hotButtonsEnabled ? isNotNull : isNull,
+      );
+      expect(
+        devtoolsMenuButton.onPressed,
+        devtoolsButtonEnabled ? isNotNull : isNull,
+      );
+    }
+
+    final tests = [
+      (
+        sessionDisplay: 'Session (Flutter) (macos) (debug)',
+        hotButtonsEnabled: true,
+        devtoolsButtonEnabled: true,
+      ),
+      (
+        sessionDisplay: 'Session (Flutter) (macos) (profile)',
+        hotButtonsEnabled: false,
+        devtoolsButtonEnabled: true,
+      ),
+      (
+        sessionDisplay: 'Session (Flutter) (macos) (release)',
+        hotButtonsEnabled: false,
+        devtoolsButtonEnabled: false,
+      ),
+      (
+        sessionDisplay: 'Session (Flutter) (macos) (jit_release)',
+        hotButtonsEnabled: false,
+        devtoolsButtonEnabled: false,
+      ),
+      (
+        sessionDisplay: 'Session (Flutter) (chrome) (debug)',
+        hotButtonsEnabled: true,
+        devtoolsButtonEnabled: true,
+      ),
+      (
+        sessionDisplay: 'Session (Flutter) (chrome) (profile)',
+        hotButtonsEnabled: false,
+        devtoolsButtonEnabled: true,
+      ),
+      (
+        sessionDisplay: 'Session (Flutter) (chrome) (release)',
+        hotButtonsEnabled: false,
+        devtoolsButtonEnabled: false,
+      ),
+      (
+        sessionDisplay: 'Session (Dart) (macos)',
+        hotButtonsEnabled: true,
+        devtoolsButtonEnabled: true,
+      ),
+    ];
+
+    testWidgetsWithWindowSize(
+      'rows render properly for run mode',
+      windowSize,
+      (tester) async {
+        await pumpDebugSessions(tester);
+        await tester.pump(const Duration(milliseconds: 500));
+        for (var i = 0; i < tests.length; i++) {
+          final test = tests[i];
+          // ignore: avoid_print, defines individual test case.
+          print('testing: ${test.sessionDisplay}');
+          verifyDebugSessionState(
+            tester,
+            debugSessionIndex: i,
+            sessionDisplayText: test.sessionDisplay,
+            hotButtonsEnabled: test.hotButtonsEnabled,
+            devtoolsButtonEnabled: test.devtoolsButtonEnabled,
+          );
+        }
+      },
+    );
+
+    testWidgetsWithWindowSize(
+      'DevTools dropdown contains extensions submenu',
+      windowSize,
+      (tester) async {
+        await pumpDebugSessions(tester);
+        await tester.pump(const Duration(milliseconds: 500));
+
+        // Index 0 so that we are checking a debug desktop session, where the
+        // DevTools button is enabled.
+        final devtoolsButtonFinder =
+            iconButtonFinder(Icons.construction, index: 0);
+        expect(devtoolsButtonFinder, findsOneWidget);
+        await tester.tap(devtoolsButtonFinder);
+        await tester.pumpAndSettle();
+
+        final extensionsSubmenuButtonFinder = find.text('Extensions');
+        expect(extensionsSubmenuButtonFinder, findsOneWidget);
+
+        // We should not see the extensions in the dropdown menu at this point.
+        expect(find.text('bar'), findsNothing);
+        expect(find.text('foo'), findsNothing);
+        expect(find.text('provider'), findsNothing);
+
+        final hoverLocation = tester.getCenter(extensionsSubmenuButtonFinder);
+        await tester.startGesture(hoverLocation, kind: PointerDeviceKind.mouse);
+        await tester.pumpAndSettle();
+
+        // We should now see the extensions in the dropdown menu.
+        expect(find.text('bar'), findsOneWidget);
+        expect(find.text('foo'), findsOneWidget);
+        expect(find.text('provider'), findsOneWidget);
+      },
+    );
+  });
+}
+
+final _debugSessions = <VsCodeDebugSession>[
+  // Flutter native apps.
+  generateDebugSession(
+    debuggerType: 'Flutter',
+    deviceId: 'macos',
+    flutterMode: 'debug',
+  ),
+  generateDebugSession(
+    debuggerType: 'Flutter',
+    deviceId: 'macos',
+    flutterMode: 'profile',
+  ),
+  generateDebugSession(
+    debuggerType: 'Flutter',
+    deviceId: 'macos',
+    flutterMode: 'release',
+  ),
+  generateDebugSession(
+    debuggerType: 'Flutter',
+    deviceId: 'macos',
+    flutterMode: 'jit_release',
+  ),
+  // Flutter web apps.
+  generateDebugSession(
+    debuggerType: 'Flutter',
+    deviceId: 'chrome',
+    flutterMode: 'debug',
+  ),
+  generateDebugSession(
+    debuggerType: 'Flutter',
+    deviceId: 'chrome',
+    flutterMode: 'profile',
+  ),
+  generateDebugSession(
+    debuggerType: 'Flutter',
+    deviceId: 'chrome',
+    flutterMode: 'release',
+  ),
+  // Dart CLI app.
+  generateDebugSession(
+    debuggerType: 'Dart',
+    deviceId: 'macos',
+  ),
+];
+
+VsCodeDebugSession generateDebugSession({
+  required String debuggerType,
+  required String deviceId,
+  String? flutterMode,
+}) {
+  return VsCodeDebugSessionImpl(
+    id: '$debuggerType-$deviceId-$flutterMode',
+    name: 'Session ($debuggerType) ($deviceId)',
+    vmServiceUri: 'ws://127.0.0.1:1234/ws',
+    flutterMode: flutterMode,
+    flutterDeviceId: deviceId,
+    debuggerType: debuggerType,
+    projectRootPath:
+        Platform.isWindows ? r'C:\mock\root\path' : '/mock/root/path',
+  );
+}
diff --git a/packages/devtools_app/test/test_infra/fixtures/memory_app/lib/main.dart b/packages/devtools_app/test/test_infra/fixtures/memory_app/lib/main.dart
index 44adde9..efaaccd 100644
--- a/packages/devtools_app/test/test_infra/fixtures/memory_app/lib/main.dart
+++ b/packages/devtools_app/test/test_infra/fixtures/memory_app/lib/main.dart
@@ -105,7 +105,7 @@
       };
 
       map = {
-        for (var _ in Iterable.generate(_width))
+        for (var _ in Iterable<void>.generate(_width))
           createInstance(): createInstance(),
       };
 
diff --git a/packages/devtools_app/test/test_infra/flutter_test_environment.dart b/packages/devtools_app/test/test_infra/flutter_test_environment.dart
index ccdb9f0..a9a66f2 100644
--- a/packages/devtools_app/test/test_infra/flutter_test_environment.dart
+++ b/packages/devtools_app/test/test_infra/flutter_test_environment.dart
@@ -150,7 +150,7 @@
 
         await serviceConnection.serviceManager.vmServiceOpened(
           _service,
-          onClosed: Completer().future,
+          onClosed: Completer<void>().future,
         );
         await _preferencesController!.init();
 
diff --git a/packages/devtools_app/test/test_infra/goldens/codeview_scrollbars.png b/packages/devtools_app/test/test_infra/goldens/codeview_scrollbars.png
index 82785bc..2360645 100644
--- a/packages/devtools_app/test/test_infra/goldens/codeview_scrollbars.png
+++ b/packages/devtools_app/test/test_infra/goldens/codeview_scrollbars.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_no_selection.png b/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_no_selection.png
index 15c89ae..e7af32e 100644
--- a/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_no_selection.png
+++ b/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_no_selection.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_with_selection.png b/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_with_selection.png
index 1221166..6713043 100644
--- a/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_with_selection.png
+++ b/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_with_selection.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_with_selection_2.png b/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_with_selection_2.png
index 7f505a0..49275e3 100644
--- a/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_with_selection_2.png
+++ b/packages/devtools_app/test/test_infra/goldens/cpu_profiler/method_table_with_selection_2.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/extensions/settings_state_modified.png b/packages/devtools_app/test/test_infra/goldens/extensions/settings_state_modified.png
index e1661d7..904e67a 100644
--- a/packages/devtools_app/test/test_infra/goldens/extensions/settings_state_modified.png
+++ b/packages/devtools_app/test/test_infra/goldens/extensions/settings_state_modified.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/extensions/settings_state_none.png b/packages/devtools_app/test/test_infra/goldens/extensions/settings_state_none.png
index 4b51d13..4e5075b 100644
--- a/packages/devtools_app/test/test_infra/goldens/extensions/settings_state_none.png
+++ b/packages/devtools_app/test/test_infra/goldens/extensions/settings_state_none.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/bool.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/bool.png
index 8ecd3c1..df141d3 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/bool.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/bool.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_list.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_list.png
index 1e14b54..fc97819 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_list.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_list.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_map.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_map.png
index 98f5c08..e087e68 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_map.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_map.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_object.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_object.png
index 6e35fe3..968a8eb 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_object.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/collasped_object.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/edit.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/edit.png
index 1e3b133..9f2c810 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/edit.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/edit.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/edit_esc.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/edit_esc.png
index b814dfb..da1918f 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/edit_esc.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/edit_esc.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/enum.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/enum.png
index 15d6758..ce7361e 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/enum.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/enum.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/error.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/error.png
index 7a223b6..24c7adc 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/error.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/error.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_list.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_list.png
index 55c8ec8..c6e6e3a 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_list.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_list.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_map.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_map.png
index d4bc793..53332e9 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_map.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_map.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_object.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_object.png
index b086e39..4ea1ec4 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_object.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/expanded_object.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/loading.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/loading.png
index 59d07de..c8d9f3a 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/loading.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/loading.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/null.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/null.png
index b3d6eff..35ea404 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/null.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/null.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/num.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/num.png
index d9f566c..1716645 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/num.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/num.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/show_internal_properties.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/show_internal_properties.png
index ce7cb29..008b5d3 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/show_internal_properties.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/show_internal_properties.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/instance_viewer/string.png b/packages/devtools_app/test/test_infra/goldens/instance_viewer/string.png
index 9cc62fe..34335d7 100644
--- a/packages/devtools_app/test/test_infra/goldens/instance_viewer/string.png
+++ b/packages/devtools_app/test/test_infra/goldens/instance_viewer/string.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_animated_physical_model_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_animated_physical_model_selected.png
index 04ec1af..96e0b31 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_animated_physical_model_selected.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_animated_physical_model_selected.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png
index c50c450..1064580 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png
index 4874110..b6652cc 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png
index 0da4f30..1d80bdd 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_richtext_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_richtext_selected.png
index d2a8b3a..c9c3b3a 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_richtext_selected.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_richtext_selected.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_scaffold_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_scaffold_selected.png
index cc9ce01..b3c4024 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_scaffold_selected.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_scaffold_selected.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png
index 293c14f..995c7b6 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center_details_tree.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center_details_tree.png
index 7b6cf4a..3cab036 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center_details_tree.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center_details_tree.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_empty1.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_empty1.png
index 75d844b..8f259c2 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_empty1.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_empty1.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_empty2.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_empty2.png
index 75d844b..8f259c2 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_empty2.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_empty2.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_custom.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_custom.png
index 6d13870..8139b9d 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_custom.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_custom.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_except.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_except.png
index 6015444..bfae5c0 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_except.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_except.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_only.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_only.png
index 6d13870..8139b9d 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_only.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_only.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_showAll.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_showAll.png
index 3958bd2..ab9bfde 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_showAll.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_filter_dialog_showAll.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_selected_class.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_selected_class.png
index ee7bdac..ae197a2 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_selected_class.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_selected_class.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_custom_diff.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_custom_diff.png
index 135ad82..1cbe96f 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_custom_diff.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_custom_diff.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_custom_single.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_custom_single.png
index 65db92e..859609b 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_custom_single.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_custom_single.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_diff.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_diff.png
index 1ba08de..c7a5f46 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_diff.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_diff.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_single.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_single.png
index 3aeb87f..83970de 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_single.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_except_single.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_only_diff.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_only_diff.png
index 135ad82..1cbe96f 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_only_diff.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_only_diff.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_only_single.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_only_single.png
index 65db92e..859609b 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_only_single.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_only_single.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_diff.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_diff.png
index 1ba08de..c7a5f46 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_diff.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_diff.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_single.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_single.png
index 3aeb87f..83970de 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_single.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_scene_single.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_diff.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_diff.png
index 1ba08de..c7a5f46 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_diff.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_diff.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_single.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_single.png
index 3aeb87f..83970de 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_single.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_snapshot_showAll_single.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots1.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots1.png
index ee7bdac..ae197a2 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots1.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots1.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots2.png b/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots2.png
index ee7bdac..ae197a2 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots2.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory_diff_three_snapshots2.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/settings_dialog_default.png b/packages/devtools_app/test/test_infra/goldens/settings_dialog_default.png
index 0bbe296..d913669 100644
--- a/packages/devtools_app/test/test_infra/goldens/settings_dialog_default.png
+++ b/packages/devtools_app/test/test_infra/goldens/settings_dialog_default.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/settings_dialog_modified.png b/packages/devtools_app/test/test_infra/goldens/settings_dialog_modified.png
index 581d386..9337f5c 100644
--- a/packages/devtools_app/test/test_infra/goldens/settings_dialog_modified.png
+++ b/packages/devtools_app/test/test_infra/goldens/settings_dialog_modified.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/timeline_flame_chart_with_selected_frame.png b/packages/devtools_app/test/test_infra/goldens/timeline_flame_chart_with_selected_frame.png
index 4aa7d7f..cf85116 100644
--- a/packages/devtools_app/test/test_infra/goldens/timeline_flame_chart_with_selected_frame.png
+++ b/packages/devtools_app/test/test_infra/goldens/timeline_flame_chart_with_selected_frame.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/treemap_apk.png b/packages/devtools_app/test/test_infra/goldens/treemap_apk.png
index e62c097..38f951a 100644
--- a/packages/devtools_app/test/test_infra/goldens/treemap_apk.png
+++ b/packages/devtools_app/test/test_infra/goldens/treemap_apk.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/treemap_sizes.png b/packages/devtools_app/test/test_infra/goldens/treemap_sizes.png
index 9dbdd8e..8e8592f 100644
--- a/packages/devtools_app/test/test_infra/goldens/treemap_sizes.png
+++ b/packages/devtools_app/test/test_infra/goldens/treemap_sizes.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/treemap_v8.png b/packages/devtools_app/test/test_infra/goldens/treemap_v8.png
index 60849f1..3d6846e 100644
--- a/packages/devtools_app/test/test_infra/goldens/treemap_v8.png
+++ b/packages/devtools_app/test/test_infra/goldens/treemap_v8.png
Binary files differ
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 df0e605..c678ca2 100644
--- a/packages/devtools_app/test/test_infra/scenes/memory/default.dart
+++ b/packages/devtools_app/test/test_infra/scenes/memory/default.dart
@@ -142,7 +142,7 @@
 
   // Create objects.
   for (var entry in classToInstanceCount.entries) {
-    for (var _ in Iterable.generate(entry.value)) {
+    for (var _ in Iterable<void>.generate(entry.value)) {
       objects.add(_createObject(entry.key));
       leafCount++;
       final objectIndex = leafCount;
diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code.dart
index 06fae3d..fb855dc 100644
--- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code.dart
+++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code.dart
@@ -12,7 +12,7 @@
 import '../../../test_infra/test_data/dart_tooling_api/mock_api.dart';
 import 'vs_code_mock_editor.dart';
 
-final _api = MockDartToolingApi();
+final _api = FakeDartToolingApi();
 
 /// To run, use the "standalone_ui/vs_code" launch configuration with the
 /// `devtools/packages/` folder open in VS Code, or run:
@@ -57,6 +57,8 @@
 
   @override
   Future<void> setUp() async {
+    setStagerMode();
     setGlobal(IdeTheme, IdeTheme());
+    setGlobal(PreferencesController, PreferencesController());
   }
 }
diff --git a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart
index 8282b90..801652d 100644
--- a/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart
+++ b/packages/devtools_app/test/test_infra/scenes/standalone_ui/vs_code_mock_editor.dart
@@ -13,7 +13,7 @@
 /// A simple UI that acts as a stand-in host IDE to simplify the development
 /// workflow when working on embedded tooling.
 ///
-/// This UI interacts with [MockDartToolingApi] to allow triggering events that
+/// This UI interacts with [FakeDartToolingApi] to allow triggering events that
 /// would normally be fired by the IDE and also shows a log of recent requests.
 class VsCodeFlutterPanelMockEditor extends StatefulWidget {
   const VsCodeFlutterPanelMockEditor({
@@ -23,7 +23,7 @@
   });
 
   /// The mock API to interact with.
-  final MockDartToolingApi api;
+  final FakeDartToolingApi api;
 
   final Widget? child;
 
@@ -34,14 +34,14 @@
 
 class _VsCodeFlutterPanelMockEditorState
     extends State<VsCodeFlutterPanelMockEditor> {
-  MockDartToolingApi get api => widget.api;
+  FakeDartToolingApi get api => widget.api;
 
   /// The number of communication messages to keep in the log.
   static const maxLogEvents = 20;
 
   /// The last [maxLogEvents] communication messages sent between the panel
   /// and the "host IDE".
-  final logRing = DoubleLinkedQueue();
+  final logRing = DoubleLinkedQueue<String>();
 
   /// A stream that emits each time the log is updated to allow the log widget
   /// to be rebuilt.
@@ -133,18 +133,39 @@
                   Row(
                     children: [
                       ElevatedButton(
-                        onPressed: () => api.startSession('debug', 'myMac'),
+                        onPressed: () => api.startSession(
+                          debuggerType: 'Flutter',
+                          deviceId: 'macos',
+                          flutterMode: 'debug',
+                        ),
                         child: const Text('Desktop debug'),
                       ),
                       const SizedBox(width: denseSpacing),
                       ElevatedButton(
-                        onPressed: () => api.startSession('debug', 'chrome'),
-                        child: const Text('Web debug'),
+                        onPressed: () => api.startSession(
+                          debuggerType: 'Flutter',
+                          deviceId: 'macos',
+                          flutterMode: 'profile',
+                        ),
+                        child: const Text('Desktop profile'),
                       ),
                       const SizedBox(width: denseSpacing),
                       ElevatedButton(
-                        onPressed: () => api.startSession('profile', 'myMac'),
-                        child: const Text('Desktop profile'),
+                        onPressed: () => api.startSession(
+                          debuggerType: 'Flutter',
+                          deviceId: 'macos',
+                          flutterMode: 'release',
+                        ),
+                        child: const Text('Desktop release'),
+                      ),
+                      const SizedBox(width: denseSpacing),
+                      ElevatedButton(
+                        onPressed: () => api.startSession(
+                          debuggerType: 'Flutter',
+                          deviceId: 'macos',
+                          flutterMode: 'jit_release',
+                        ),
+                        child: const Text('Desktop jit_release'),
                       ),
                     ],
                   ),
@@ -152,27 +173,55 @@
                   Row(
                     children: [
                       ElevatedButton(
-                        onPressed: () => api.startSession('release', 'myMac'),
-                        child: const Text('Desktop release'),
-                      ),
-                      const SizedBox(width: denseSpacing),
-                      ElevatedButton(
-                        onPressed: () =>
-                            api.startSession('jit_release', 'myMac'),
-                        child: const Text('Desktop jit_release'),
-                      ),
-                      const SizedBox(width: denseSpacing),
-                      ElevatedButton(
-                        onPressed: () => api.endSessions(),
-                        style: theme.elevatedButtonTheme.style!.copyWith(
-                          backgroundColor: const MaterialStatePropertyAll(
-                            Colors.red,
-                          ),
+                        onPressed: () => api.startSession(
+                          debuggerType: 'Flutter',
+                          deviceId: 'chrome',
+                          flutterMode: 'debug',
                         ),
-                        child: const Text('Stop All'),
+                        child: const Text('Web debug'),
+                      ),
+                      const SizedBox(width: denseSpacing),
+                      ElevatedButton(
+                        onPressed: () => api.startSession(
+                          debuggerType: 'Flutter',
+                          deviceId: 'chrome',
+                          flutterMode: 'profile',
+                        ),
+                        child: const Text('Web profile'),
+                      ),
+                      const SizedBox(width: denseSpacing),
+                      ElevatedButton(
+                        onPressed: () => api.startSession(
+                          debuggerType: 'Flutter',
+                          deviceId: 'chrome',
+                          flutterMode: 'release',
+                        ),
+                        child: const Text('Web release'),
                       ),
                     ],
                   ),
+                  const SizedBox(height: denseSpacing),
+                  Row(
+                    children: [
+                      ElevatedButton(
+                        onPressed: () => api.startSession(
+                          debuggerType: 'Dart',
+                          deviceId: 'macos',
+                        ),
+                        child: const Text('Dart CLI'),
+                      ),
+                    ],
+                  ),
+                  const SizedBox(height: denseSpacing),
+                  ElevatedButton(
+                    onPressed: () => api.endSessions(),
+                    style: theme.elevatedButtonTheme.style!.copyWith(
+                      backgroundColor: const MaterialStatePropertyAll(
+                        Colors.red,
+                      ),
+                    ),
+                    child: const Text('Stop All'),
+                  ),
                 ],
               ),
             ),
diff --git a/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart b/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart
index 0a3ce66..5ac963e 100644
--- a/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart
+++ b/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart
@@ -17,8 +17,8 @@
 /// events in a similar way to the IDE would. It is used by
 /// [VsCodeFlutterPanelMock] which provides a UI onto this functionality and a
 /// log of recent requests.
-class MockDartToolingApi extends DartToolingApiImpl {
-  factory MockDartToolingApi() {
+class FakeDartToolingApi extends DartToolingApiImpl {
+  factory FakeDartToolingApi() {
     // Set up channels where we can act as the server in-process without really
     // going over postMessage or a WebSocket (since in the mock environment we
     // can't do either).
@@ -48,14 +48,14 @@
     final serverPeer = json_rpc_2.Peer(serverChannel);
     unawaited(serverPeer.listen());
 
-    return MockDartToolingApi._(
+    return FakeDartToolingApi._(
       client: clientPeer,
       server: serverPeer,
       log: log.stream,
     );
   }
 
-  MockDartToolingApi._({
+  FakeDartToolingApi._({
     required this.client,
     required this.server,
     required this.log,
@@ -83,53 +83,9 @@
   final json_rpc_2.Peer client;
   final json_rpc_2.Peer server;
 
-  /// A set of mock devices that can be presented for testing.
-  final _mockDevices = <VsCodeDevice>[
-    VsCodeDeviceImpl(
-      id: 'myMac',
-      name: 'Mac',
-      category: 'desktop',
-      emulator: false,
-      emulatorId: null,
-      ephemeral: false,
-      platform: 'darwin-x64',
-      platformType: 'macos',
-    ),
-    VsCodeDeviceImpl(
-      id: 'myPhone',
-      name: 'My Android Phone',
-      category: 'mobile',
-      emulator: false,
-      emulatorId: null,
-      ephemeral: true,
-      platform: 'android-x64',
-      platformType: 'android',
-    ),
-    VsCodeDeviceImpl(
-      id: 'chrome',
-      name: 'Chrome',
-      category: 'web',
-      emulator: false,
-      emulatorId: null,
-      ephemeral: true,
-      platform: 'web-javascript',
-      platformType: 'web',
-    ),
-    VsCodeDeviceImpl(
-      id: 'web-server',
-      name: 'Web Server',
-      category: 'web',
-      emulator: false,
-      emulatorId: null,
-      ephemeral: true,
-      platform: 'web-javascript',
-      platformType: 'web',
-    ),
-  ];
-
   /// The current set of enabled platform types.
   ///
-  /// Defauls are set in [connectDevices].
+  /// Defaults are set in [connectDevices].
   final _enabledPlatformTypes = <String>{};
 
   /// The current set of devices being presented to the embedded panel.
@@ -202,7 +158,7 @@
   void connectDevices() {
     _devices
       ..clear()
-      ..addAll(_mockDevices);
+      ..addAll(stubbedDevices);
     _enabledPlatformTypes
       ..clear()
       ..addAll(['macos', 'android']);
@@ -211,17 +167,29 @@
   }
 
   /// Simulates starting a debug session.
-  void startSession(String mode, String deviceId) {
+  ///
+  /// [debuggerType] should match one of the available values for [VsCodeDebugSession.debuggerType].
+  ///
+  /// [deviceId] can be any id for a flutter device. This should be null if [debuggerType]
+  /// is not 'Flutter'.
+  ///
+  /// [flutterMode] should match one of the available values for [VsCodeDebugSession.flutterMode].
+  /// This should be null if [debuggerType] is not 'Flutter'.
+  void startSession({
+    required String debuggerType,
+    required String deviceId,
+    String? flutterMode,
+  }) {
     final sessionNum = _nextDebugSessionNumber++;
     _debugSessions.add(
       VsCodeDebugSessionImpl(
         id: 'debug-$sessionNum',
-        name: 'Session $sessionNum',
+        name: 'Session $sessionNum ($deviceId)',
         vmServiceUri: 'ws://127.0.0.1:1234/ws',
-        flutterMode: mode,
+        flutterMode: flutterMode,
         flutterDeviceId: deviceId,
-        debuggerType: 'Flutter',
-        projectRootPath: null,
+        debuggerType: debuggerType,
+        projectRootPath: '/mock/root/path',
       ),
     );
     _sendDebugSessionsChanged();
@@ -261,3 +229,47 @@
     );
   }
 }
+
+/// A set of mock devices that can be presented for testing.
+final stubbedDevices = <VsCodeDevice>[
+  VsCodeDeviceImpl(
+    id: 'macos',
+    name: 'Mac',
+    category: 'desktop',
+    emulator: false,
+    emulatorId: null,
+    ephemeral: false,
+    platform: 'darwin-x64',
+    platformType: 'macos',
+  ),
+  VsCodeDeviceImpl(
+    id: 'myPhone',
+    name: 'My Android Phone',
+    category: 'mobile',
+    emulator: false,
+    emulatorId: null,
+    ephemeral: true,
+    platform: 'android-x64',
+    platformType: 'android',
+  ),
+  VsCodeDeviceImpl(
+    id: 'chrome',
+    name: 'Chrome',
+    category: 'web',
+    emulator: false,
+    emulatorId: null,
+    ephemeral: true,
+    platform: 'web-javascript',
+    platformType: 'web',
+  ),
+  VsCodeDeviceImpl(
+    id: 'web-server',
+    name: 'Web Server',
+    category: 'web',
+    emulator: false,
+    emulatorId: null,
+    ephemeral: true,
+    platform: 'web-javascript',
+    platformType: 'web',
+  ),
+];
diff --git a/packages/devtools_app/test/test_infra/test_data/memory/heap/heap_data.dart b/packages/devtools_app/test/test_infra/test_data/memory/heap/heap_data.dart
index f6a445e..200795c 100644
--- a/packages/devtools_app/test/test_infra/test_data/memory/heap/heap_data.dart
+++ b/packages/devtools_app/test/test_infra/test_data/memory/heap/heap_data.dart
@@ -5,7 +5,6 @@
 import 'dart:io';
 
 import 'package:devtools_app/src/shared/memory/adapted_heap_data.dart';
-import 'package:vm_service/vm_service.dart';
 
 const _dataDir = 'test/test_infra/test_data/memory/heap/';
 
@@ -50,7 +49,5 @@
 ) async {
   final file = File(fileName);
   final bytes = await file.readAsBytes();
-  final data = bytes.buffer.asByteData();
-  final graph = HeapSnapshotGraph.fromChunks([data]);
-  return AdaptedHeapData.fromHeapSnapshot(graph);
+  return AdaptedHeapData.fromBytes(bytes);
 }
diff --git a/packages/devtools_app/test/test_infra/test_data/network.dart b/packages/devtools_app/test/test_infra/test_data/network.dart
index f4e45eb..83a9c9e 100644
--- a/packages/devtools_app/test/test_infra/test_data/network.dart
+++ b/packages/devtools_app/test/test_infra/test_data/network.dart
@@ -116,7 +116,6 @@
 
 final httpGetRequest = HttpProfileRequest.parse(httpGetJson)!;
 final httpGet = DartIOHttpRequestData(
-  0,
   httpGetRequest,
   requestFullDataFromVmService: false,
 );
@@ -126,15 +125,15 @@
   'isolateId': 'isolates/2013291945734727',
   'method': 'GET',
   'uri': 'https://jsonplaceholder.typicode.com/albums/1',
+  'events': [
+    {'timestamp': 6326808941, 'event': 'Connection established'},
+    {'timestamp': 6326808965, 'event': 'Request sent'},
+    {'timestamp': 6327090622, 'event': 'Waiting (TTFB)'},
+    {'timestamp': 6327091650, 'event': 'Content Download'},
+  ],
   'startTime': 6326279935,
   'endTime': 6326808974,
   'request': {
-    'events': [
-      {'timestamp': 6326808941, 'event': 'Connection established'},
-      {'timestamp': 6326808965, 'event': 'Request sent'},
-      {'timestamp': 6327090622, 'event': 'Waiting (TTFB)'},
-      {'timestamp': 6327091650, 'event': 'Content Download'},
-    ],
     'headers': {
       'content-length': ['0'],
     },
@@ -182,7 +181,6 @@
 
 final httpPostRequest = HttpProfileRequest.parse(httpPostJson)!;
 final httpPost = DartIOHttpRequestData(
-  0,
   httpPostRequest,
   requestFullDataFromVmService: false,
 );
@@ -192,15 +190,15 @@
   'isolateId': 'isolates/979700762893215',
   'method': 'POST',
   'uri': 'https://jsonplaceholder.typicode.com/posts',
+  'events': [
+    {'timestamp': 2400314657, 'event': 'Connection established'},
+    {'timestamp': 2400320066, 'event': 'Request sent'},
+    {'timestamp': 2400994822, 'event': 'Waiting (TTFB)'},
+    {'timestamp': 2401000729, 'event': 'Content Download'},
+  ],
   'startTime': 2399492629,
   'endTime': 2400321715,
   'request': {
-    'events': [
-      {'timestamp': 2400314657, 'event': 'Connection established'},
-      {'timestamp': 2400320066, 'event': 'Request sent'},
-      {'timestamp': 2400994822, 'event': 'Waiting (TTFB)'},
-      {'timestamp': 2401000729, 'event': 'Content Download'},
-    ],
     'headers': {
       'transfer-encoding': [],
     },
@@ -259,7 +257,6 @@
 
 final httpPutRequest = HttpProfileRequest.parse(httpPutJson)!;
 final httpPut = DartIOHttpRequestData(
-  0,
   httpPutRequest,
   requestFullDataFromVmService: false,
 );
@@ -269,15 +266,15 @@
   'isolateId': 'isolates/4447876918484683',
   'method': 'PUT',
   'uri': 'https://jsonplaceholder.typicode.com/posts/1',
+  'events': [
+    {'timestamp': 1205855316, 'event': 'Connection established'},
+    {'timestamp': 1205858323, 'event': 'Request sent'},
+    {'timestamp': 1206602445, 'event': 'Waiting (TTFB)'},
+    {'timestamp': 1206609213, 'event': 'Content Download'},
+  ],
   'startTime': 1205283313,
   'endTime': 1205859179,
   'request': {
-    'events': [
-      {'timestamp': 1205855316, 'event': 'Connection established'},
-      {'timestamp': 1205858323, 'event': 'Request sent'},
-      {'timestamp': 1206602445, 'event': 'Waiting (TTFB)'},
-      {'timestamp': 1206609213, 'event': 'Content Download'},
-    ],
     'headers': {
       'transfer-encoding': [],
     },
@@ -338,7 +335,6 @@
 
 final httpPatchRequest = HttpProfileRequest.parse(httpPatchJson)!;
 final httpPatch = DartIOHttpRequestData(
-  0,
   httpPatchRequest,
   requestFullDataFromVmService: false,
 );
@@ -348,15 +344,15 @@
   'isolateId': 'isolates/4447876918484683',
   'method': 'PATCH',
   'uri': 'https://jsonplaceholder.typicode.com/posts/1',
+  'events': [
+    {'timestamp': 1910722654, 'event': 'Connection established'},
+    {'timestamp': 1910722783, 'event': 'Request sent'},
+    {'timestamp': 1911415225, 'event': 'Waiting (TTFB)'},
+    {'timestamp': 1911421003, 'event': 'Content Download'},
+  ],
   'startTime': 1910177192,
   'endTime': 1910722856,
   'request': {
-    'events': [
-      {'timestamp': 1910722654, 'event': 'Connection established'},
-      {'timestamp': 1910722783, 'event': 'Request sent'},
-      {'timestamp': 1911415225, 'event': 'Waiting (TTFB)'},
-      {'timestamp': 1911421003, 'event': 'Content Download'},
-    ],
     'headers': {
       'transfer-encoding': [],
     },
@@ -420,7 +416,6 @@
 
 final httpGetWithErrorRequest = HttpProfileRequest.parse(httpGetWithErrorJson)!;
 final httpGetWithError = DartIOHttpRequestData(
-  0,
   httpGetWithErrorRequest,
   requestFullDataFromVmService: false,
 );
@@ -430,17 +425,16 @@
   'isolateId': 'isolates/1939772779732043',
   'method': 'GET',
   'uri': 'https://www.examplez.com/1',
+  'events': [],
   'startTime': 5385227316,
   'endTime': 5387256813,
   'request': {
-    'events': [],
     'error': 'HandshakeException: Connection terminated during handshake',
   },
 };
 
 final httpWsHandshakeRequest = HttpProfileRequest.parse(httpWsHandshakeJson)!;
 final httpWsHandshake = DartIOHttpRequestData(
-  0,
   httpWsHandshakeRequest,
   requestFullDataFromVmService: false,
 );
@@ -450,14 +444,14 @@
   'isolateId': 'isolates/1350291957483171',
   'method': 'GET',
   'uri': 'http://localhost:8080',
+  'events': [
+    {'timestamp': 8140247076, 'event': 'Connection established'},
+    {'timestamp': 8140247156, 'event': 'Request sent'},
+    {'timestamp': 8140261573, 'event': 'Waiting (TTFB)'},
+  ],
   'startTime': 8140222102,
   'endTime': 8140247377,
   'request': {
-    'events': [
-      {'timestamp': 8140247076, 'event': 'Connection established'},
-      {'timestamp': 8140247156, 'event': 'Request sent'},
-      {'timestamp': 8140261573, 'event': 'Waiting (TTFB)'},
-    ],
     'headers': {
       'content-length': ['0'],
     },
@@ -510,15 +504,15 @@
   'isolateId': 'isolates/2013291945734727',
   'method': 'GET',
   'uri': 'https://jsonplaceholder.typicode.com/albums/10',
+  'events': [
+    {'timestamp': 6326808941, 'event': 'Connection established'},
+    {'timestamp': 6326808965, 'event': 'Request sent'},
+    {'timestamp': 6327090622, 'event': 'Waiting (TTFB)'},
+    {'timestamp': 6327091650, 'event': 'Content Download'},
+  ],
   'startTime': 6326279935,
   'endTime': 6326808974,
   'request': {
-    'events': [
-      {'timestamp': 6326808941, 'event': 'Connection established'},
-      {'timestamp': 6326808965, 'event': 'Request sent'},
-      {'timestamp': 6327090622, 'event': 'Waiting (TTFB)'},
-      {'timestamp': 6327091650, 'event': 'Content Download'},
-    ],
     'headers': {
       'content-length': ['0'],
     },
diff --git a/packages/devtools_app/test/test_infra/utils/test_utils.dart b/packages/devtools_app/test/test_infra/utils/test_utils.dart
index 6ffdeb8..e9fde11 100644
--- a/packages/devtools_app/test/test_infra/utils/test_utils.dart
+++ b/packages/devtools_app/test/test_infra/utils/test_utils.dart
@@ -27,7 +27,7 @@
 /// [clipboardContentsCallback]  when Clipboard.setData is triggered, the text
 /// contents will be passed to [clipboardContentsCallback]
 void setupClipboardCopyListener({
-  required Function(String?) clipboardContentsCallback,
+  required void Function(String?) clipboardContentsCallback,
 }) {
   // This intercepts the Clipboard.setData SystemChannel message,
   // and stores the contents that were (attempted) to be copied.
@@ -37,7 +37,7 @@
     (MethodCall call) {
       switch (call.method) {
         case 'Clipboard.setData':
-          clipboardContentsCallback(call.arguments['text']);
+          clipboardContentsCallback((call.arguments as Map)['text']);
           break;
         case 'Clipboard.getData':
           return Future.value(<String, Object?>{});
diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md
index df089f3..95bd6a2 100644
--- a/packages/devtools_app_shared/CHANGELOG.md
+++ b/packages/devtools_app_shared/CHANGELOG.md
@@ -1,3 +1,23 @@
+## 0.0.10-wip
+* Add `DTDManager` class and export from `service.dart`.
+* Add `showDevToolsDialog` helper method.
+* Add `FlexSplitColumn` and `BlankHeader` common widgets.
+* Bump `package:vm_service` dependency to ^14.0.0.
+
+## 0.0.9
+* Bump `package:web` to `^0.4.1`.
+* Densify overall UI.
+* Add public members: `PaddedDivider.noPadding`, `singleLineDialogTextFieldDecoration`, `extraLargeSpacing`, `regularTextStyleWithColor`.
+* Remove public members: `areaPaneHeaderHeight`, `defaultSwitchHeight`,`
+`dialogTextFieldDecoration`
+* Automatically show tooltips for `DevToolsButton` whose labels have been hidden due to
+hitting a narrow screen threshold, specified by `minScreenWidthForTextBeforeScaling`.
+* Add an optional parameter `borderColor` to `DevToolsToggleButtonGroup`.
+* Add a strict type on `DialogApplyButton.onPressed` and `ToggleableServiceExtension`.
+* Change default styling of `regularTextStyle` to inherit from `TextTheme.bodySmall`.
+* Change default styling of `TextTheme.bodySmall`, `TextTheme.bodyMedium`,
+`TextTheme.titleSmall` in the base theme.
+
 ## 0.0.8
 * Add `ServiceManager.resolvedUriManager` for looking up package and file uris from
 a VM service connection.
diff --git a/packages/devtools_app_shared/lib/service.dart b/packages/devtools_app_shared/lib/service.dart
index ddf3650..c715611 100644
--- a/packages/devtools_app_shared/lib/service.dart
+++ b/packages/devtools_app_shared/lib/service.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 export 'src/service/connected_app.dart';
+export 'src/service/dtd_manager.dart';
 export 'src/service/eval_on_dart_library.dart';
 export 'src/service/flutter_version.dart';
 export 'src/service/isolate_manager.dart' hide TestIsolateManager;
diff --git a/packages/devtools_app/lib/src/service/dtd_manager.dart b/packages/devtools_app_shared/lib/src/service/dtd_manager.dart
similarity index 66%
rename from packages/devtools_app/lib/src/service/dtd_manager.dart
rename to packages/devtools_app_shared/lib/src/service/dtd_manager.dart
index 08d139e..c94d208 100644
--- a/packages/devtools_app/lib/src/service/dtd_manager.dart
+++ b/packages/devtools_app_shared/lib/src/service/dtd_manager.dart
@@ -4,9 +4,9 @@
 
 import 'package:dtd/dtd.dart';
 import 'package:flutter/foundation.dart';
+import 'package:logging/logging.dart';
 
-import '../framework/app_error_handling.dart';
-import '../shared/globals.dart';
+final _log = Logger('dtd_manager');
 
 /// Manages a connection to the Dart Tooling Daemon.
 class DTDManager {
@@ -14,25 +14,29 @@
   final ValueNotifier<DTDConnection?> _connection =
       ValueNotifier<DTDConnection?>(null);
 
+  /// Whether the [DTDManager] is connected to a running instance of the DTD.
+  bool get hasConnection => connection.value != null;
+
+  /// The URI of the current DTD connection.
+  Uri? get uri => _uri;
+  Uri? _uri;
+
   /// Sets the Dart Tooling Daemon connection to point to [uri].
   ///
   /// Before connecting to [uri], if a current connection exists, then
   /// [disconnect] is called to close it.
-  Future<void> connect(Uri uri) async {
+  Future<void> connect(
+    Uri uri, {
+    void Function(Object, StackTrace?)? onError,
+  }) async {
     await disconnect();
 
     try {
       _connection.value = await DartToolingDaemon.connect(uri);
+      _uri = uri;
+      _log.info('Successfully connected to DTD at: $uri');
     } catch (e, st) {
-      notificationService.pushError(
-        'Failed to connect to the Dart Tooling Daemon',
-        isReportable: false,
-      );
-      reportError(
-        e,
-        errorType: 'Dart Tooling Daemon connection failed.',
-        stack: st,
-      );
+      onError?.call(e, st);
     }
   }
 
@@ -43,5 +47,6 @@
     }
 
     _connection.value = null;
+    _uri = null;
   }
 }
diff --git a/packages/devtools_app_shared/lib/src/service/service_manager.dart b/packages/devtools_app_shared/lib/src/service/service_manager.dart
index fa619db..e4d7d85 100644
--- a/packages/devtools_app_shared/lib/src/service/service_manager.dart
+++ b/packages/devtools_app_shared/lib/src/service/service_manager.dart
@@ -83,7 +83,7 @@
 
   final resolvedUriManager = ResolvedUriManager();
 
-  /// Proxy to state inside the isolateManager, for code consizeness.
+  /// Proxy to state inside the isolateManager, for code conciseness.
   ///
   /// Defaults to false if there is no main isolate.
   bool get isMainIsolatePaused =>
@@ -145,6 +145,7 @@
     String name, {
     String? isolateId,
     Map<String, dynamic>? args,
+    // ignore: avoid-redundant-async, for some reasons tests fail without `async
   }) async {
     final registeredMethod = _registeredMethodsForService[name];
     if (registeredMethod == null) {
diff --git a/packages/devtools_app_shared/lib/src/ui/common.dart b/packages/devtools_app_shared/lib/src/ui/common.dart
index 95dc682..8443adf 100644
--- a/packages/devtools_app_shared/lib/src/ui/common.dart
+++ b/packages/devtools_app_shared/lib/src/ui/common.dart
@@ -8,9 +8,6 @@
 
 import '../utils/utils.dart';
 import 'theme/theme.dart';
-import 'ui_utils.dart';
-
-double get areaPaneHeaderHeight => scaleByFontFactor(36.0);
 
 /// Create a bordered, fixed-height header area with a title and optional child
 /// on the right-hand side.
@@ -104,13 +101,27 @@
   @override
   Size get preferredSize {
     return Size.fromHeight(
-      tall
-          ? areaPaneHeaderHeight + 2 * densePadding
-          : defaultHeaderHeight(isDense: dense),
+      tall ? defaultHeaderHeight + 2 * densePadding : defaultHeaderHeight,
     );
   }
 }
 
+/// A blank, drop-in replacement for [AreaPaneHeader].
+///
+/// Acts as an empty header widget with zero size that is compatible with
+/// interfaces that expect a [PreferredSizeWidget].
+final class BlankHeader extends StatelessWidget implements PreferredSizeWidget {
+  const BlankHeader({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container();
+  }
+
+  @override
+  Size get preferredSize => Size.zero;
+}
+
 /// Wraps [child] in a rounded border with default styling.
 ///
 /// This border can optionally be made non-uniform by setting any of
@@ -278,6 +289,8 @@
   const PaddedDivider.thin({super.key})
       : padding = const EdgeInsets.only(bottom: 4.0);
 
+  const PaddedDivider.noPadding({super.key}) : padding = EdgeInsets.zero;
+
   PaddedDivider.vertical({super.key, double padding = densePadding})
       : padding = EdgeInsets.symmetric(vertical: padding);
 
@@ -352,6 +365,8 @@
 
   @override
   Widget build(BuildContext context) {
+    var tooltip = this.tooltip;
+
     if (label == null) {
       return SizedBox(
         // This is required to force the button size.
@@ -359,15 +374,16 @@
         width: defaultButtonHeight,
         child: maybeWrapWithTooltip(
           tooltip: tooltip,
+          tooltipPadding: tooltipPadding,
           child: outlined
               ? IconButton.outlined(
                   onPressed: onPressed,
-                  iconSize: actionsIconSize,
+                  iconSize: defaultIconSize,
                   icon: Icon(icon),
                 )
               : IconButton(
                   onPressed: onPressed,
-                  iconSize: actionsIconSize,
+                  iconSize: defaultIconSize,
                   icon: Icon(
                     icon,
                   ),
@@ -387,13 +403,26 @@
       minScreenWidthForTextBeforeScaling: minScreenWidthForTextBeforeScaling,
       color: textColor,
     );
+
+    // If we hid the label due to a small screen width and the button does not
+    // have a tooltip, use the label as a tooltip.
+    final labelHidden =
+        !isScreenWiderThan(context, minScreenWidthForTextBeforeScaling);
+    if (labelHidden && tooltip == null) {
+      tooltip = label;
+    }
+
     if (elevated) {
-      return maybeWrapWithTooltip(
-        tooltip: tooltip,
-        tooltipPadding: tooltipPadding,
-        child: ElevatedButton(
-          onPressed: onPressed,
-          child: iconLabel,
+      return SizedBox(
+        // This is required to force the button size.
+        height: defaultButtonHeight,
+        child: maybeWrapWithTooltip(
+          tooltip: tooltip,
+          tooltipPadding: tooltipPadding,
+          child: ElevatedButton(
+            onPressed: onPressed,
+            child: iconLabel,
+          ),
         ),
       );
     }
@@ -485,6 +514,7 @@
     required this.onPressed,
     this.fillColor,
     this.selectedColor,
+    this.borderColor,
   }) : super(key: key);
 
   final List<Widget> children;
@@ -497,6 +527,8 @@
 
   final Color? selectedColor;
 
+  final Color? borderColor;
+
   @override
   Widget build(BuildContext context) {
     final theme = Theme.of(context);
@@ -506,7 +538,8 @@
         borderRadius: defaultBorderRadius,
         fillColor: fillColor,
         selectedColor: selectedColor,
-        textStyle: theme.textTheme.bodyMedium,
+        borderColor: borderColor,
+        textStyle: theme.textTheme.bodySmall,
         constraints: BoxConstraints(
           minWidth: defaultButtonHeight,
           minHeight: defaultButtonHeight,
@@ -548,6 +581,9 @@
   @override
   Widget build(BuildContext context) {
     return DevToolsToggleButtonGroup(
+      borderColor: outlined || isSelected
+          ? Theme.of(context).focusColor
+          : Colors.transparent,
       selectedStates: [isSelected],
       onPressed: (_) => onPressed(),
       children: [
@@ -769,11 +805,11 @@
             isScreenWiderThan(context, minScreenWidthForTextBeforeScaling))
           Padding(
             padding: EdgeInsets.only(
-              left: iconData != null ? denseSpacing : 0.0,
+              left: iconData != null ? densePadding : 0.0,
             ),
             child: Text(
               label!,
-              style: TextStyle(color: color),
+              style: Theme.of(context).regularTextStyleWithColor(color),
             ),
           ),
       ],
diff --git a/packages/devtools_app_shared/lib/src/ui/dialogs.dart b/packages/devtools_app_shared/lib/src/ui/dialogs.dart
index e7e8ba3..e9eed67 100644
--- a/packages/devtools_app_shared/lib/src/ui/dialogs.dart
+++ b/packages/devtools_app_shared/lib/src/ui/dialogs.dart
@@ -2,6 +2,8 @@
 // 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/material.dart';
 import 'package:pointer_interceptor/pointer_interceptor.dart';
 
@@ -54,7 +56,10 @@
           contentPadding,
           contentPadding,
         ),
-        content: content,
+        content: DefaultTextStyle(
+          style: Theme.of(context).regularTextStyle,
+          child: content,
+        ),
         actions: actions,
         actionsAlignment: actionsAlignment,
         buttonPadding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
@@ -71,20 +76,22 @@
 
   @override
   Widget build(BuildContext context) =>
-      Text(text, style: Theme.of(context).textTheme.titleLarge);
+      Text(text, style: Theme.of(context).textTheme.titleMedium);
 }
 
 List<Widget> dialogSubHeader(ThemeData theme, String titleText) {
   return [
-    Text(titleText, style: theme.textTheme.titleMedium),
+    Text(titleText, style: theme.textTheme.titleSmall),
     const PaddedDivider(padding: EdgeInsets.only(bottom: denseRowSpacing)),
   ];
 }
 
-final dialogTextFieldDecoration = InputDecoration(
-  border: OutlineInputBorder(
-    borderRadius: defaultBorderRadius,
+final singleLineDialogTextFieldDecoration = InputDecoration(
+  constraints: BoxConstraints(
+    minHeight: defaultTextFieldHeight,
+    maxHeight: defaultTextFieldHeight,
   ),
+  border: const OutlineInputBorder(),
 );
 
 /// A standardized dialog with help text and buttons `Reset to default`,
@@ -260,3 +267,27 @@
     );
   }
 }
+
+void showDevToolsDialog({
+  required BuildContext context,
+  required String title,
+  required Widget content,
+  List<Widget> actions = const <Widget>[],
+}) {
+  unawaited(
+    showDialog(
+      context: context,
+      builder: (context) => DevToolsDialog(
+        title: DialogTitleText(title),
+        includeDivider: false,
+        content: content,
+        actionsAlignment:
+            actions.isNotEmpty ? MainAxisAlignment.spaceBetween : null,
+        actions: [
+          ...actions,
+          const DialogCloseButton(),
+        ],
+      ),
+    ),
+  );
+}
diff --git a/packages/devtools_app/lib/src/shared/flex_split_column.dart b/packages/devtools_app_shared/lib/src/ui/flex_split_column.dart
similarity index 84%
rename from packages/devtools_app/lib/src/shared/flex_split_column.dart
rename to packages/devtools_app_shared/lib/src/ui/flex_split_column.dart
index a47a421..c35bccd 100644
--- a/packages/devtools_app/lib/src/shared/flex_split_column.dart
+++ b/packages/devtools_app_shared/lib/src/ui/flex_split_column.dart
@@ -2,10 +2,26 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
 
-class FlexSplitColumn extends StatelessWidget {
+import 'split.dart';
+
+/// A widget that takes a list of [children] and lays them out in a column where
+/// each child has a flexible height.
+/// 
+/// Each child in [children] must have an accompanying header in [header]. Since
+/// each header must be a [PreferredSizeWidget], it is common to use the shared
+/// widgets [AreaPaneHeader] or [BlankHeader] for the column [headers].
+/// 
+/// The user can customize the amount of space allocated to each child by
+/// clicking and dragging the [header]s that separate the children.
+///
+/// [initialFractions] defines how much space to give each child when building
+/// this widget.
+/// 
+/// [minSizes] defines the minimum size that each child can be set to when
+/// adjusting the sizes of the children.
+final class FlexSplitColumn extends StatelessWidget {
   FlexSplitColumn({
     Key? key,
     required this.totalHeight,
diff --git a/packages/devtools_app_shared/lib/src/ui/split.dart b/packages/devtools_app_shared/lib/src/ui/split.dart
index 4385ba8..c4a087a 100644
--- a/packages/devtools_app_shared/lib/src/ui/split.dart
+++ b/packages/devtools_app_shared/lib/src/ui/split.dart
@@ -17,6 +17,9 @@
 ///
 /// [initialFractions] defines how much space to give each child when building
 /// this widget.
+/// 
+/// [minSizes] defines the minimum size that each child can be set to when
+/// adjusting the sizes of the children.
 final class Split extends StatefulWidget {
   /// Builds a split oriented along [axis].
   Split({
diff --git a/packages/devtools_app_shared/lib/src/ui/theme/_ide_theme_web.dart b/packages/devtools_app_shared/lib/src/ui/theme/_ide_theme_web.dart
index 48aa88a..4bc5f56 100644
--- a/packages/devtools_app_shared/lib/src/ui/theme/_ide_theme_web.dart
+++ b/packages/devtools_app_shared/lib/src/ui/theme/_ide_theme_web.dart
@@ -4,7 +4,7 @@
 
 import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import '../../utils/url/url.dart';
 import '../../utils/utils.dart';
diff --git a/packages/devtools_app_shared/lib/src/ui/theme/theme.dart b/packages/devtools_app_shared/lib/src/ui/theme/theme.dart
index 2389eff..a8c69a1 100644
--- a/packages/devtools_app_shared/lib/src/ui/theme/theme.dart
+++ b/packages/devtools_app_shared/lib/src/ui/theme/theme.dart
@@ -88,6 +88,7 @@
         minimumSize: Size(buttonMinWidth, defaultButtonHeight),
         fixedSize: Size.fromHeight(defaultButtonHeight),
         foregroundColor: theme.colorScheme.onSurface,
+        padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
       ),
     ),
     textButtonTheme: TextButtonThemeData(
@@ -103,11 +104,26 @@
         fixedSize: Size.fromHeight(defaultButtonHeight),
         backgroundColor: theme.colorScheme.primary,
         foregroundColor: theme.colorScheme.onPrimary,
+        padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
       ),
     ),
+    menuButtonTheme: MenuButtonThemeData(
+      style: ButtonStyle(
+        textStyle: MaterialStatePropertyAll<TextStyle>(theme.regularTextStyle),
+        fixedSize: const MaterialStatePropertyAll<Size>(Size.fromHeight(24.0)),
+      ),
+    ),
+    dropdownMenuTheme: DropdownMenuThemeData(
+      textStyle: theme.regularTextStyle,
+    ),
     progressIndicatorTheme: ProgressIndicatorThemeData(
       linearMinHeight: defaultLinearProgressIndicatorHeight,
     ),
+    textTheme: theme.textTheme.copyWith(
+      bodySmall: theme.regularTextStyle,
+      bodyMedium: theme.regularTextStyle,
+      titleSmall: theme.regularTextStyle.copyWith(fontWeight: FontWeight.w400),
+    ),
   );
 }
 
@@ -203,7 +219,7 @@
 /// A value of 0.5 would result in all colours being considered light/dark, and
 /// a value of 0.12 allowing around only the 12% darkest/lightest colours by
 /// Flutter's luminance calculation.
-/// 12% was chosen becaues VS Code's default light background color is #f3f3f3
+/// 12% was chosen because VS Code's default light background color is #f3f3f3
 /// which is a little under 11%.
 const _lightDarkLuminanceThreshold = 0.12;
 
@@ -223,50 +239,49 @@
 
 // Size constants:
 double get defaultToolbarHeight => scaleByFontFactor(32.0);
-double defaultHeaderHeight({bool isDense = false}) =>
-    isDense ? scaleByFontFactor(34.0) : scaleByFontFactor(38.0);
-double get defaultButtonHeight => scaleByFontFactor(32.0);
-double get defaultSwitchHeight => scaleByFontFactor(26.0);
+double get defaultHeaderHeight => scaleByFontFactor(28.0);
+double get defaultButtonHeight => scaleByFontFactor(26.0);
+double get defaultRowHeight => scaleByFontFactor(24.0);
 double get defaultLinearProgressIndicatorHeight => scaleByFontFactor(4.0);
-double get buttonMinWidth => scaleByFontFactor(36.0);
+double get buttonMinWidth => scaleByFontFactor(26.0);
 
-const defaultIconSizeBeforeScaling = 16.0;
-const defaultActionsIconSizeBeforeScaling = 20.0;
+const defaultIconSizeBeforeScaling = 14.0;
+const defaultActionsIconSizeBeforeScaling = 18.0;
 double get defaultIconSize => scaleByFontFactor(defaultIconSizeBeforeScaling);
 double get actionsIconSize =>
     scaleByFontFactor(defaultActionsIconSizeBeforeScaling);
 double get tooltipIconSize => scaleByFontFactor(12.0);
 double get tableIconSize => scaleByFontFactor(12.0);
-double get defaultListItemHeight => scaleByFontFactor(28.0);
+double get defaultListItemHeight => scaleByFontFactor(24.0);
 double get defaultDialogWidth => scaleByFontFactor(700.0);
 
 const extraWideSearchFieldWidth = 600.0;
 const wideSearchFieldWidth = 400.0;
 const defaultSearchFieldWidth = 200.0;
 
-double get defaultTextFieldHeight => scaleByFontFactor(32.0);
+double get defaultTextFieldHeight => scaleByFontFactor(26.0);
 double get defaultTextFieldNumberWidth => scaleByFontFactor(100.0);
 
 // TODO(jacobr) define a more sophisticated formula for chart height.
 // The chart height does need to increase somewhat to leave room for the legend
 // and tick marks but does not need to scale linearly with the font factor.
-double get defaultChartHeight => scaleByFontFactor(120.0);
+double get defaultChartHeight => scaleByFontFactor(110.0);
 
 double get actionWidgetSize => scaleByFontFactor(48.0);
 
-double get statusLineHeight => scaleByFontFactor(24.0);
+double get statusLineHeight => scaleByFontFactor(20.0);
 
 double get inputDecorationElementHeight => scaleByFontFactor(20.0);
 
 // Padding / spacing constants:
-const largeSpacing = 32.0;
-const defaultSpacing = 16.0;
-const intermediateSpacing = 12.0;
+const extraLargeSpacing = 32.0;
+const largeSpacing = 16.0;
+const defaultSpacing = 12.0;
+const intermediateSpacing = 10.0;
 const denseSpacing = 8.0;
-const denseModeDenseSpacing = 2.0;
 
 const defaultTabBarPadding = 14.0;
-const tabBarSpacing = 14.0;
+const tabBarSpacing = 8.0;
 const denseRowSpacing = 6.0;
 
 const hoverCardBorderSize = 2.0;
@@ -291,7 +306,7 @@
 // Font size constants:
 
 double get defaultFontSize => scaleByFontFactor(unscaledDefaultFontSize);
-const unscaledDefaultFontSize = 14.0;
+const unscaledDefaultFontSize = 12.0;
 
 double get smallFontSize => scaleByFontFactor(unscaledSmallFontSize);
 const unscaledSmallFontSize = 10.0;
@@ -341,25 +356,24 @@
   bool get isDarkTheme => brightness == Brightness.dark;
 
   TextStyle get regularTextStyle => fixBlurryText(
-        TextStyle(
+        textTheme.bodySmall!.copyWith(
           color: colorScheme.onSurface,
           fontSize: defaultFontSize,
         ),
       );
 
+  TextStyle regularTextStyleWithColor(Color? color) =>
+      regularTextStyle.copyWith(color: color);
+
   TextStyle get boldTextStyle =>
       regularTextStyle.copyWith(fontWeight: FontWeight.bold);
 
-  TextStyle get subtleTextStyle => fixBlurryText(
-        TextStyle(
-          color: colorScheme.subtleTextColor,
-        ),
-      );
+  TextStyle get subtleTextStyle =>
+      regularTextStyle.copyWith(color: colorScheme.subtleTextColor);
 
   TextStyle get fixedFontStyle => fixBlurryText(
-        textTheme.bodyMedium!.copyWith(
+        regularTextStyle.copyWith(
           fontFamily: 'RobotoMono',
-          color: colorScheme.onSurface,
           // Slightly smaller for fixes font text since it will appear larger
           // to begin with.
           fontSize: defaultFontSize - 1,
diff --git a/packages/devtools_app_shared/lib/src/utils/url/_url_web.dart b/packages/devtools_app_shared/lib/src/utils/url/_url_web.dart
index cc240f0..def617b 100644
--- a/packages/devtools_app_shared/lib/src/utils/url/_url_web.dart
+++ b/packages/devtools_app_shared/lib/src/utils/url/_url_web.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.
 
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 Map<String, String> loadQueryParams({String Function(String)? urlModifier}) {
   var url = getWebUrl()!;
diff --git a/packages/devtools_app_shared/lib/src/utils/web_utils.dart b/packages/devtools_app_shared/lib/src/utils/web_utils.dart
index deec56d..515de5f 100644
--- a/packages/devtools_app_shared/lib/src/utils/web_utils.dart
+++ b/packages/devtools_app_shared/lib/src/utils/web_utils.dart
@@ -4,13 +4,12 @@
 
 import 'dart:js_interop';
 
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 extension MessageExtension on Event {
   bool get isMessageEvent =>
       // TODO(srujzs): This is necessary in order to support package:web 0.4.0.
       // This was not needed with 0.3.0, hence the lint.
-      // ignore: avoid-unnecessary-type-casts
       (this as JSObject).instanceOfString('MessageEvent');
 }
 
diff --git a/packages/devtools_app_shared/lib/ui.dart b/packages/devtools_app_shared/lib/ui.dart
index 921f8a0..ab04edb 100644
--- a/packages/devtools_app_shared/lib/ui.dart
+++ b/packages/devtools_app_shared/lib/ui.dart
@@ -4,6 +4,7 @@
 
 export 'src/ui/common.dart';
 export 'src/ui/dialogs.dart';
+export 'src/ui/flex_split_column.dart';
 export 'src/ui/split.dart';
 export 'src/ui/theme/ide_theme.dart';
 export 'src/ui/theme/theme.dart';
diff --git a/packages/devtools_app_shared/pubspec.yaml b/packages/devtools_app_shared/pubspec.yaml
index ad9faf9..1b133ca 100644
--- a/packages/devtools_app_shared/pubspec.yaml
+++ b/packages/devtools_app_shared/pubspec.yaml
@@ -1,7 +1,7 @@
 name: devtools_app_shared
 description: Package of Dart & Flutter structures shared between devtools_app and devtools extensions.
-version: 0.0.8
-repository: https://github.com/flutter/devtools/tree/master/packages/devtools_extensions
+version: 0.0.10-wip
+repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app_shared
 
 environment:
   sdk: '>=3.3.0-91.0.dev <4.0.0'
@@ -10,13 +10,14 @@
 dependencies:
   collection: ^1.15.0
   devtools_shared: ^6.0.1
+  dtd: ^0.0.3
   flutter:
     sdk: flutter
   logging: ^1.1.1
   meta: ^1.9.1
   pointer_interceptor: ^0.9.3+3
-  vm_service: ^13.0.0
-  web: ^0.4.0
+  vm_service: ^14.0.0
+  web: ^0.4.1
 
 dev_dependencies:
   flutter_lints: ^2.0.3
@@ -57,4 +58,4 @@
 
 dependency_overrides:
   devtools_shared:
-    path: ../devtools_shared
\ No newline at end of file
+    path: ../devtools_shared
diff --git a/packages/devtools_app/test/shared/flex_split_column_test.dart b/packages/devtools_app_shared/test/ui/flex_split_column_test.dart
similarity index 90%
rename from packages/devtools_app/test/shared/flex_split_column_test.dart
rename to packages/devtools_app_shared/test/ui/flex_split_column_test.dart
index c95c05f..5decdd0 100644
--- a/packages/devtools_app/test/shared/flex_split_column_test.dart
+++ b/packages/devtools_app_shared/test/ui/flex_split_column_test.dart
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:devtools_app/src/shared/flex_split_column.dart';
-import 'package:devtools_app/src/shared/primitives/utils.dart';
+import 'package:collection/collection.dart';
+import 'package:devtools_app_shared/src/ui/flex_split_column.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -45,7 +45,7 @@
         totalHeight,
       );
       expect(
-        collectionEquals(
+        const DeepCollectionEquality().equals(
           adjustedFractions,
           [
             0.2857142857142857,
@@ -65,7 +65,8 @@
         headers,
       );
       expect(
-        collectionEquals(adjustedFractions, [60.0, 10.0, 10.0, 10.0]),
+        const DeepCollectionEquality()
+            .equals(adjustedFractions, [60.0, 10.0, 10.0, 10.0]),
         isTrue,
       );
     });
diff --git a/packages/devtools_app_shared/test/utils/auto_dispose_mixin_test.dart b/packages/devtools_app_shared/test/utils/auto_dispose_mixin_test.dart
index 91fb6c8..87978fa 100644
--- a/packages/devtools_app_shared/test/utils/auto_dispose_mixin_test.dart
+++ b/packages/devtools_app_shared/test/utils/auto_dispose_mixin_test.dart
@@ -45,17 +45,17 @@
   group('Disposer', () {
     test('disposes streams', () {
       final disposer = Disposer();
-      final controller1 = StreamController(sync: true);
-      final controller2 = StreamController(sync: true);
+      final controller1 = StreamController<void>(sync: true);
+      final controller2 = StreamController<void>(sync: true);
       var c1Events = 0;
       var c2Events = 0;
       disposer.autoDisposeStreamSubscription(
-        controller1.stream.listen((data) {
+        controller1.stream.listen((_) {
           c1Events++;
         }),
       );
       disposer.autoDisposeStreamSubscription(
-        controller2.stream.listen((data) {
+        controller2.stream.listen((_) {
           c2Events++;
         }),
       );
@@ -324,7 +324,7 @@
   testWidgets('Test stream auto dispose', (WidgetTester tester) async {
     // Build our app and trigger a frame.
     final key = GlobalKey();
-    final controller = StreamController();
+    final controller = StreamController<void>();
     await tester.pumpWidget(AutoDisposedWidget(controller.stream, key: key));
 
     final state = key.currentState as _AutoDisposedWidgetState;
diff --git a/packages/devtools_extensions/CHANGELOG.md b/packages/devtools_extensions/CHANGELOG.md
index b22d13c..b9784fc 100644
--- a/packages/devtools_extensions/CHANGELOG.md
+++ b/packages/devtools_extensions/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 0.0.14-wip
+* Add a global `dtdManager` for interacting with the Dart Tooling Daemon.
+* Bump `package:vm_service` dependency to ^14.0.0.
+
+## 0.0.13
+* Bump `package:web` to `^0.4.1`.
+* Fix `README.md` instructions for adding a `.pubignore` file.
+* Make Simulated DevTools Environment Panel collapsible.
+
 ## 0.0.12
 * Fix a bug preventing Dart server apps from connecting to DevTools extensions.
 
diff --git a/packages/devtools_extensions/README.md b/packages/devtools_extensions/README.md
index 26cbc28..e587103 100644
--- a/packages/devtools_extensions/README.md
+++ b/packages/devtools_extensions/README.md
@@ -245,7 +245,7 @@
 `extension/devtools/` directory:
 
 ```
-!./build
+!build
 ```
 
 This will ensure that, even if the `extension/devtools/build` directory has been been git
diff --git a/packages/devtools_extensions/example/foo/packages/foo/extension/devtools/.pubignore b/packages/devtools_extensions/example/foo/packages/foo/extension/devtools/.pubignore
index 85d9a32..71860a7 100644
--- a/packages/devtools_extensions/example/foo/packages/foo/extension/devtools/.pubignore
+++ b/packages/devtools_extensions/example/foo/packages/foo/extension/devtools/.pubignore
@@ -1 +1 @@
-!./build
+!build
diff --git a/packages/devtools_extensions/integration_test/test/simulated_environment_test.dart b/packages/devtools_extensions/integration_test/test/simulated_environment_test.dart
index 5be3930..d5272ee 100644
--- a/packages/devtools_extensions/integration_test/test/simulated_environment_test.dart
+++ b/packages/devtools_extensions/integration_test/test/simulated_environment_test.dart
@@ -50,6 +50,8 @@
     await _testShowNotification(tester, simController);
     logStatus('test showing a banner message from the extension');
     await _testShowBannerMessage(tester, simController);
+    logStatus('test collapsing environment panel');
+    await _testCollapseEnvironmentPanel(tester, simController);
 
     // NOTE: the force reload functionality cannot be tested because it will
     // make this test run in an infinite loop (it refreshes the whole window
@@ -192,6 +194,65 @@
   await _clearLogs(tester, simController);
 }
 
+Future<void> _testCollapseEnvironmentPanel(
+  WidgetTester tester,
+  SimulatedDevToolsController simController,
+) async {
+  final split = tester.widget<Split>(find.byType(Split));
+
+  final divider = find.byKey(split.dividerKey(0));
+  final environmentPanel = split.children[1];
+
+  final environmentPanelSizedBox = find.descendant(
+    of: find.byWidget(environmentPanel),
+    matching: find.byType(SizedBox),
+  );
+
+  final double environmentPanelSizedBoxWidth =
+      tester.firstWidget<SizedBox>(environmentPanelSizedBox).width!;
+
+  // Check that the [environmentPanelSizedBoxWidth] is the expected width.
+  expect(
+    environmentPanelSizedBoxWidth,
+    VmServiceConnection.totalControlsWidth + 2 * defaultSpacing,
+  );
+
+  // Drag the divider to the right by [environmentPanelSizedBoxWidth].
+  await tester.drag(
+    divider,
+    Offset(
+      environmentPanelSizedBoxWidth,
+      0,
+    ),
+  );
+  await tester.pumpAndSettle();
+
+  final Rect simulatedDevToolsWrapperRect =
+      tester.getRect(find.byType(SimulatedDevToolsWrapper));
+  final Rect environmentPanelRect =
+      tester.getRect(find.byWidget(environmentPanel));
+
+  // Verify that the environment panel is off screen to the right of the
+  // simulated devtools wrapper.
+  expect(
+    simulatedDevToolsWrapperRect.right,
+    lessThanOrEqualTo(environmentPanelRect.left.ceil()),
+  );
+
+  // Drag the divider to the left by [environmentPanelSizedBoxWidth].
+  //
+  // This is to bring the 'Clear logs' button into view so it can be tapped.
+  await tester.drag(
+    divider,
+    Offset(
+      -environmentPanelSizedBoxWidth,
+      0,
+    ),
+  );
+  await tester.pumpAndSettle();
+  await _clearLogs(tester, simController);
+}
+
 Future<void> _clearLogs(
   WidgetTester tester,
   SimulatedDevToolsController simController,
diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_connect_ui.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_connect_ui.dart
index 14ef3c7..69d0ff8 100644
--- a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_connect_ui.dart
+++ b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_connect_ui.dart
@@ -4,14 +4,18 @@
 
 part of '_simulated_devtools_environment.dart';
 
-class _VmServiceConnection extends StatelessWidget {
-  const _VmServiceConnection({
+@visibleForTesting
+class VmServiceConnection extends StatelessWidget {
+  const VmServiceConnection({
+    super.key,
     required this.simController,
     required this.connected,
   });
 
-  static const _totalControlsHeight = 45.0;
-  static const _totalControlsWidth = 415.0;
+  @visibleForTesting
+  static const totalControlsHeight = 45.0;
+  @visibleForTesting
+  static const totalControlsWidth = 415.0;
 
   final SimulatedDevToolsController simController;
   final bool connected;
@@ -19,7 +23,7 @@
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: _totalControlsHeight,
+      height: totalControlsHeight,
       child: connected
           ? _ConnectedVmServiceDisplay(simController: simController)
           : _DisconnectedVmServiceDisplay(simController: simController),
diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_environment.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_environment.dart
index 6cc8dde..2d9faa9 100644
--- a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_environment.dart
+++ b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_environment.dart
@@ -4,12 +4,13 @@
 
 import 'dart:async';
 import 'dart:js_interop';
+import 'dart:math' as math;
 
 import 'package:devtools_app_shared/service.dart';
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/material.dart';
-import 'package:web/helpers.dart' hide Text;
+import 'package:web/web.dart' hide Text;
 
 import '../../api/api.dart';
 import '../../api/model.dart';
@@ -48,6 +49,8 @@
     with AutoDisposeMixin {
   late final SimulatedDevToolsController simController;
 
+  late final ScrollController scrollController;
+
   late ConnectedState connectionState;
 
   bool get connected => connectionState.connected;
@@ -57,6 +60,8 @@
     super.initState();
     simController = SimulatedDevToolsController()..init();
 
+    scrollController = ScrollController();
+
     connectionState = serviceManager.connectedState.value;
     addAutoDisposeListener(serviceManager.connectedState, () {
       setState(() {
@@ -68,79 +73,118 @@
   @override
   void dispose() {
     simController.dispose();
+    scrollController.dispose();
     super.dispose();
   }
 
   @override
   Widget build(BuildContext context) {
     final theme = Theme.of(context);
-    return Split(
-      axis: Axis.horizontal,
-      initialFractions: const [0.8, 0.2],
-      minSizes: const [
-        100.0,
-        _VmServiceConnection._totalControlsWidth + 2 * defaultSpacing,
-      ],
-      children: [
-        OutlineDecoration.onlyRight(
-          child: Padding(
-            padding: const EdgeInsets.all(defaultSpacing),
-            child: widget.child,
-          ),
-        ),
-        OutlineDecoration.onlyLeft(
-          child: Padding(
-            padding: const EdgeInsets.all(defaultSpacing),
-            child: Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                Text(
-                  'Simulated DevTools Environment',
-                  style: theme.textTheme.titleMedium,
-                ),
-                const PaddedDivider(),
-                _VmServiceConnection(
-                  connected: connected,
-                  simController: simController,
-                ),
-                const SizedBox(height: denseSpacing),
-                _SimulatedApi(
-                  simController: simController,
-                  requiresRunningApplication: widget.requiresRunningApplication,
-                  connectedToApplication: connected,
-                ),
-                const PaddedDivider(),
-                Expanded(
-                  child: Column(
-                    crossAxisAlignment: CrossAxisAlignment.start,
-                    children: [
-                      Row(
-                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                        children: [
-                          Text(
-                            'Logs:',
-                            style: theme.textTheme.titleMedium,
-                          ),
-                          DevToolsButton.iconOnly(
-                            icon: Icons.clear,
-                            outlined: false,
-                            tooltip: 'Clear logs',
-                            onPressed: () => simController.messageLogs.clear(),
-                          ),
-                        ],
-                      ),
-                      const PaddedDivider.thin(),
-                      Expanded(
-                        child: _LogMessages(simController: simController),
-                      ),
-                    ],
-                  ),
-                ),
-              ],
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final availableWidth = constraints.maxWidth;
+        const environmentPanelMinWidth =
+            VmServiceConnection.totalControlsWidth + 2 * defaultSpacing;
+
+        final environmentPanelFraction =
+            environmentPanelMinWidth / availableWidth;
+        final childFraction = 1 - environmentPanelFraction;
+
+        return Split(
+          axis: Axis.horizontal,
+          initialFractions: [childFraction, environmentPanelFraction],
+          minSizes: const [100.0, 0.0],
+          children: [
+            OutlineDecoration.onlyRight(
+              child: Padding(
+                padding: const EdgeInsets.all(defaultSpacing),
+                child: widget.child,
+              ),
             ),
-          ),
-        ),
-      ],
+            LayoutBuilder(
+              builder: (context, environmentPanelConstraints) {
+                final availableEnvironmentPanelWidth =
+                    environmentPanelConstraints.maxWidth;
+                final environmentPanelWidth = math.max(
+                  environmentPanelMinWidth,
+                  availableEnvironmentPanelWidth,
+                );
+
+                return Scrollbar(
+                  controller: scrollController,
+                  thumbVisibility: true,
+                  child: SingleChildScrollView(
+                    controller: scrollController,
+                    scrollDirection: Axis.horizontal,
+                    child: SizedBox(
+                      width: environmentPanelWidth,
+                      child: OutlineDecoration.onlyLeft(
+                        child: Padding(
+                          padding: const EdgeInsets.all(defaultSpacing),
+                          child: Column(
+                            crossAxisAlignment: CrossAxisAlignment.start,
+                            children: [
+                              Text(
+                                'Simulated DevTools Environment',
+                                style: theme.textTheme.titleMedium,
+                              ),
+                              const PaddedDivider(),
+                              VmServiceConnection(
+                                connected: connected,
+                                simController: simController,
+                              ),
+                              const SizedBox(height: denseSpacing),
+                              // TODO(kenz): figure out how to simulate the DTD
+                              // connection. This may be non-trivial.
+                              _SimulatedApi(
+                                simController: simController,
+                                requiresRunningApplication:
+                                    widget.requiresRunningApplication,
+                                connectedToApplication: connected,
+                              ),
+                              const PaddedDivider(),
+                              Expanded(
+                                child: Column(
+                                  crossAxisAlignment: CrossAxisAlignment.start,
+                                  children: [
+                                    Row(
+                                      mainAxisAlignment:
+                                          MainAxisAlignment.spaceBetween,
+                                      children: [
+                                        Text(
+                                          'Logs:',
+                                          style: theme.textTheme.titleMedium,
+                                        ),
+                                        DevToolsButton.iconOnly(
+                                          icon: Icons.clear,
+                                          outlined: false,
+                                          tooltip: 'Clear logs',
+                                          onPressed: () =>
+                                              simController.messageLogs.clear(),
+                                        ),
+                                      ],
+                                    ),
+                                    const PaddedDivider.thin(),
+                                    Expanded(
+                                      child: _LogMessages(
+                                        simController: simController,
+                                      ),
+                                    ),
+                                  ],
+                                ),
+                              ),
+                            ],
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                );
+              },
+            ),
+          ],
+        );
+      },
     );
   }
 }
diff --git a/packages/devtools_extensions/lib/src/template/devtools_extension.dart b/packages/devtools_extensions/lib/src/template/devtools_extension.dart
index 69acf7b..e74afe8 100644
--- a/packages/devtools_extensions/lib/src/template/devtools_extension.dart
+++ b/packages/devtools_extensions/lib/src/template/devtools_extension.dart
@@ -14,7 +14,7 @@
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:vm_service/vm_service.dart' hide Event;
-import 'package:web/helpers.dart' hide Text;
+import 'package:web/web.dart' hide Text;
 
 import '../api/api.dart';
 import '../api/model.dart';
@@ -63,6 +63,23 @@
 ServiceManager get serviceManager =>
     _accessGlobalOrThrow<ServiceManager>(globalName: 'serviceManager');
 
+/// A manager for interacting with the Dart Tooling Daemon, if available.
+///
+/// This manager stores the current [DTDConnection], which provides access to
+/// public methods registered by other DTD clients (for example, the IDE), as
+/// well as a minimal file sytsem API for reading, writing, and listing
+/// directories within the user's project.
+///
+/// [dtdManager] can only be accessed below the [DevToolsExtension] widget
+/// in the widget tree, since it is initialized as part of the
+/// [DevToolsExtension]'s [initState] lifecycle method.
+///
+/// DevTools extensions should not manually call [dtdManager.connect] or
+/// [dtdManager.disconnect], since this lifecycle is already handled by the
+/// [DevToolsExtension] widget.
+DTDManager get dtdManager =>
+    _accessGlobalOrThrow<DTDManager>(globalName: 'dtdManager');
+
 T _accessGlobalOrThrow<T>({required String globalName}) {
   final manager = globals[T] as T?;
   if (manager == null) {
@@ -134,22 +151,25 @@
   void _initGlobals() {
     setGlobal(ExtensionManager, ExtensionManager());
     setGlobal(ServiceManager, ServiceManager());
+    setGlobal(DTDManager, DTDManager());
     // TODO(kenz): pull the IDE theme from the url query params.
     setGlobal(IdeTheme, IdeTheme());
   }
 
-  void _shutdown() {
+  Future<void> _shutdown() async {
     (globals[ExtensionManager] as ExtensionManager?)?._dispose();
     removeGlobal(ExtensionManager);
     removeGlobal(ServiceManager);
     removeGlobal(IdeTheme);
+    await (globals[DTDManager] as DTDManager?)?.disconnect();
+    removeGlobal(DTDManager);
   }
 
   @override
-  void dispose() {
+  Future<void> dispose() async {
     // TODO(https://github.com/flutter/flutter/issues/10437): dispose is never
     // called on hot restart, so these resources leak for local development.
-    _shutdown();
+    unawaited(_shutdown());
     super.dispose();
   }
 
diff --git a/packages/devtools_extensions/lib/src/template/extension_manager.dart b/packages/devtools_extensions/lib/src/template/extension_manager.dart
index 246379f..6d391f2 100644
--- a/packages/devtools_extensions/lib/src/template/extension_manager.dart
+++ b/packages/devtools_extensions/lib/src/template/extension_manager.dart
@@ -6,6 +6,9 @@
 
 final _log = Logger('devtools_extensions/extension_manager');
 
+const _vmServiceQueryParameter = 'uri';
+const _dtdQueryParameter = 'dtdUri';
+
 class ExtensionManager {
   final _registeredEventHandlers =
       <DevToolsExtensionEventType, ExtensionEventHandler>{};
@@ -53,7 +56,7 @@
     final themeValue = queryParams[ExtensionEventParameters.theme];
     _setThemeForValue(themeValue);
 
-    final vmServiceUri = queryParams['uri'];
+    final vmServiceUri = queryParams[_vmServiceQueryParameter];
     if (connectToVmService) {
       if (vmServiceUri == null) {
         // Request the vm service uri for the connected app. DevTools will
@@ -68,6 +71,11 @@
         unawaited(_connectToVmService(vmServiceUri));
       }
     }
+
+    final dtdUri = queryParams[_dtdQueryParameter];
+    if (dtdUri != null) {
+      unawaited(_connectToDtd(dtdUri));
+    }
   }
 
   // ignore: unused_element, false positive due to part files
@@ -154,8 +162,8 @@
       if (serviceManager.hasConnection) {
         await serviceManager.manuallyDisconnect();
       }
-      if (loadQueryParams().containsKey('uri')) {
-        _updateQueryParameter('uri', null);
+      if (loadQueryParams().containsKey(_vmServiceQueryParameter)) {
+        _updateQueryParameter(_vmServiceQueryParameter, null);
       }
       return;
     }
@@ -176,7 +184,10 @@
         vmService,
         onClosed: finishedCompleter.future,
       );
-      _updateQueryParameter('uri', serviceManager.service!.wsUri!);
+      _updateQueryParameter(
+        _vmServiceQueryParameter,
+        serviceManager.service!.wsUri!,
+      );
     } catch (e) {
       final errorMessage =
           'Unable to connect extension to VM service at $vmServiceUri: $e';
@@ -185,6 +196,33 @@
     }
   }
 
+  Future<void> _connectToDtd(String? dtdUri) async {
+    // TODO(kenz): investigate. this is weird but `dtdUri` != null even
+    // when the `toString()` representation is 'null'.
+    if (dtdUri == null || dtdUri == 'null') {
+      if (dtdManager.hasConnection) {
+        await dtdManager.disconnect();
+      }
+      if (loadQueryParams().containsKey(_dtdQueryParameter)) {
+        _updateQueryParameter(_dtdQueryParameter, null);
+      }
+      return;
+    }
+
+    try {
+      await dtdManager.connect(Uri.parse(dtdUri));
+      _updateQueryParameter(
+        _dtdQueryParameter,
+        dtdManager.uri.toString(),
+      );
+    } catch (e) {
+      final errorMessage =
+          'Unable to connect extension to the Dart Tooling Daemon at $dtdUri: $e';
+      showNotification('Error: $errorMessage');
+      _log.shout(errorMessage);
+    }
+  }
+
   void _setThemeForValue(String? themeValue) {
     final useDarkTheme = (themeValue == null && useDarkThemeAsDefault) ||
         themeValue == ExtensionEventParameters.themeValueDark;
diff --git a/packages/devtools_extensions/lib/src/utils.dart b/packages/devtools_extensions/lib/src/utils.dart
index 8fd8689..19cdce8 100644
--- a/packages/devtools_extensions/lib/src/utils.dart
+++ b/packages/devtools_extensions/lib/src/utils.dart
@@ -5,7 +5,7 @@
 import 'dart:js_interop';
 
 import 'package:devtools_app_shared/web_utils.dart';
-import 'package:web/helpers.dart';
+import 'package:web/web.dart';
 
 import '../api.dart';
 
diff --git a/packages/devtools_extensions/pubspec.yaml b/packages/devtools_extensions/pubspec.yaml
index 7ea82ce..8531c36 100644
--- a/packages/devtools_extensions/pubspec.yaml
+++ b/packages/devtools_extensions/pubspec.yaml
@@ -1,6 +1,6 @@
 name: devtools_extensions
 description: A package for building and supporting extensions for Dart DevTools.
-version: 0.0.12
+version: 0.0.14-wip
 
 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_extensions
 
@@ -20,8 +20,8 @@
   io: ^1.0.4
   path: ^1.8.0
   logging: ^1.1.1
-  vm_service: ^13.0.0
-  web: ^0.4.0
+  vm_service: ^14.0.0
+  web: ^0.4.1
 
 dev_dependencies:
   flutter_driver:
diff --git a/packages/devtools_extensions/test/web/devtools_extension_test.dart b/packages/devtools_extensions/test/web/devtools_extension_test.dart
index 223b902..1cd40b4 100644
--- a/packages/devtools_extensions/test/web/devtools_extension_test.dart
+++ b/packages/devtools_extensions/test/web/devtools_extension_test.dart
@@ -13,6 +13,7 @@
     test('accessing early throws error', () {
       expect(() => serviceManager, throwsStateError);
       expect(() => extensionManager, throwsStateError);
+      expect(() => dtdManager, throwsStateError);
     });
 
     testWidgets(
@@ -21,6 +22,7 @@
         await tester.pumpWidget(const DevToolsExtension(child: SizedBox()));
         expect(serviceManager, isNotNull);
         expect(extensionManager, isNotNull);
+        expect(dtdManager, isNotNull);
       },
     );
   });
diff --git a/packages/devtools_shared/CHANGELOG.md b/packages/devtools_shared/CHANGELOG.md
index 73316ed..db91768 100644
--- a/packages/devtools_shared/CHANGELOG.md
+++ b/packages/devtools_shared/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 6.0.4
+* Add `apiGetDtdUri` to the server api.
+* Add a description and link to documentation to the `devtools_options.yaml` file that
+is created in a user's project.
+
 # 6.0.3
 * `CompareMixin` is now generic, implementing `Comparable<T>` instead of
   `Comparable<dynamic>`, and it's operators each therefore accept a `T`
diff --git a/packages/devtools_shared/lib/src/deeplink/app_link_settings.dart b/packages/devtools_shared/lib/src/deeplink/app_link_settings.dart
index 8530279..ac07ccf 100644
--- a/packages/devtools_shared/lib/src/deeplink/app_link_settings.dart
+++ b/packages/devtools_shared/lib/src/deeplink/app_link_settings.dart
@@ -12,10 +12,14 @@
   const AppLinkSettings._(this.applicationId, this.deeplinks);
 
   factory AppLinkSettings.fromJson(String json) {
-    final jsonObject = jsonDecode(json);
+    final jsonObject = jsonDecode(json) as Map;
+    final {
+      _kApplicationIdKey: String applicationId,
+      _kDeeplinksKey: List<Object?> deepLinks,
+    } = jsonObject;
     return AppLinkSettings._(
-      jsonObject[_kApplicationIdKey] as String,
-      (jsonObject[_kDeeplinksKey] as List<dynamic>)
+      applicationId,
+      deepLinks
           .cast<Map<String, dynamic>>()
           .map<AndroidDeeplink>(AndroidDeeplink._fromJsonObject)
           .toList(),
diff --git a/packages/devtools_shared/lib/src/devtools_api.dart b/packages/devtools_shared/lib/src/devtools_api.dart
index 63441fa..cc2a312 100644
--- a/packages/devtools_shared/lib/src/devtools_api.dart
+++ b/packages/devtools_shared/lib/src/devtools_api.dart
@@ -62,6 +62,11 @@
 
 const testAppSizeFilePropertyName = 'appSizeTest';
 
+abstract class DtdApi {
+  static const apiGetDtdUri = '${apiPrefix}getDtdUri';
+  static const uriPropertyName = 'dtdUri';
+}
+
 abstract class ExtensionsApi {
   /// Serves any available extensions and returns a list of their configurations
   /// to DevTools.
@@ -71,6 +76,8 @@
   /// The property name for the query parameter passed along with
   /// extension-related requests to the server that describes the package root
   /// for the app whose extensions are being queried.
+  ///
+  /// This field is a file:// URI string and NOT a path.
   static const extensionRootPathPropertyName = 'rootPath';
 
   /// The property name for the response that the server sends back upon
diff --git a/packages/devtools_shared/lib/src/extensions/extension_enablement.dart b/packages/devtools_shared/lib/src/extensions/extension_enablement.dart
index a3b69bb..721f781 100644
--- a/packages/devtools_shared/lib/src/extensions/extension_enablement.dart
+++ b/packages/devtools_shared/lib/src/extensions/extension_enablement.dart
@@ -14,10 +14,12 @@
 /// Manages the `devtools_options.yaml` file and allows read / write access.
 class DevToolsOptions {
   static const optionsFileName = 'devtools_options.yaml';
-
   static const _extensionsKey = 'extensions';
-
+  static const _descriptionKey = 'description';
+  static const _documentationKey = 'documentation';
   static const _defaultOptions = '''
+$_descriptionKey: This file stores settings for Dart & Flutter DevTools.
+$_documentationKey: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
 $_extensionsKey:
 ''';
 
diff --git a/packages/devtools_shared/lib/src/memory/class_heap_detail_stats.dart b/packages/devtools_shared/lib/src/memory/class_heap_detail_stats.dart
index ede83de..0201c6e 100644
--- a/packages/devtools_shared/lib/src/memory/class_heap_detail_stats.dart
+++ b/packages/devtools_shared/lib/src/memory/class_heap_detail_stats.dart
@@ -20,8 +20,7 @@
         isStacktraced = traceAllocations;
 
   factory ClassHeapDetailStats.fromJson(Map<String, dynamic> json) {
-    final classId = json['class']['id'];
-    final className = json['class']['name'];
+    final {'id': classId, 'name': className} = json['class'] as Map;
 
     return ClassHeapDetailStats(
       ClassRef(id: classId, name: className),
diff --git a/packages/devtools_shared/lib/src/server/file_system.dart b/packages/devtools_shared/lib/src/server/file_system.dart
index 93c2ecb..eb89707 100644
--- a/packages/devtools_shared/lib/src/server/file_system.dart
+++ b/packages/devtools_shared/lib/src/server/file_system.dart
@@ -71,7 +71,7 @@
     if (!fileName.endsWith('.json')) return null;
 
     final content = file.readAsStringSync();
-    final json = jsonDecode(content);
+    final json = jsonDecode(content) as Map;
     json['lastModifiedTime'] = file.lastModifiedSync().toString();
     return jsonEncode(json);
   }
diff --git a/packages/devtools_shared/lib/src/server/server_api.dart b/packages/devtools_shared/lib/src/server/server_api.dart
index f80b2a0..5c35fb7 100644
--- a/packages/devtools_shared/lib/src/server/server_api.dart
+++ b/packages/devtools_shared/lib/src/server/server_api.dart
@@ -36,6 +36,7 @@
     required ExtensionsManager extensionsManager,
     required DeeplinkManager deeplinkManager,
     ServerApi? api,
+    String? dtdUri,
   }) {
     api ??= ServerApi();
     final queryParams = request.requestedUri.queryParameters;
@@ -252,7 +253,10 @@
           queryParams,
           deeplinkManager,
         );
-
+      case DtdApi.apiGetDtdUri:
+        return api.setCompleted(
+          json.encode({DtdApi.uriPropertyName: dtdUri}),
+        );
       default:
         return api.notImplemented();
     }
diff --git a/packages/devtools_shared/lib/src/service/service.dart b/packages/devtools_shared/lib/src/service/service.dart
index 9a248cb..eb2a10d 100644
--- a/packages/devtools_shared/lib/src/service/service.dart
+++ b/packages/devtools_shared/lib/src/service/service.dart
@@ -41,7 +41,7 @@
   );
   serviceCompleter.complete(service);
 
-  unawaited(stream.drain().catchError(onError));
+  unawaited(stream.drain<void>().catchError(onError));
   return serviceCompleter.future;
 }
 
@@ -87,9 +87,9 @@
 }) {
   final connectedCompleter = Completer<T>();
 
-  void onError(error) {
+  void onError(Object? error) {
     if (!connectedCompleter.isCompleted) {
-      connectedCompleter.completeError(error);
+      connectedCompleter.completeError(error!);
     }
   }
 
diff --git a/packages/devtools_shared/lib/src/test/cli_test_driver.dart b/packages/devtools_shared/lib/src/test/cli_test_driver.dart
index 049eb13..c676352 100644
--- a/packages/devtools_shared/lib/src/test/cli_test_driver.dart
+++ b/packages/devtools_shared/lib/src/test/cli_test_driver.dart
@@ -176,7 +176,7 @@
               // for an isolate that hasn't started yet. We can just ignore these
               // as on the next trip around the Isolate will be returned.
               // https://github.com/dart-lang/sdk/issues/33747
-              .catchError((error) {
+              .catchError((Object error) {
             print('getIsolate(${ref.id}) failed, skipping\n$error');
             return Future<Isolate>.value(Isolate(id: skipId));
           }),
diff --git a/packages/devtools_shared/lib/src/test/integration_test_runner.dart b/packages/devtools_shared/lib/src/test/integration_test_runner.dart
index f4e137f..6c92db2 100644
--- a/packages/devtools_shared/lib/src/test/integration_test_runner.dart
+++ b/packages/devtools_shared/lib/src/test/integration_test_runner.dart
@@ -8,7 +8,6 @@
 import 'dart:io';
 
 import 'package:args/args.dart';
-import 'package:collection/collection.dart';
 import 'package:meta/meta.dart';
 
 import 'chrome_driver.dart';
diff --git a/packages/devtools_shared/pubspec.yaml b/packages/devtools_shared/pubspec.yaml
index 54ff661..7fa0633 100644
--- a/packages/devtools_shared/pubspec.yaml
+++ b/packages/devtools_shared/pubspec.yaml
@@ -1,12 +1,12 @@
 name: devtools_shared
 description: Package of shared Dart structures between devtools_app, dds, and other tools.
 
-version: 6.0.3
+version: 6.0.4
 
 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_shared
 
 environment:
-  sdk: '>=3.3.0-91.0.dev <4.0.0'
+  sdk: ">=3.3.0-91.0.dev <4.0.0"
 
 dependencies:
   args: ^2.4.2
@@ -17,7 +17,7 @@
   shelf: ^1.1.0
   sse: ^4.1.2
   usage: ^4.0.0
-  vm_service: '>=13.0.0 <15.0.0'
+  vm_service: ">=13.0.0 <15.0.0"
   web_socket_channel: ^2.4.0
   webkit_inspection_protocol: ">=0.5.0 <2.0.0"
   yaml: ^3.1.2
diff --git a/packages/devtools_shared/test/extensions/extension_enablement_test.dart b/packages/devtools_shared/test/extensions/extension_enablement_test.dart
index c370ce1..41305aa 100644
--- a/packages/devtools_shared/test/extensions/extension_enablement_test.dart
+++ b/packages/devtools_shared/test/extensions/extension_enablement_test.dart
@@ -44,6 +44,8 @@
       expect(
         file.readAsStringSync(),
         '''
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
 extensions:
 ''',
       );
@@ -59,6 +61,8 @@
       expect(
         file.readAsStringSync(),
         '''
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
 extensions:
   - foo: true''',
       );
@@ -79,6 +83,8 @@
       expect(
         file.readAsStringSync(),
         '''
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
 extensions:
   - foo: true
   - bar: false''',
diff --git a/packages/devtools_shared/test/server/server_api_test.dart b/packages/devtools_shared/test/server/server_api_test.dart
index 338db21..e661531 100644
--- a/packages/devtools_shared/test/server/server_api_test.dart
+++ b/packages/devtools_shared/test/server/server_api_test.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.
 
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:devtools_shared/devtools_shared.dart';
@@ -150,6 +151,30 @@
     expect(fakeManager.receivedConfiguration, configuration);
     expect(fakeManager.receivedTarget, target);
   });
+
+  test('handle apiGetDtdUri', () async {
+    const dtdUri = 'ws://dtd:uri';
+    final request = Request(
+      'get',
+      Uri(
+        scheme: 'https',
+        host: 'localhost',
+        path: DtdApi.apiGetDtdUri,
+      ),
+    );
+    final fakeManager = FakeDeeplinkManager();
+    final response = await ServerApi.handle(
+      request,
+      extensionsManager: ExtensionsManager(buildDir: '/'),
+      deeplinkManager: fakeManager,
+      dtdUri: dtdUri,
+    );
+    expect(response.statusCode, HttpStatus.ok);
+    expect(
+      await response.readAsString(),
+      jsonEncode({DtdApi.uriPropertyName: dtdUri}),
+    );
+  });
 }
 
 class FakeDeeplinkManager extends DeeplinkManager {
diff --git a/packages/devtools_test/lib/src/helpers/actions.dart b/packages/devtools_test/lib/src/helpers/actions.dart
index 2b32853..0da7145 100644
--- a/packages/devtools_test/lib/src/helpers/actions.dart
+++ b/packages/devtools_test/lib/src/helpers/actions.dart
@@ -122,11 +122,11 @@
 }
 
 /// Scrolls to the end of the first [Scrollable] descendant of the [T] widget.
-/// 
+///
 /// For example, if you have some widget in the tree 'Foo' that contains a
 /// [Scrollbar] somewhere in its descendants, calling
 /// `scrollToEnd<Foo>(controller)` would perform the following steps:
-/// 
+///
 /// 1) find the [Scrollbar] widget descending from [Foo].
 /// 2) access the [Scrollbar] widget's [ScrollController].
 /// 3) scroll the scrollable attached to the [ScrollController] to the end of
diff --git a/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart b/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart
index a6920a1..8573307 100644
--- a/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart
+++ b/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart
@@ -26,14 +26,14 @@
   TestApp testApp,
 ) async {
   await pumpDevTools(tester);
-  expect(find.byType(ConnectDialog), findsOneWidget);
+  expect(find.byType(ConnectInput), findsOneWidget);
   expect(find.byType(ConnectedAppSummary), findsNothing);
   expect(find.text('No client connection'), findsOneWidget);
   _verifyFooterColor(tester, null);
 
   logStatus('verify that we can connect to an app');
   await connectToTestApp(tester, testApp);
-  expect(find.byType(ConnectDialog), findsNothing);
+  expect(find.byType(ConnectInput), findsNothing);
   expect(find.byType(ConnectedAppSummary), findsOneWidget);
   expect(find.text('No client connection'), findsNothing);
   _verifyFooterColor(tester, darkColorScheme.primary);
@@ -123,8 +123,7 @@
 
   factory TestApp.fromEnvironment() {
     const testArgs = String.fromEnvironment('test_args');
-    final Map<String, Object> argsMap =
-        jsonDecode(testArgs).cast<String, Object>();
+    final argsMap = (jsonDecode(testArgs) as Map).cast<String, Object>();
     return TestApp.parse(argsMap);
   }
 
diff --git a/packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart b/packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart
index dc2e83b..38962ea 100644
--- a/packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart
+++ b/packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart
@@ -51,8 +51,8 @@
     'timeOriginMicros': 47377796685,
     'timeExtentMicros': 3000,
     'pid': 54321,
-    'functions': [],
-    'samples': [],
+    'functions': <Object?>[],
+    'samples': <Object?>[],
   })!;
 
   CpuSamples cpuSamples;
@@ -416,10 +416,14 @@
   @override
   Future<HttpProfile> getHttpProfileWrapper(
     String isolateId, {
-    int? updatedSince,
+    DateTime? updatedSince,
   }) {
     return Future.value(
-      _httpProfile ?? HttpProfile(requests: [], timestamp: 0),
+      _httpProfile ??
+          HttpProfile(
+            requests: [],
+            timestamp: DateTime.fromMicrosecondsSinceEpoch(0),
+          ),
     );
   }
 
@@ -430,7 +434,10 @@
   }
 
   void restoreFakeHttpProfileRequests() {
-    _httpProfile = HttpProfile(requests: _startingRequests, timestamp: 0);
+    _httpProfile = HttpProfile(
+      requests: _startingRequests,
+      timestamp: DateTime.fromMicrosecondsSinceEpoch(0),
+    );
   }
 
   @override
diff --git a/packages/devtools_test/lib/src/mocks/generated.dart b/packages/devtools_test/lib/src/mocks/generated.dart
index 9db0eed..3da6816 100644
--- a/packages/devtools_test/lib/src/mocks/generated.dart
+++ b/packages/devtools_test/lib/src/mocks/generated.dart
@@ -50,5 +50,6 @@
   MockSpec<IsolateState>(),
   MockSpec<Obj>(),
   MockSpec<VM>(),
+  MockSpec<VsCodeApi>(),
 ])
 void main() {}
diff --git a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
index b4c3728..ccd706c 100644
--- a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
+++ b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
@@ -219,23 +219,38 @@
       .thenReturn(ImmediateValueNotifier(extensions));
 
   final stubEnabledStates = <String, ValueNotifier<ExtensionEnabledState>>{};
+
+  void computeVisibleExtensions() {
+    final visible = <DevToolsExtensionConfig>[];
+    for (final e in extensions) {
+      final state = stubEnabledStates[e.name.toLowerCase()]!.value;
+      if (state != ExtensionEnabledState.disabled) {
+        visible.add(e);
+      }
+    }
+    when(mockExtensionService.visibleExtensions)
+        .thenReturn(ValueNotifier(visible));
+  }
+
   for (final e in extensions) {
     stubEnabledStates[e.displayName] =
         ValueNotifier<ExtensionEnabledState>(ExtensionEnabledState.none);
     when(mockExtensionService.enabledStateListenable(e.name))
-        .thenReturn(stubEnabledStates[e.name.toLowerCase()]!);
+        .thenReturn(stubEnabledStates[e.displayName]!);
     when(mockExtensionService.enabledStateListenable(e.name.toLowerCase()))
-        .thenReturn(stubEnabledStates[e.name.toLowerCase()]!);
+        .thenReturn(stubEnabledStates[e.displayName]!);
     when(mockExtensionService.setExtensionEnabledState(e, enable: true))
         .thenAnswer((_) async {
-      stubEnabledStates[e.name.toLowerCase()]!.value =
-          ExtensionEnabledState.enabled;
+      stubEnabledStates[e.displayName]!.value = ExtensionEnabledState.enabled;
+      computeVisibleExtensions();
     });
     when(mockExtensionService.setExtensionEnabledState(e, enable: false))
         .thenAnswer((_) async {
       stubEnabledStates[e.name.toLowerCase()]!.value =
           ExtensionEnabledState.disabled;
+      computeVisibleExtensions();
     });
   }
+  computeVisibleExtensions();
   return mockExtensionService;
 }
diff --git a/packages/devtools_test/pubspec.yaml b/packages/devtools_test/pubspec.yaml
index 87a57db..67e2a57 100644
--- a/packages/devtools_test/pubspec.yaml
+++ b/packages/devtools_test/pubspec.yaml
@@ -7,7 +7,7 @@
 # When publishing new versions of this package be sure to publish a new version
 # of package:devtools as well. package:devtools contains a compiled snapshot of
 # this package.
-version: 2.31.1
+version: 2.33.0-dev.3
 
 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_test
 
@@ -19,7 +19,7 @@
   async: ^2.0.0
   collection: ^1.15.0
   devtools_shared: ^6.0.1
-  devtools_app: 2.31.1
+  devtools_app: 2.33.0-dev.3
   devtools_app_shared:
     path: ../devtools_app_shared
   flutter:
@@ -32,8 +32,7 @@
   mockito: ^5.4.1
   path: ^1.8.0
   provider: ^6.0.2
-  vm_service: ^13.0.0
-  vm_snapshot_analysis: ^0.7.1
+  vm_service: ^14.0.0
   webkit_inspection_protocol: '>=0.5.0 <2.0.0'
 
 dependency_overrides:
diff --git a/third_party/packages/ansi_up/analysis_options.yaml b/third_party/packages/ansi_up/analysis_options.yaml
new file mode 100644
index 0000000..572dd23
--- /dev/null
+++ b/third_party/packages/ansi_up/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:lints/recommended.yaml
diff --git a/third_party/packages/ansi_up/lib/ansi_up.dart b/third_party/packages/ansi_up/lib/ansi_up.dart
index 10d812e..a6be41f 100644
--- a/third_party/packages/ansi_up/lib/ansi_up.dart
+++ b/third_party/packages/ansi_up/lib/ansi_up.dart
@@ -2,8 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// ignore_for_file: constant_identifier_names
+
 /// ansi_up is an library that parses text containing ANSI color escape
 /// codes.
+library;
 
 class AnsiUp {
   AnsiUp()
@@ -372,7 +375,7 @@
     this.url = '',
   });
 
-  factory StyledText.from(_TextWithAttr fragment) {
+  factory StyledText._from(_TextWithAttr fragment) {
     return StyledText(
       fragment.text,
       fgColor: fragment.fg?.rgb?.toList(),
@@ -420,11 +423,10 @@
           (decoration == null) ? 'line-through' : '$decoration line-through';
     }
 
-    final _bgColor = bgColor;
-    final _fgColor = fgColor;
     return <String>[
-      if (_bgColor != null) 'background-color: ${_colorToCss(_bgColor)}',
-      if (_fgColor != null) 'color: ${_colorToCss(_fgColor)}',
+      if (bgColor case final bgColor?)
+        'background-color: ${_colorToCss(bgColor)}',
+      if (fgColor case final fgColor?) 'color: ${_colorToCss(fgColor)}',
       if (bold) 'font-weight: bold',
       if (italic) 'font-style: italic',
       if (decoration != null) 'text-decoration: $decoration',
@@ -455,7 +457,7 @@
     }
 
     if (packet.kind == PacketKind.Text) {
-      yield StyledText.from(ansiUp._withState(packet));
+      yield StyledText._from(ansiUp._withState(packet));
     } else if (packet.kind == PacketKind.SGR) {
       ansiUp._processAnsi(packet);
     } else if (packet.kind == PacketKind.OSCURL) {
diff --git a/third_party/packages/ansi_up/pubspec.yaml b/third_party/packages/ansi_up/pubspec.yaml
index ddb5ae0..038689a 100644
--- a/third_party/packages/ansi_up/pubspec.yaml
+++ b/third_party/packages/ansi_up/pubspec.yaml
@@ -4,4 +4,7 @@
 version: 1.0.0
 
 environment:
-  sdk: '>=2.12.0 <4.0.0'
+  sdk: ^3.0.0
+
+dev_dependencies:
+  lints: ^3.0.0
diff --git a/third_party/packages/codicon/analysis_options.yaml b/third_party/packages/codicon/analysis_options.yaml
new file mode 100644
index 0000000..f9b3034
--- /dev/null
+++ b/third_party/packages/codicon/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flutter_lints/flutter.yaml
diff --git a/third_party/packages/codicon/lib/codicon.dart b/third_party/packages/codicon/lib/codicon.dart
index 0851be5..946bba9 100644
--- a/third_party/packages/codicon/lib/codicon.dart
+++ b/third_party/packages/codicon/lib/codicon.dart
@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 
 // The members for this class were generated from the data at
-// https://github.com/microsoft/vscode-codicons/blob/master/src/template/mapping.json.
+// https://github.com/microsoft/vscode-codicons/blob/main/src/template/mapping.json.
 
 class Codicons {
   static const IconData add = IconData(60000, fontFamily: 'Codicon');
diff --git a/third_party/packages/codicon/pubspec.yaml b/third_party/packages/codicon/pubspec.yaml
index 205007a..872b855 100644
--- a/third_party/packages/codicon/pubspec.yaml
+++ b/third_party/packages/codicon/pubspec.yaml
@@ -4,12 +4,15 @@
 version: 1.0.0
 
 environment:
-  sdk: '>=2.12.0 <4.0.0'
+  sdk: ^3.0.0
 
 dependencies:
   flutter:
     sdk: flutter
 
+dev_dependencies:
+  flutter_lints: ^3.0.1
+
 flutter:
   fonts:
     - family: Codicon
diff --git a/third_party/packages/perfetto_ui_compiled/pubspec.yaml b/third_party/packages/perfetto_ui_compiled/pubspec.yaml
index 70aab97..f240f96 100644
--- a/third_party/packages/perfetto_ui_compiled/pubspec.yaml
+++ b/third_party/packages/perfetto_ui_compiled/pubspec.yaml
@@ -4,4 +4,4 @@
 version: 0.0.1
 
 environment:
-  sdk: '>=2.12.0 <4.0.0'
+  sdk: ^3.0.0
diff --git a/third_party/packages/widget_icons/analysis_options.yaml b/third_party/packages/widget_icons/analysis_options.yaml
index e69de29..f9b3034 100644
--- a/third_party/packages/widget_icons/analysis_options.yaml
+++ b/third_party/packages/widget_icons/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flutter_lints/flutter.yaml
diff --git a/third_party/packages/widget_icons/example/.gitignore b/third_party/packages/widget_icons/example/.gitignore
index 0fa6b67..3af1cca 100644
--- a/third_party/packages/widget_icons/example/.gitignore
+++ b/third_party/packages/widget_icons/example/.gitignore
@@ -31,6 +31,14 @@
 .pub/
 /build/
 
+# Platform related
+android/**/*
+ios/**/*
+linux/**/*
+macos/**/*
+web/**/*
+windows/**/*
+
 # Web related
 lib/generated_plugin_registrant.dart
 
diff --git a/third_party/packages/widget_icons/example/analysis_options.yaml b/third_party/packages/widget_icons/example/analysis_options.yaml
index 61b6c4d..f9b3034 100644
--- a/third_party/packages/widget_icons/example/analysis_options.yaml
+++ b/third_party/packages/widget_icons/example/analysis_options.yaml
@@ -1,29 +1 @@
-# This file configures the analyzer, which statically analyzes Dart code to
-# check for errors, warnings, and lints.
-#
-# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
-# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
-# invoked from the command line by running `flutter analyze`.
-
-# The following line activates a set of recommended lints for Flutter apps,
-# packages, and plugins designed to encourage good coding practices.
 include: package:flutter_lints/flutter.yaml
-
-linter:
-  # The lint rules applied to this project can be customized in the
-  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
-  # included above or to enable additional rules. A list of all available lints
-  # and their documentation is published at
-  # https://dart-lang.github.io/linter/lints/index.html.
-  #
-  # Instead of disabling a lint rule for the entire project in the
-  # section below, it can also be suppressed for a single line of code
-  # or a specific dart file by using the `// ignore: name_of_lint` and
-  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
-  # producing the lint.
-  rules:
-    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
-    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/third_party/packages/widget_icons/example/android/.gitignore b/third_party/packages/widget_icons/example/android/.gitignore
deleted file mode 100644
index 6f56801..0000000
--- a/third_party/packages/widget_icons/example/android/.gitignore
+++ /dev/null
@@ -1,13 +0,0 @@
-gradle-wrapper.jar
-/.gradle
-/captures/
-/gradlew
-/gradlew.bat
-/local.properties
-GeneratedPluginRegistrant.java
-
-# Remember to never publicly share your keystore.
-# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
-key.properties
-**/*.keystore
-**/*.jks
diff --git a/third_party/packages/widget_icons/example/android/app/build.gradle b/third_party/packages/widget_icons/example/android/app/build.gradle
deleted file mode 100644
index 56bfa9b..0000000
--- a/third_party/packages/widget_icons/example/android/app/build.gradle
+++ /dev/null
@@ -1,68 +0,0 @@
-def localProperties = new Properties()
-def localPropertiesFile = rootProject.file('local.properties')
-if (localPropertiesFile.exists()) {
-    localPropertiesFile.withReader('UTF-8') { reader ->
-        localProperties.load(reader)
-    }
-}
-
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
-    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
-def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
-if (flutterVersionCode == null) {
-    flutterVersionCode = '1'
-}
-
-def flutterVersionName = localProperties.getProperty('flutter.versionName')
-if (flutterVersionName == null) {
-    flutterVersionName = '1.0'
-}
-
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
-
-android {
-    compileSdkVersion 30
-
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
-
-    kotlinOptions {
-        jvmTarget = '1.8'
-    }
-
-    sourceSets {
-        main.java.srcDirs += 'src/main/kotlin'
-    }
-
-    defaultConfig {
-        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
-        applicationId "com.example.example"
-        minSdkVersion 16
-        targetSdkVersion 30
-        versionCode flutterVersionCode.toInteger()
-        versionName flutterVersionName
-    }
-
-    buildTypes {
-        release {
-            // TODO: Add your own signing config for the release build.
-            // Signing with the debug keys for now, so `flutter run --release` works.
-            signingConfig signingConfigs.debug
-        }
-    }
-}
-
-flutter {
-    source '../..'
-}
-
-dependencies {
-    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-}
diff --git a/third_party/packages/widget_icons/example/android/app/src/debug/AndroidManifest.xml b/third_party/packages/widget_icons/example/android/app/src/debug/AndroidManifest.xml
deleted file mode 100644
index c208884..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.example">
-    <!-- Flutter needs it to communicate with the running application
-         to allow setting breakpoints, to provide hot reload, etc.
-    -->
-    <uses-permission android:name="android.permission.INTERNET"/>
-</manifest>
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/AndroidManifest.xml b/third_party/packages/widget_icons/example/android/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 6f325b4..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.example">
-   <application
-        android:label="example"
-        android:icon="@mipmap/ic_launcher">
-        <activity
-            android:name=".MainActivity"
-            android:launchMode="singleTop"
-            android:theme="@style/LaunchTheme"
-            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
-            android:hardwareAccelerated="true"
-            android:windowSoftInputMode="adjustResize">
-            <!-- Specifies an Android theme to apply to this Activity as soon as
-                 the Android process has started. This theme is visible to the user
-                 while the Flutter UI initializes. After that, this theme continues
-                 to determine the Window background behind the Flutter UI. -->
-            <meta-data
-              android:name="io.flutter.embedding.android.NormalTheme"
-              android:resource="@style/NormalTheme"
-              />
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.LAUNCHER"/>
-            </intent-filter>
-        </activity>
-        <!-- Don't delete the meta-data below.
-             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
-        <meta-data
-            android:name="flutterEmbedding"
-            android:value="2" />
-    </application>
-</manifest>
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/third_party/packages/widget_icons/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
deleted file mode 100644
index e793a00..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.example.example
-
-import io.flutter.embedding.android.FlutterActivity
-
-class MainActivity: FlutterActivity() {
-}
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/drawable-v21/launch_background.xml b/third_party/packages/widget_icons/example/android/app/src/main/res/drawable-v21/launch_background.xml
deleted file mode 100644
index f74085f..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/drawable-v21/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Modify this file to customize your launch splash screen -->
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:drawable="?android:colorBackground" />
-
-    <!-- You can insert your own image assets here -->
-    <!-- <item>
-        <bitmap
-            android:gravity="center"
-            android:src="@mipmap/launch_image" />
-    </item> -->
-</layer-list>
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/drawable/launch_background.xml b/third_party/packages/widget_icons/example/android/app/src/main/res/drawable/launch_background.xml
deleted file mode 100644
index 304732f..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/drawable/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Modify this file to customize your launch splash screen -->
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:drawable="@android:color/white" />
-
-    <!-- You can insert your own image assets here -->
-    <!-- <item>
-        <bitmap
-            android:gravity="center"
-            android:src="@mipmap/launch_image" />
-    </item> -->
-</layer-list>
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index db77bb4..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 17987b7..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 09d4391..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index d5f1c8d..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 4d6372e..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/values-night/styles.xml b/third_party/packages/widget_icons/example/android/app/src/main/res/values-night/styles.xml
deleted file mode 100644
index 449a9f9..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
-    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
-        <!-- Show a splash screen on the activity. Automatically removed when
-             Flutter draws its first frame -->
-        <item name="android:windowBackground">@drawable/launch_background</item>
-    </style>
-    <!-- Theme applied to the Android Window as soon as the process has started.
-         This theme determines the color of the Android Window while your
-         Flutter UI initializes, as well as behind your Flutter UI while its
-         running.
-         
-         This Theme is only used starting with V2 of Flutter's Android embedding. -->
-    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
-        <item name="android:windowBackground">?android:colorBackground</item>
-    </style>
-</resources>
diff --git a/third_party/packages/widget_icons/example/android/app/src/main/res/values/styles.xml b/third_party/packages/widget_icons/example/android/app/src/main/res/values/styles.xml
deleted file mode 100644
index d74aa35..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
-    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
-        <!-- Show a splash screen on the activity. Automatically removed when
-             Flutter draws its first frame -->
-        <item name="android:windowBackground">@drawable/launch_background</item>
-    </style>
-    <!-- Theme applied to the Android Window as soon as the process has started.
-         This theme determines the color of the Android Window while your
-         Flutter UI initializes, as well as behind your Flutter UI while its
-         running.
-         
-         This Theme is only used starting with V2 of Flutter's Android embedding. -->
-    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
-        <item name="android:windowBackground">?android:colorBackground</item>
-    </style>
-</resources>
diff --git a/third_party/packages/widget_icons/example/android/app/src/profile/AndroidManifest.xml b/third_party/packages/widget_icons/example/android/app/src/profile/AndroidManifest.xml
deleted file mode 100644
index c208884..0000000
--- a/third_party/packages/widget_icons/example/android/app/src/profile/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.example.example">
-    <!-- Flutter needs it to communicate with the running application
-         to allow setting breakpoints, to provide hot reload, etc.
-    -->
-    <uses-permission android:name="android.permission.INTERNET"/>
-</manifest>
diff --git a/third_party/packages/widget_icons/example/android/build.gradle b/third_party/packages/widget_icons/example/android/build.gradle
deleted file mode 100644
index ed45c65..0000000
--- a/third_party/packages/widget_icons/example/android/build.gradle
+++ /dev/null
@@ -1,29 +0,0 @@
-buildscript {
-    ext.kotlin_version = '1.3.50'
-    repositories {
-        google()
-        mavenCentral()
-    }
-
-    dependencies {
-        classpath 'com.android.tools.build:gradle:4.1.0'
-        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
-    }
-}
-
-allprojects {
-    repositories {
-        google()
-        mavenCentral()
-    }
-}
-
-rootProject.buildDir = '../build'
-subprojects {
-    project.buildDir = "${rootProject.buildDir}/${project.name}"
-    project.evaluationDependsOn(':app')
-}
-
-task clean(type: Delete) {
-    delete rootProject.buildDir
-}
diff --git a/third_party/packages/widget_icons/example/android/gradle.properties b/third_party/packages/widget_icons/example/android/gradle.properties
deleted file mode 100644
index 94adc3a..0000000
--- a/third_party/packages/widget_icons/example/android/gradle.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-org.gradle.jvmargs=-Xmx1536M
-android.useAndroidX=true
-android.enableJetifier=true
diff --git a/third_party/packages/widget_icons/example/android/gradle/wrapper/gradle-wrapper.properties b/third_party/packages/widget_icons/example/android/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index bc6a58a..0000000
--- a/third_party/packages/widget_icons/example/android/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Fri Jun 23 08:50:38 CEST 2017
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/third_party/packages/widget_icons/example/android/settings.gradle b/third_party/packages/widget_icons/example/android/settings.gradle
deleted file mode 100644
index 44e62bc..0000000
--- a/third_party/packages/widget_icons/example/android/settings.gradle
+++ /dev/null
@@ -1,11 +0,0 @@
-include ':app'
-
-def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
-def properties = new Properties()
-
-assert localPropertiesFile.exists()
-localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
-
-def flutterSdkPath = properties.getProperty("flutter.sdk")
-assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
-apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/third_party/packages/widget_icons/example/ios/.gitignore b/third_party/packages/widget_icons/example/ios/.gitignore
deleted file mode 100644
index 151026b..0000000
--- a/third_party/packages/widget_icons/example/ios/.gitignore
+++ /dev/null
@@ -1,33 +0,0 @@
-*.mode1v3
-*.mode2v3
-*.moved-aside
-*.pbxuser
-*.perspectivev3
-**/*sync/
-.sconsign.dblite
-.tags*
-**/.vagrant/
-**/DerivedData/
-Icon?
-**/Pods/
-**/.symlinks/
-profile
-xcuserdata
-**/.generated/
-Flutter/App.framework
-Flutter/Flutter.framework
-Flutter/Flutter.podspec
-Flutter/Generated.xcconfig
-Flutter/ephemeral/
-Flutter/app.flx
-Flutter/app.zip
-Flutter/flutter_assets/
-Flutter/flutter_export_environment.sh
-ServiceDefinitions.json
-Runner/GeneratedPluginRegistrant.*
-
-# Exceptions to above rules.
-!default.mode1v3
-!default.mode2v3
-!default.pbxuser
-!default.perspectivev3
diff --git a/third_party/packages/widget_icons/example/ios/Flutter/AppFrameworkInfo.plist b/third_party/packages/widget_icons/example/ios/Flutter/AppFrameworkInfo.plist
deleted file mode 100644
index 8d4492f..0000000
--- a/third_party/packages/widget_icons/example/ios/Flutter/AppFrameworkInfo.plist
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-  <key>CFBundleDevelopmentRegion</key>
-  <string>en</string>
-  <key>CFBundleExecutable</key>
-  <string>App</string>
-  <key>CFBundleIdentifier</key>
-  <string>io.flutter.flutter.app</string>
-  <key>CFBundleInfoDictionaryVersion</key>
-  <string>6.0</string>
-  <key>CFBundleName</key>
-  <string>App</string>
-  <key>CFBundlePackageType</key>
-  <string>FMWK</string>
-  <key>CFBundleShortVersionString</key>
-  <string>1.0</string>
-  <key>CFBundleSignature</key>
-  <string>????</string>
-  <key>CFBundleVersion</key>
-  <string>1.0</string>
-  <key>MinimumOSVersion</key>
-  <string>9.0</string>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/ios/Flutter/Debug.xcconfig b/third_party/packages/widget_icons/example/ios/Flutter/Debug.xcconfig
deleted file mode 100644
index 592ceee..0000000
--- a/third_party/packages/widget_icons/example/ios/Flutter/Debug.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/third_party/packages/widget_icons/example/ios/Flutter/Release.xcconfig b/third_party/packages/widget_icons/example/ios/Flutter/Release.xcconfig
deleted file mode 100644
index 592ceee..0000000
--- a/third_party/packages/widget_icons/example/ios/Flutter/Release.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"
diff --git a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.pbxproj b/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.pbxproj
deleted file mode 100644
index 6edd238..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,481 +0,0 @@
-// !$*UTF8*$!
-{
-	archiveVersion = 1;
-	classes = {
-	};
-	objectVersion = 50;
-	objects = {
-
-/* Begin PBXBuildFile section */
-		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
-		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
-		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
-		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
-		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
-		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXCopyFilesBuildPhase section */
-		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
-			isa = PBXCopyFilesBuildPhase;
-			buildActionMask = 2147483647;
-			dstPath = "";
-			dstSubfolderSpec = 10;
-			files = (
-			);
-			name = "Embed Frameworks";
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
-		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
-		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
-		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
-		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
-		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
-		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
-		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
-		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
-		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
-		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
-		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
-		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
-		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
-		97C146EB1CF9000F007C117D /* Frameworks */ = {
-			isa = PBXFrameworksBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
-		9740EEB11CF90186004384FC /* Flutter */ = {
-			isa = PBXGroup;
-			children = (
-				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
-				9740EEB21CF90195004384FC /* Debug.xcconfig */,
-				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
-				9740EEB31CF90195004384FC /* Generated.xcconfig */,
-			);
-			name = Flutter;
-			sourceTree = "<group>";
-		};
-		97C146E51CF9000F007C117D = {
-			isa = PBXGroup;
-			children = (
-				9740EEB11CF90186004384FC /* Flutter */,
-				97C146F01CF9000F007C117D /* Runner */,
-				97C146EF1CF9000F007C117D /* Products */,
-			);
-			sourceTree = "<group>";
-		};
-		97C146EF1CF9000F007C117D /* Products */ = {
-			isa = PBXGroup;
-			children = (
-				97C146EE1CF9000F007C117D /* Runner.app */,
-			);
-			name = Products;
-			sourceTree = "<group>";
-		};
-		97C146F01CF9000F007C117D /* Runner */ = {
-			isa = PBXGroup;
-			children = (
-				97C146FA1CF9000F007C117D /* Main.storyboard */,
-				97C146FD1CF9000F007C117D /* Assets.xcassets */,
-				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
-				97C147021CF9000F007C117D /* Info.plist */,
-				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
-				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
-				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
-				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
-			);
-			path = Runner;
-			sourceTree = "<group>";
-		};
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
-		97C146ED1CF9000F007C117D /* Runner */ = {
-			isa = PBXNativeTarget;
-			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
-			buildPhases = (
-				9740EEB61CF901F6004384FC /* Run Script */,
-				97C146EA1CF9000F007C117D /* Sources */,
-				97C146EB1CF9000F007C117D /* Frameworks */,
-				97C146EC1CF9000F007C117D /* Resources */,
-				9705A1C41CF9048500538489 /* Embed Frameworks */,
-				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
-			);
-			buildRules = (
-			);
-			dependencies = (
-			);
-			name = Runner;
-			productName = Runner;
-			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
-			productType = "com.apple.product-type.application";
-		};
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
-		97C146E61CF9000F007C117D /* Project object */ = {
-			isa = PBXProject;
-			attributes = {
-				LastUpgradeCheck = 1300;
-				ORGANIZATIONNAME = "";
-				TargetAttributes = {
-					97C146ED1CF9000F007C117D = {
-						CreatedOnToolsVersion = 7.3.1;
-						LastSwiftMigration = 1100;
-					};
-				};
-			};
-			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
-			compatibilityVersion = "Xcode 9.3";
-			developmentRegion = en;
-			hasScannedForEncodings = 0;
-			knownRegions = (
-				en,
-				Base,
-			);
-			mainGroup = 97C146E51CF9000F007C117D;
-			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
-			projectDirPath = "";
-			projectRoot = "";
-			targets = (
-				97C146ED1CF9000F007C117D /* Runner */,
-			);
-		};
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
-		97C146EC1CF9000F007C117D /* Resources */ = {
-			isa = PBXResourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
-				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
-				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
-				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
-		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
-			isa = PBXShellScriptBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputPaths = (
-			);
-			name = "Thin Binary";
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
-		};
-		9740EEB61CF901F6004384FC /* Run Script */ = {
-			isa = PBXShellScriptBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputPaths = (
-			);
-			name = "Run Script";
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
-		};
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
-		97C146EA1CF9000F007C117D /* Sources */ = {
-			isa = PBXSourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
-				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXVariantGroup section */
-		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
-			isa = PBXVariantGroup;
-			children = (
-				97C146FB1CF9000F007C117D /* Base */,
-			);
-			name = Main.storyboard;
-			sourceTree = "<group>";
-		};
-		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
-			isa = PBXVariantGroup;
-			children = (
-				97C147001CF9000F007C117D /* Base */,
-			);
-			name = LaunchScreen.storyboard;
-			sourceTree = "<group>";
-		};
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
-		249021D3217E4FDB00AE95B9 /* Profile */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_COMMA = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
-				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
-				CLANG_WARN_STRICT_PROTOTYPES = YES;
-				CLANG_WARN_SUSPICIOUS_MOVE = YES;
-				CLANG_WARN_UNREACHABLE_CODE = YES;
-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
-				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				COPY_PHASE_STRIP = NO;
-				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
-				ENABLE_NS_ASSERTIONS = NO;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu99;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNDECLARED_SELECTOR = YES;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				MTL_ENABLE_DEBUG_INFO = NO;
-				SDKROOT = iphoneos;
-				SUPPORTED_PLATFORMS = iphoneos;
-				TARGETED_DEVICE_FAMILY = "1,2";
-				VALIDATE_PRODUCT = YES;
-			};
-			name = Profile;
-		};
-		249021D4217E4FDB00AE95B9 /* Profile */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
-			buildSettings = {
-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				CLANG_ENABLE_MODULES = YES;
-				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
-				ENABLE_BITCODE = NO;
-				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
-				PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
-				SWIFT_VERSION = 5.0;
-				VERSIONING_SYSTEM = "apple-generic";
-			};
-			name = Profile;
-		};
-		97C147031CF9000F007C117D /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_COMMA = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
-				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
-				CLANG_WARN_STRICT_PROTOTYPES = YES;
-				CLANG_WARN_SUSPICIOUS_MOVE = YES;
-				CLANG_WARN_UNREACHABLE_CODE = YES;
-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
-				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				COPY_PHASE_STRIP = NO;
-				DEBUG_INFORMATION_FORMAT = dwarf;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				ENABLE_TESTABILITY = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu99;
-				GCC_DYNAMIC_NO_PIC = NO;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_OPTIMIZATION_LEVEL = 0;
-				GCC_PREPROCESSOR_DEFINITIONS = (
-					"DEBUG=1",
-					"$(inherited)",
-				);
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNDECLARED_SELECTOR = YES;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				MTL_ENABLE_DEBUG_INFO = YES;
-				ONLY_ACTIVE_ARCH = YES;
-				SDKROOT = iphoneos;
-				TARGETED_DEVICE_FAMILY = "1,2";
-			};
-			name = Debug;
-		};
-		97C147041CF9000F007C117D /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_COMMA = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
-				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
-				CLANG_WARN_STRICT_PROTOTYPES = YES;
-				CLANG_WARN_SUSPICIOUS_MOVE = YES;
-				CLANG_WARN_UNREACHABLE_CODE = YES;
-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
-				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
-				COPY_PHASE_STRIP = NO;
-				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
-				ENABLE_NS_ASSERTIONS = NO;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu99;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNDECLARED_SELECTOR = YES;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 9.0;
-				MTL_ENABLE_DEBUG_INFO = NO;
-				SDKROOT = iphoneos;
-				SUPPORTED_PLATFORMS = iphoneos;
-				SWIFT_COMPILATION_MODE = wholemodule;
-				SWIFT_OPTIMIZATION_LEVEL = "-O";
-				TARGETED_DEVICE_FAMILY = "1,2";
-				VALIDATE_PRODUCT = YES;
-			};
-			name = Release;
-		};
-		97C147061CF9000F007C117D /* Debug */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
-			buildSettings = {
-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				CLANG_ENABLE_MODULES = YES;
-				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
-				ENABLE_BITCODE = NO;
-				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
-				PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
-				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
-				SWIFT_VERSION = 5.0;
-				VERSIONING_SYSTEM = "apple-generic";
-			};
-			name = Debug;
-		};
-		97C147071CF9000F007C117D /* Release */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
-			buildSettings = {
-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				CLANG_ENABLE_MODULES = YES;
-				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
-				ENABLE_BITCODE = NO;
-				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/Frameworks",
-				);
-				PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
-				SWIFT_VERSION = 5.0;
-				VERSIONING_SYSTEM = "apple-generic";
-			};
-			name = Release;
-		};
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
-		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				97C147031CF9000F007C117D /* Debug */,
-				97C147041CF9000F007C117D /* Release */,
-				249021D3217E4FDB00AE95B9 /* Profile */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				97C147061CF9000F007C117D /* Debug */,
-				97C147071CF9000F007C117D /* Release */,
-				249021D4217E4FDB00AE95B9 /* Profile */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-/* End XCConfigurationList section */
-	};
-	rootObject = 97C146E61CF9000F007C117D /* Project object */;
-}
diff --git a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Workspace
-   version = "1.0">
-   <FileRef
-      location = "self:">
-   </FileRef>
-</Workspace>
diff --git a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>IDEDidComputeMac32BitWarning</key>
-	<true/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>PreviewsEnabled</key>
-	<false/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100644
index c87d15a..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,87 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Scheme
-   LastUpgradeVersion = "1300"
-   version = "1.3">
-   <BuildAction
-      parallelizeBuildables = "YES"
-      buildImplicitDependencies = "YES">
-      <BuildActionEntries>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
-               BuildableName = "Runner.app"
-               BlueprintName = "Runner"
-               ReferencedContainer = "container:Runner.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-      </BuildActionEntries>
-   </BuildAction>
-   <TestAction
-      buildConfiguration = "Debug"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      shouldUseLaunchSchemeArgsEnv = "YES">
-      <MacroExpansion>
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
-            BuildableName = "Runner.app"
-            BlueprintName = "Runner"
-            ReferencedContainer = "container:Runner.xcodeproj">
-         </BuildableReference>
-      </MacroExpansion>
-      <Testables>
-      </Testables>
-   </TestAction>
-   <LaunchAction
-      buildConfiguration = "Debug"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      launchStyle = "0"
-      useCustomWorkingDirectory = "NO"
-      ignoresPersistentStateOnLaunch = "NO"
-      debugDocumentVersioning = "YES"
-      debugServiceExtension = "internal"
-      allowLocationSimulation = "YES">
-      <BuildableProductRunnable
-         runnableDebuggingMode = "0">
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
-            BuildableName = "Runner.app"
-            BlueprintName = "Runner"
-            ReferencedContainer = "container:Runner.xcodeproj">
-         </BuildableReference>
-      </BuildableProductRunnable>
-   </LaunchAction>
-   <ProfileAction
-      buildConfiguration = "Profile"
-      shouldUseLaunchSchemeArgsEnv = "YES"
-      savedToolIdentifier = ""
-      useCustomWorkingDirectory = "NO"
-      debugDocumentVersioning = "YES">
-      <BuildableProductRunnable
-         runnableDebuggingMode = "0">
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
-            BuildableName = "Runner.app"
-            BlueprintName = "Runner"
-            ReferencedContainer = "container:Runner.xcodeproj">
-         </BuildableReference>
-      </BuildableProductRunnable>
-   </ProfileAction>
-   <AnalyzeAction
-      buildConfiguration = "Debug">
-   </AnalyzeAction>
-   <ArchiveAction
-      buildConfiguration = "Release"
-      revealArchiveInOrganizer = "YES">
-   </ArchiveAction>
-</Scheme>
diff --git a/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 1d526a1..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Workspace
-   version = "1.0">
-   <FileRef
-      location = "group:Runner.xcodeproj">
-   </FileRef>
-</Workspace>
diff --git a/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>IDEDidComputeMac32BitWarning</key>
-	<true/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
deleted file mode 100644
index f9b0d7c..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>PreviewsEnabled</key>
-	<false/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/ios/Runner/AppDelegate.swift b/third_party/packages/widget_icons/example/ios/Runner/AppDelegate.swift
deleted file mode 100644
index 70693e4..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/AppDelegate.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import UIKit
-import Flutter
-
-@UIApplicationMain
-@objc class AppDelegate: FlutterAppDelegate {
-  override func application(
-    _ application: UIApplication,
-    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
-  ) -> Bool {
-    GeneratedPluginRegistrant.register(with: self)
-    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
-  }
-}
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index d36b1fa..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,122 +0,0 @@
-{
-  "images" : [
-    {
-      "size" : "20x20",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-20x20@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "20x20",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-20x20@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-29x29@1x.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-29x29@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-29x29@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-40x40@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-40x40@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "size" : "60x60",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-60x60@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "60x60",
-      "idiom" : "iphone",
-      "filename" : "Icon-App-60x60@3x.png",
-      "scale" : "3x"
-    },
-    {
-      "size" : "20x20",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-20x20@1x.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "20x20",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-20x20@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-29x29@1x.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "29x29",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-29x29@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-40x40@1x.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "40x40",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-40x40@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "76x76",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-76x76@1x.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "76x76",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-76x76@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "83.5x83.5",
-      "idiom" : "ipad",
-      "filename" : "Icon-App-83.5x83.5@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "1024x1024",
-      "idiom" : "ios-marketing",
-      "filename" : "Icon-App-1024x1024@1x.png",
-      "scale" : "1x"
-    }
-  ],
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  }
-}
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
deleted file mode 100644
index dc9ada4..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
deleted file mode 100644
index 28c6bf0..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
deleted file mode 100644
index 2ccbfd9..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
deleted file mode 100644
index f091b6b..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100644
index 4cde121..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100644
index d0ef06e..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100644
index dcdc230..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100644
index 2ccbfd9..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100644
index c8f9ed8..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100644
index a6d6b86..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100644
index a6d6b86..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100644
index 75b2d16..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100644
index c4df70d..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100644
index 6a84f41..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100644
index d0e1f58..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
deleted file mode 100644
index 0bedcf2..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "images" : [
-    {
-      "idiom" : "universal",
-      "filename" : "LaunchImage.png",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "universal",
-      "filename" : "LaunchImage@2x.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "universal",
-      "filename" : "LaunchImage@3x.png",
-      "scale" : "3x"
-    }
-  ],
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  }
-}
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
deleted file mode 100644
index 9da19ea..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
deleted file mode 100644
index 9da19ea..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
deleted file mode 100644
index 9da19ea..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
deleted file mode 100644
index 89c2725..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Launch Screen Assets
-
-You can customize the launch screen with your own desired assets by replacing the image files in this directory.
-
-You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/third_party/packages/widget_icons/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index f2e259c..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
-    <dependencies>
-        <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
-    </dependencies>
-    <scenes>
-        <!--View Controller-->
-        <scene sceneID="EHf-IW-A2E">
-            <objects>
-                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
-                    <layoutGuides>
-                        <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
-                        <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
-                    </layoutGuides>
-                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <subviews>
-                            <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
-                            </imageView>
-                        </subviews>
-                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
-                        <constraints>
-                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
-                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
-                        </constraints>
-                    </view>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="53" y="375"/>
-        </scene>
-    </scenes>
-    <resources>
-        <image name="LaunchImage" width="168" height="185"/>
-    </resources>
-</document>
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Base.lproj/Main.storyboard b/third_party/packages/widget_icons/example/ios/Runner/Base.lproj/Main.storyboard
deleted file mode 100644
index f3c2851..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
-    <dependencies>
-        <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
-    </dependencies>
-    <scenes>
-        <!--Flutter View Controller-->
-        <scene sceneID="tne-QT-ifu">
-            <objects>
-                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
-                    <layoutGuides>
-                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
-                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
-                    </layoutGuides>
-                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
-                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
-                    </view>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
-            </objects>
-        </scene>
-    </scenes>
-</document>
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Info.plist b/third_party/packages/widget_icons/example/ios/Runner/Info.plist
deleted file mode 100644
index a060db6..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Info.plist
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>CFBundleDevelopmentRegion</key>
-	<string>$(DEVELOPMENT_LANGUAGE)</string>
-	<key>CFBundleExecutable</key>
-	<string>$(EXECUTABLE_NAME)</string>
-	<key>CFBundleIdentifier</key>
-	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
-	<key>CFBundleInfoDictionaryVersion</key>
-	<string>6.0</string>
-	<key>CFBundleName</key>
-	<string>example</string>
-	<key>CFBundlePackageType</key>
-	<string>APPL</string>
-	<key>CFBundleShortVersionString</key>
-	<string>$(FLUTTER_BUILD_NAME)</string>
-	<key>CFBundleSignature</key>
-	<string>????</string>
-	<key>CFBundleVersion</key>
-	<string>$(FLUTTER_BUILD_NUMBER)</string>
-	<key>LSRequiresIPhoneOS</key>
-	<true/>
-	<key>UILaunchStoryboardName</key>
-	<string>LaunchScreen</string>
-	<key>UIMainStoryboardFile</key>
-	<string>Main</string>
-	<key>UISupportedInterfaceOrientations</key>
-	<array>
-		<string>UIInterfaceOrientationPortrait</string>
-		<string>UIInterfaceOrientationLandscapeLeft</string>
-		<string>UIInterfaceOrientationLandscapeRight</string>
-	</array>
-	<key>UISupportedInterfaceOrientations~ipad</key>
-	<array>
-		<string>UIInterfaceOrientationPortrait</string>
-		<string>UIInterfaceOrientationPortraitUpsideDown</string>
-		<string>UIInterfaceOrientationLandscapeLeft</string>
-		<string>UIInterfaceOrientationLandscapeRight</string>
-	</array>
-	<key>UIViewControllerBasedStatusBarAppearance</key>
-	<false/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/ios/Runner/Runner-Bridging-Header.h b/third_party/packages/widget_icons/example/ios/Runner/Runner-Bridging-Header.h
deleted file mode 100644
index 308a2a5..0000000
--- a/third_party/packages/widget_icons/example/ios/Runner/Runner-Bridging-Header.h
+++ /dev/null
@@ -1 +0,0 @@
-#import "GeneratedPluginRegistrant.h"
diff --git a/third_party/packages/widget_icons/example/lib/main.dart b/third_party/packages/widget_icons/example/lib/main.dart
index 062fa83..d9a0d91 100644
--- a/third_party/packages/widget_icons/example/lib/main.dart
+++ b/third_party/packages/widget_icons/example/lib/main.dart
@@ -10,7 +10,7 @@
 }
 
 class HomePage extends StatelessWidget {
-  const HomePage({Key? key}) : super(key: key);
+  const HomePage({super.key});
 
   @override
   Widget build(BuildContext context) {
@@ -196,10 +196,10 @@
   final String name;
 
   const IconContainer({
-    Key? key,
+    super.key,
     required this.icon,
     required this.name,
-  }) : super(key: key);
+  });
 
   @override
   Widget build(BuildContext context) {
diff --git a/third_party/packages/widget_icons/example/macos/.gitignore b/third_party/packages/widget_icons/example/macos/.gitignore
deleted file mode 100644
index d2fd377..0000000
--- a/third_party/packages/widget_icons/example/macos/.gitignore
+++ /dev/null
@@ -1,6 +0,0 @@
-# Flutter-related
-**/Flutter/ephemeral/
-**/Pods/
-
-# Xcode-related
-**/xcuserdata/
diff --git a/third_party/packages/widget_icons/example/macos/Flutter/Flutter-Debug.xcconfig b/third_party/packages/widget_icons/example/macos/Flutter/Flutter-Debug.xcconfig
deleted file mode 100644
index c2efd0b..0000000
--- a/third_party/packages/widget_icons/example/macos/Flutter/Flutter-Debug.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/third_party/packages/widget_icons/example/macos/Flutter/Flutter-Release.xcconfig b/third_party/packages/widget_icons/example/macos/Flutter/Flutter-Release.xcconfig
deleted file mode 100644
index c2efd0b..0000000
--- a/third_party/packages/widget_icons/example/macos/Flutter/Flutter-Release.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/third_party/packages/widget_icons/example/macos/Flutter/GeneratedPluginRegistrant.swift b/third_party/packages/widget_icons/example/macos/Flutter/GeneratedPluginRegistrant.swift
deleted file mode 100644
index cccf817..0000000
--- a/third_party/packages/widget_icons/example/macos/Flutter/GeneratedPluginRegistrant.swift
+++ /dev/null
@@ -1,10 +0,0 @@
-//
-//  Generated file. Do not edit.
-//
-
-import FlutterMacOS
-import Foundation
-
-
-func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
-}
diff --git a/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/project.pbxproj b/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/project.pbxproj
deleted file mode 100644
index c84862c..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,572 +0,0 @@
-// !$*UTF8*$!
-{
-	archiveVersion = 1;
-	classes = {
-	};
-	objectVersion = 51;
-	objects = {
-
-/* Begin PBXAggregateTarget section */
-		33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
-			isa = PBXAggregateTarget;
-			buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
-			buildPhases = (
-				33CC111E2044C6BF0003C045 /* ShellScript */,
-			);
-			dependencies = (
-			);
-			name = "Flutter Assemble";
-			productName = FLX;
-		};
-/* End PBXAggregateTarget section */
-
-/* Begin PBXBuildFile section */
-		335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
-		33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
-		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
-		33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
-		33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
-		33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
-			isa = PBXContainerItemProxy;
-			containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
-			proxyType = 1;
-			remoteGlobalIDString = 33CC111A2044C6BA0003C045;
-			remoteInfo = FLX;
-		};
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXCopyFilesBuildPhase section */
-		33CC110E2044A8840003C045 /* Bundle Framework */ = {
-			isa = PBXCopyFilesBuildPhase;
-			buildActionMask = 2147483647;
-			dstPath = "";
-			dstSubfolderSpec = 10;
-			files = (
-			);
-			name = "Bundle Framework";
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
-		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
-		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
-		33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
-		33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
-		33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
-		33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
-		33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
-		33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
-		33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
-		33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
-		33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
-		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
-		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
-		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
-		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
-		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
-		33CC10EA2044A3C60003C045 /* Frameworks */ = {
-			isa = PBXFrameworksBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
-		33BA886A226E78AF003329D5 /* Configs */ = {
-			isa = PBXGroup;
-			children = (
-				33E5194F232828860026EE4D /* AppInfo.xcconfig */,
-				9740EEB21CF90195004384FC /* Debug.xcconfig */,
-				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
-				333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
-			);
-			path = Configs;
-			sourceTree = "<group>";
-		};
-		33CC10E42044A3C60003C045 = {
-			isa = PBXGroup;
-			children = (
-				33FAB671232836740065AC1E /* Runner */,
-				33CEB47122A05771004F2AC0 /* Flutter */,
-				33CC10EE2044A3C60003C045 /* Products */,
-				D73912EC22F37F3D000D13A0 /* Frameworks */,
-			);
-			sourceTree = "<group>";
-		};
-		33CC10EE2044A3C60003C045 /* Products */ = {
-			isa = PBXGroup;
-			children = (
-				33CC10ED2044A3C60003C045 /* example.app */,
-			);
-			name = Products;
-			sourceTree = "<group>";
-		};
-		33CC11242044D66E0003C045 /* Resources */ = {
-			isa = PBXGroup;
-			children = (
-				33CC10F22044A3C60003C045 /* Assets.xcassets */,
-				33CC10F42044A3C60003C045 /* MainMenu.xib */,
-				33CC10F72044A3C60003C045 /* Info.plist */,
-			);
-			name = Resources;
-			path = ..;
-			sourceTree = "<group>";
-		};
-		33CEB47122A05771004F2AC0 /* Flutter */ = {
-			isa = PBXGroup;
-			children = (
-				335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
-				33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
-				33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
-				33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
-			);
-			path = Flutter;
-			sourceTree = "<group>";
-		};
-		33FAB671232836740065AC1E /* Runner */ = {
-			isa = PBXGroup;
-			children = (
-				33CC10F02044A3C60003C045 /* AppDelegate.swift */,
-				33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
-				33E51913231747F40026EE4D /* DebugProfile.entitlements */,
-				33E51914231749380026EE4D /* Release.entitlements */,
-				33CC11242044D66E0003C045 /* Resources */,
-				33BA886A226E78AF003329D5 /* Configs */,
-			);
-			path = Runner;
-			sourceTree = "<group>";
-		};
-		D73912EC22F37F3D000D13A0 /* Frameworks */ = {
-			isa = PBXGroup;
-			children = (
-			);
-			name = Frameworks;
-			sourceTree = "<group>";
-		};
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
-		33CC10EC2044A3C60003C045 /* Runner */ = {
-			isa = PBXNativeTarget;
-			buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
-			buildPhases = (
-				33CC10E92044A3C60003C045 /* Sources */,
-				33CC10EA2044A3C60003C045 /* Frameworks */,
-				33CC10EB2044A3C60003C045 /* Resources */,
-				33CC110E2044A8840003C045 /* Bundle Framework */,
-				3399D490228B24CF009A79C7 /* ShellScript */,
-			);
-			buildRules = (
-			);
-			dependencies = (
-				33CC11202044C79F0003C045 /* PBXTargetDependency */,
-			);
-			name = Runner;
-			productName = Runner;
-			productReference = 33CC10ED2044A3C60003C045 /* example.app */;
-			productType = "com.apple.product-type.application";
-		};
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
-		33CC10E52044A3C60003C045 /* Project object */ = {
-			isa = PBXProject;
-			attributes = {
-				LastSwiftUpdateCheck = 0920;
-				LastUpgradeCheck = 1300;
-				ORGANIZATIONNAME = "";
-				TargetAttributes = {
-					33CC10EC2044A3C60003C045 = {
-						CreatedOnToolsVersion = 9.2;
-						LastSwiftMigration = 1100;
-						ProvisioningStyle = Automatic;
-						SystemCapabilities = {
-							com.apple.Sandbox = {
-								enabled = 1;
-							};
-						};
-					};
-					33CC111A2044C6BA0003C045 = {
-						CreatedOnToolsVersion = 9.2;
-						ProvisioningStyle = Manual;
-					};
-				};
-			};
-			buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
-			compatibilityVersion = "Xcode 9.3";
-			developmentRegion = en;
-			hasScannedForEncodings = 0;
-			knownRegions = (
-				en,
-				Base,
-			);
-			mainGroup = 33CC10E42044A3C60003C045;
-			productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
-			projectDirPath = "";
-			projectRoot = "";
-			targets = (
-				33CC10EC2044A3C60003C045 /* Runner */,
-				33CC111A2044C6BA0003C045 /* Flutter Assemble */,
-			);
-		};
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
-		33CC10EB2044A3C60003C045 /* Resources */ = {
-			isa = PBXResourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
-				33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
-		3399D490228B24CF009A79C7 /* ShellScript */ = {
-			isa = PBXShellScriptBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputFileListPaths = (
-			);
-			inputPaths = (
-			);
-			outputFileListPaths = (
-			);
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
-		};
-		33CC111E2044C6BF0003C045 /* ShellScript */ = {
-			isa = PBXShellScriptBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputFileListPaths = (
-				Flutter/ephemeral/FlutterInputs.xcfilelist,
-			);
-			inputPaths = (
-				Flutter/ephemeral/tripwire,
-			);
-			outputFileListPaths = (
-				Flutter/ephemeral/FlutterOutputs.xcfilelist,
-			);
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
-		};
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
-		33CC10E92044A3C60003C045 /* Sources */ = {
-			isa = PBXSourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
-				33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
-				335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
-		33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
-			isa = PBXTargetDependency;
-			target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
-			targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
-		};
-/* End PBXTargetDependency section */
-
-/* Begin PBXVariantGroup section */
-		33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
-			isa = PBXVariantGroup;
-			children = (
-				33CC10F52044A3C60003C045 /* Base */,
-			);
-			name = MainMenu.xib;
-			path = Runner;
-			sourceTree = "<group>";
-		};
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
-		338D0CE9231458BD00FA5F75 /* Profile */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
-				CLANG_WARN_SUSPICIOUS_MOVE = YES;
-				CODE_SIGN_IDENTITY = "-";
-				COPY_PHASE_STRIP = NO;
-				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
-				ENABLE_NS_ASSERTIONS = NO;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu11;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.11;
-				MTL_ENABLE_DEBUG_INFO = NO;
-				SDKROOT = macosx;
-				SWIFT_COMPILATION_MODE = wholemodule;
-				SWIFT_OPTIMIZATION_LEVEL = "-O";
-			};
-			name = Profile;
-		};
-		338D0CEA231458BD00FA5F75 /* Profile */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
-			buildSettings = {
-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				CLANG_ENABLE_MODULES = YES;
-				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
-				CODE_SIGN_STYLE = Automatic;
-				COMBINE_HIDPI_IMAGES = YES;
-				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/../Frameworks",
-				);
-				PROVISIONING_PROFILE_SPECIFIER = "";
-				SWIFT_VERSION = 5.0;
-			};
-			name = Profile;
-		};
-		338D0CEB231458BD00FA5F75 /* Profile */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				CODE_SIGN_STYLE = Manual;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-			};
-			name = Profile;
-		};
-		33CC10F92044A3C60003C045 /* Debug */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
-				CLANG_WARN_SUSPICIOUS_MOVE = YES;
-				CODE_SIGN_IDENTITY = "-";
-				COPY_PHASE_STRIP = NO;
-				DEBUG_INFORMATION_FORMAT = dwarf;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				ENABLE_TESTABILITY = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu11;
-				GCC_DYNAMIC_NO_PIC = NO;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_OPTIMIZATION_LEVEL = 0;
-				GCC_PREPROCESSOR_DEFINITIONS = (
-					"DEBUG=1",
-					"$(inherited)",
-				);
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.11;
-				MTL_ENABLE_DEBUG_INFO = YES;
-				ONLY_ACTIVE_ARCH = YES;
-				SDKROOT = macosx;
-				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
-				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
-			};
-			name = Debug;
-		};
-		33CC10FA2044A3C60003C045 /* Release */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
-			buildSettings = {
-				ALWAYS_SEARCH_USER_PATHS = NO;
-				CLANG_ANALYZER_NONNULL = YES;
-				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
-				CLANG_CXX_LIBRARY = "libc++";
-				CLANG_ENABLE_MODULES = YES;
-				CLANG_ENABLE_OBJC_ARC = YES;
-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
-				CLANG_WARN_BOOL_CONVERSION = YES;
-				CLANG_WARN_CONSTANT_CONVERSION = YES;
-				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
-				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
-				CLANG_WARN_EMPTY_BODY = YES;
-				CLANG_WARN_ENUM_CONVERSION = YES;
-				CLANG_WARN_INFINITE_RECURSION = YES;
-				CLANG_WARN_INT_CONVERSION = YES;
-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
-				CLANG_WARN_SUSPICIOUS_MOVE = YES;
-				CODE_SIGN_IDENTITY = "-";
-				COPY_PHASE_STRIP = NO;
-				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
-				ENABLE_NS_ASSERTIONS = NO;
-				ENABLE_STRICT_OBJC_MSGSEND = YES;
-				GCC_C_LANGUAGE_STANDARD = gnu11;
-				GCC_NO_COMMON_BLOCKS = YES;
-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
-				GCC_WARN_UNUSED_FUNCTION = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.11;
-				MTL_ENABLE_DEBUG_INFO = NO;
-				SDKROOT = macosx;
-				SWIFT_COMPILATION_MODE = wholemodule;
-				SWIFT_OPTIMIZATION_LEVEL = "-O";
-			};
-			name = Release;
-		};
-		33CC10FC2044A3C60003C045 /* Debug */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
-			buildSettings = {
-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				CLANG_ENABLE_MODULES = YES;
-				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
-				CODE_SIGN_STYLE = Automatic;
-				COMBINE_HIDPI_IMAGES = YES;
-				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/../Frameworks",
-				);
-				PROVISIONING_PROFILE_SPECIFIER = "";
-				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
-				SWIFT_VERSION = 5.0;
-			};
-			name = Debug;
-		};
-		33CC10FD2044A3C60003C045 /* Release */ = {
-			isa = XCBuildConfiguration;
-			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
-			buildSettings = {
-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
-				CLANG_ENABLE_MODULES = YES;
-				CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
-				CODE_SIGN_STYLE = Automatic;
-				COMBINE_HIDPI_IMAGES = YES;
-				INFOPLIST_FILE = Runner/Info.plist;
-				LD_RUNPATH_SEARCH_PATHS = (
-					"$(inherited)",
-					"@executable_path/../Frameworks",
-				);
-				PROVISIONING_PROFILE_SPECIFIER = "";
-				SWIFT_VERSION = 5.0;
-			};
-			name = Release;
-		};
-		33CC111C2044C6BA0003C045 /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				CODE_SIGN_STYLE = Manual;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-			};
-			name = Debug;
-		};
-		33CC111D2044C6BA0003C045 /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				CODE_SIGN_STYLE = Automatic;
-				PRODUCT_NAME = "$(TARGET_NAME)";
-			};
-			name = Release;
-		};
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
-		33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				33CC10F92044A3C60003C045 /* Debug */,
-				33CC10FA2044A3C60003C045 /* Release */,
-				338D0CE9231458BD00FA5F75 /* Profile */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-		33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				33CC10FC2044A3C60003C045 /* Debug */,
-				33CC10FD2044A3C60003C045 /* Release */,
-				338D0CEA231458BD00FA5F75 /* Profile */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-		33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				33CC111C2044C6BA0003C045 /* Debug */,
-				33CC111D2044C6BA0003C045 /* Release */,
-				338D0CEB231458BD00FA5F75 /* Profile */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-/* End XCConfigurationList section */
-	};
-	rootObject = 33CC10E52044A3C60003C045 /* Project object */;
-}
diff --git a/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>IDEDidComputeMac32BitWarning</key>
-	<true/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100644
index fb7259e..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,87 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Scheme
-   LastUpgradeVersion = "1300"
-   version = "1.3">
-   <BuildAction
-      parallelizeBuildables = "YES"
-      buildImplicitDependencies = "YES">
-      <BuildActionEntries>
-         <BuildActionEntry
-            buildForTesting = "YES"
-            buildForRunning = "YES"
-            buildForProfiling = "YES"
-            buildForArchiving = "YES"
-            buildForAnalyzing = "YES">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "33CC10EC2044A3C60003C045"
-               BuildableName = "example.app"
-               BlueprintName = "Runner"
-               ReferencedContainer = "container:Runner.xcodeproj">
-            </BuildableReference>
-         </BuildActionEntry>
-      </BuildActionEntries>
-   </BuildAction>
-   <TestAction
-      buildConfiguration = "Debug"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      shouldUseLaunchSchemeArgsEnv = "YES">
-      <MacroExpansion>
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
-            BuildableName = "example.app"
-            BlueprintName = "Runner"
-            ReferencedContainer = "container:Runner.xcodeproj">
-         </BuildableReference>
-      </MacroExpansion>
-      <Testables>
-      </Testables>
-   </TestAction>
-   <LaunchAction
-      buildConfiguration = "Debug"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      launchStyle = "0"
-      useCustomWorkingDirectory = "NO"
-      ignoresPersistentStateOnLaunch = "NO"
-      debugDocumentVersioning = "YES"
-      debugServiceExtension = "internal"
-      allowLocationSimulation = "YES">
-      <BuildableProductRunnable
-         runnableDebuggingMode = "0">
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
-            BuildableName = "example.app"
-            BlueprintName = "Runner"
-            ReferencedContainer = "container:Runner.xcodeproj">
-         </BuildableReference>
-      </BuildableProductRunnable>
-   </LaunchAction>
-   <ProfileAction
-      buildConfiguration = "Profile"
-      shouldUseLaunchSchemeArgsEnv = "YES"
-      savedToolIdentifier = ""
-      useCustomWorkingDirectory = "NO"
-      debugDocumentVersioning = "YES">
-      <BuildableProductRunnable
-         runnableDebuggingMode = "0">
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
-            BuildableName = "example.app"
-            BlueprintName = "Runner"
-            ReferencedContainer = "container:Runner.xcodeproj">
-         </BuildableReference>
-      </BuildableProductRunnable>
-   </ProfileAction>
-   <AnalyzeAction
-      buildConfiguration = "Debug">
-   </AnalyzeAction>
-   <ArchiveAction
-      buildConfiguration = "Release"
-      revealArchiveInOrganizer = "YES">
-   </ArchiveAction>
-</Scheme>
diff --git a/third_party/packages/widget_icons/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/third_party/packages/widget_icons/example/macos/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 1d526a1..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Workspace
-   version = "1.0">
-   <FileRef
-      location = "group:Runner.xcodeproj">
-   </FileRef>
-</Workspace>
diff --git a/third_party/packages/widget_icons/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/third_party/packages/widget_icons/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>IDEDidComputeMac32BitWarning</key>
-	<true/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/macos/Runner/AppDelegate.swift b/third_party/packages/widget_icons/example/macos/Runner/AppDelegate.swift
deleted file mode 100644
index d53ef64..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/AppDelegate.swift
+++ /dev/null
@@ -1,9 +0,0 @@
-import Cocoa
-import FlutterMacOS
-
-@NSApplicationMain
-class AppDelegate: FlutterAppDelegate {
-  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
-    return true
-  }
-}
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index a2ec33f..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
-  "images" : [
-    {
-      "size" : "16x16",
-      "idiom" : "mac",
-      "filename" : "app_icon_16.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "16x16",
-      "idiom" : "mac",
-      "filename" : "app_icon_32.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "32x32",
-      "idiom" : "mac",
-      "filename" : "app_icon_32.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "32x32",
-      "idiom" : "mac",
-      "filename" : "app_icon_64.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "128x128",
-      "idiom" : "mac",
-      "filename" : "app_icon_128.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "128x128",
-      "idiom" : "mac",
-      "filename" : "app_icon_256.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "256x256",
-      "idiom" : "mac",
-      "filename" : "app_icon_256.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "256x256",
-      "idiom" : "mac",
-      "filename" : "app_icon_512.png",
-      "scale" : "2x"
-    },
-    {
-      "size" : "512x512",
-      "idiom" : "mac",
-      "filename" : "app_icon_512.png",
-      "scale" : "1x"
-    },
-    {
-      "size" : "512x512",
-      "idiom" : "mac",
-      "filename" : "app_icon_1024.png",
-      "scale" : "2x"
-    }
-  ],
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  }
-}
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
deleted file mode 100644
index 3c4935a..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
deleted file mode 100644
index ed4cc16..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
deleted file mode 100644
index 483be61..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
deleted file mode 100644
index bcbf36d..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
deleted file mode 100644
index 9c0a652..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
deleted file mode 100644
index e71a726..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
deleted file mode 100644
index 8a31fe2..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Base.lproj/MainMenu.xib b/third_party/packages/widget_icons/example/macos/Runner/Base.lproj/MainMenu.xib
deleted file mode 100644
index 537341a..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Base.lproj/MainMenu.xib
+++ /dev/null
@@ -1,339 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
-    <dependencies>
-        <deployment identifier="macosx"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
-        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
-    </dependencies>
-    <objects>
-        <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
-            <connections>
-                <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
-            </connections>
-        </customObject>
-        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
-        <customObject id="-3" userLabel="Application" customClass="NSObject"/>
-        <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
-            <connections>
-                <outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
-                <outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
-            </connections>
-        </customObject>
-        <customObject id="YLy-65-1bz" customClass="NSFontManager"/>
-        <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
-            <items>
-                <menuItem title="APP_NAME" id="1Xt-HY-uBw">
-                    <modifierMask key="keyEquivalentModifierMask"/>
-                    <menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
-                        <items>
-                            <menuItem title="About APP_NAME" id="5kV-Vb-QxS">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <connections>
-                                    <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
-                            <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
-                            <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
-                            <menuItem title="Services" id="NMo-om-nkz">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
-                            </menuItem>
-                            <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
-                            <menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
-                                <connections>
-                                    <action selector="hide:" target="-1" id="PnN-Uc-m68"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
-                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
-                                <connections>
-                                    <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Show All" id="Kd2-mp-pUS">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <connections>
-                                    <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
-                            <menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
-                                <connections>
-                                    <action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
-                                </connections>
-                            </menuItem>
-                        </items>
-                    </menu>
-                </menuItem>
-                <menuItem title="Edit" id="5QF-Oa-p0T">
-                    <modifierMask key="keyEquivalentModifierMask"/>
-                    <menu key="submenu" title="Edit" id="W48-6f-4Dl">
-                        <items>
-                            <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
-                                <connections>
-                                    <action selector="undo:" target="-1" id="M6e-cu-g7V"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
-                                <connections>
-                                    <action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
-                            <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
-                                <connections>
-                                    <action selector="cut:" target="-1" id="YJe-68-I9s"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
-                                <connections>
-                                    <action selector="copy:" target="-1" id="G1f-GL-Joy"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
-                                <connections>
-                                    <action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
-                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
-                                <connections>
-                                    <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Delete" id="pa3-QI-u2k">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <connections>
-                                    <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
-                                <connections>
-                                    <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
-                            <menuItem title="Find" id="4EN-yA-p0u">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <menu key="submenu" title="Find" id="1b7-l0-nxx">
-                                    <items>
-                                        <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
-                                            <connections>
-                                                <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
-                                            <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
-                                            <connections>
-                                                <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
-                                            <connections>
-                                                <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
-                                            <connections>
-                                                <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
-                                            <connections>
-                                                <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
-                                            <connections>
-                                                <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
-                                            </connections>
-                                        </menuItem>
-                                    </items>
-                                </menu>
-                            </menuItem>
-                            <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
-                                    <items>
-                                        <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
-                                            <connections>
-                                                <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
-                                            <connections>
-                                                <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
-                                        <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
-                                            </connections>
-                                        </menuItem>
-                                    </items>
-                                </menu>
-                            </menuItem>
-                            <menuItem title="Substitutions" id="9ic-FL-obx">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
-                                    <items>
-                                        <menuItem title="Show Substitutions" id="z6F-FW-3nz">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
-                                        <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Smart Quotes" id="hQb-2v-fYv">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Smart Dashes" id="rgM-f4-ycn">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Smart Links" id="cwL-P1-jid">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Data Detectors" id="tRr-pd-1PS">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Text Replacement" id="HFQ-gK-NFA">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
-                                            </connections>
-                                        </menuItem>
-                                    </items>
-                                </menu>
-                            </menuItem>
-                            <menuItem title="Transformations" id="2oI-Rn-ZJC">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <menu key="submenu" title="Transformations" id="c8a-y6-VQd">
-                                    <items>
-                                        <menuItem title="Make Upper Case" id="vmV-6d-7jI">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Make Lower Case" id="d9M-CD-aMd">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Capitalize" id="UEZ-Bs-lqG">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
-                                            </connections>
-                                        </menuItem>
-                                    </items>
-                                </menu>
-                            </menuItem>
-                            <menuItem title="Speech" id="xrE-MZ-jX0">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <menu key="submenu" title="Speech" id="3rS-ZA-NoH">
-                                    <items>
-                                        <menuItem title="Start Speaking" id="Ynk-f8-cLZ">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
-                                            </connections>
-                                        </menuItem>
-                                        <menuItem title="Stop Speaking" id="Oyz-dy-DGm">
-                                            <modifierMask key="keyEquivalentModifierMask"/>
-                                            <connections>
-                                                <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
-                                            </connections>
-                                        </menuItem>
-                                    </items>
-                                </menu>
-                            </menuItem>
-                        </items>
-                    </menu>
-                </menuItem>
-                <menuItem title="View" id="H8h-7b-M4v">
-                    <modifierMask key="keyEquivalentModifierMask"/>
-                    <menu key="submenu" title="View" id="HyV-fh-RgO">
-                        <items>
-                            <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
-                                <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
-                                <connections>
-                                    <action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
-                                </connections>
-                            </menuItem>
-                        </items>
-                    </menu>
-                </menuItem>
-                <menuItem title="Window" id="aUF-d1-5bR">
-                    <modifierMask key="keyEquivalentModifierMask"/>
-                    <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
-                        <items>
-                            <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
-                                <connections>
-                                    <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem title="Zoom" id="R4o-n2-Eq4">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <connections>
-                                    <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
-                                </connections>
-                            </menuItem>
-                            <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
-                            <menuItem title="Bring All to Front" id="LE2-aR-0XJ">
-                                <modifierMask key="keyEquivalentModifierMask"/>
-                                <connections>
-                                    <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
-                                </connections>
-                            </menuItem>
-                        </items>
-                    </menu>
-                </menuItem>
-            </items>
-            <point key="canvasLocation" x="142" y="-258"/>
-        </menu>
-        <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
-            <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
-            <rect key="contentRect" x="335" y="390" width="800" height="600"/>
-            <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
-            <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
-                <rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
-                <autoresizingMask key="autoresizingMask"/>
-            </view>
-        </window>
-    </objects>
-</document>
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Configs/AppInfo.xcconfig b/third_party/packages/widget_icons/example/macos/Runner/Configs/AppInfo.xcconfig
deleted file mode 100644
index cf9be60..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Configs/AppInfo.xcconfig
+++ /dev/null
@@ -1,14 +0,0 @@
-// Application-level settings for the Runner target.
-//
-// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
-// future. If not, the values below would default to using the project name when this becomes a
-// 'flutter create' template.
-
-// The application's name. By default this is also the title of the Flutter window.
-PRODUCT_NAME = example
-
-// The application's bundle identifier
-PRODUCT_BUNDLE_IDENTIFIER = com.example.example
-
-// The copyright displayed in application information
-PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Configs/Debug.xcconfig b/third_party/packages/widget_icons/example/macos/Runner/Configs/Debug.xcconfig
deleted file mode 100644
index 36b0fd9..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Configs/Debug.xcconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-#include "../../Flutter/Flutter-Debug.xcconfig"
-#include "Warnings.xcconfig"
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Configs/Release.xcconfig b/third_party/packages/widget_icons/example/macos/Runner/Configs/Release.xcconfig
deleted file mode 100644
index dff4f49..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Configs/Release.xcconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-#include "../../Flutter/Flutter-Release.xcconfig"
-#include "Warnings.xcconfig"
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Configs/Warnings.xcconfig b/third_party/packages/widget_icons/example/macos/Runner/Configs/Warnings.xcconfig
deleted file mode 100644
index 42bcbf4..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Configs/Warnings.xcconfig
+++ /dev/null
@@ -1,13 +0,0 @@
-WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
-GCC_WARN_UNDECLARED_SELECTOR = YES
-CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
-CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
-CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
-CLANG_WARN_PRAGMA_PACK = YES
-CLANG_WARN_STRICT_PROTOTYPES = YES
-CLANG_WARN_COMMA = YES
-GCC_WARN_STRICT_SELECTOR_MATCH = YES
-CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
-CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
-GCC_WARN_SHADOW = YES
-CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/third_party/packages/widget_icons/example/macos/Runner/DebugProfile.entitlements b/third_party/packages/widget_icons/example/macos/Runner/DebugProfile.entitlements
deleted file mode 100644
index dddb8a3..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/DebugProfile.entitlements
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>com.apple.security.app-sandbox</key>
-	<true/>
-	<key>com.apple.security.cs.allow-jit</key>
-	<true/>
-	<key>com.apple.security.network.server</key>
-	<true/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Info.plist b/third_party/packages/widget_icons/example/macos/Runner/Info.plist
deleted file mode 100644
index 4789daa..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Info.plist
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>CFBundleDevelopmentRegion</key>
-	<string>$(DEVELOPMENT_LANGUAGE)</string>
-	<key>CFBundleExecutable</key>
-	<string>$(EXECUTABLE_NAME)</string>
-	<key>CFBundleIconFile</key>
-	<string></string>
-	<key>CFBundleIdentifier</key>
-	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
-	<key>CFBundleInfoDictionaryVersion</key>
-	<string>6.0</string>
-	<key>CFBundleName</key>
-	<string>$(PRODUCT_NAME)</string>
-	<key>CFBundlePackageType</key>
-	<string>APPL</string>
-	<key>CFBundleShortVersionString</key>
-	<string>$(FLUTTER_BUILD_NAME)</string>
-	<key>CFBundleVersion</key>
-	<string>$(FLUTTER_BUILD_NUMBER)</string>
-	<key>LSMinimumSystemVersion</key>
-	<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
-	<key>NSHumanReadableCopyright</key>
-	<string>$(PRODUCT_COPYRIGHT)</string>
-	<key>NSMainNibFile</key>
-	<string>MainMenu</string>
-	<key>NSPrincipalClass</key>
-	<string>NSApplication</string>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/macos/Runner/MainFlutterWindow.swift b/third_party/packages/widget_icons/example/macos/Runner/MainFlutterWindow.swift
deleted file mode 100644
index 2722837..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/MainFlutterWindow.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-import Cocoa
-import FlutterMacOS
-
-class MainFlutterWindow: NSWindow {
-  override func awakeFromNib() {
-    let flutterViewController = FlutterViewController.init()
-    let windowFrame = self.frame
-    self.contentViewController = flutterViewController
-    self.setFrame(windowFrame, display: true)
-
-    RegisterGeneratedPlugins(registry: flutterViewController)
-
-    super.awakeFromNib()
-  }
-}
diff --git a/third_party/packages/widget_icons/example/macos/Runner/Release.entitlements b/third_party/packages/widget_icons/example/macos/Runner/Release.entitlements
deleted file mode 100644
index 852fa1a..0000000
--- a/third_party/packages/widget_icons/example/macos/Runner/Release.entitlements
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>com.apple.security.app-sandbox</key>
-	<true/>
-</dict>
-</plist>
diff --git a/third_party/packages/widget_icons/example/pubspec.yaml b/third_party/packages/widget_icons/example/pubspec.yaml
index 02b5305..6aee3ce 100644
--- a/third_party/packages/widget_icons/example/pubspec.yaml
+++ b/third_party/packages/widget_icons/example/pubspec.yaml
@@ -1,31 +1,11 @@
-name: example
-description: A new Flutter project.
-
-# The following line prevents the package from being accidentally published to
-# pub.dev using `flutter pub publish`. This is preferred for private packages.
-publish_to: "none" # Remove this line if you wish to publish to pub.dev
-
-# The following defines the version and build number for your application.
-# A version number is three numbers separated by dots, like 1.2.43
-# followed by an optional build number separated by a +.
-# Both the version and the builder number may be overridden in flutter
-# build by specifying --build-name and --build-number, respectively.
-# In Android, build-name is used as versionName while build-number used as versionCode.
-# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
-# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
-# Read more about iOS versioning at
-# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 1.0.0+1
+name: widget_icons_example
+description: Example usage of widget_icons.
+publish_to: none
+version: 0.0.2
 
 environment:
-  sdk: ">=2.12.0 <3.0.0"
+  sdk: ^3.0.0
 
-# Dependencies specify other packages that your package needs in order to work.
-# To automatically upgrade your package dependencies to the latest versions
-# consider running `flutter pub upgrade --major-versions`. Alternatively,
-# dependencies can be manually updated by changing the version numbers below to
-# the latest version available on pub.dev. To see which dependencies have newer
-# versions available, run `flutter pub outdated`.
 dependencies:
   flutter:
     sdk: flutter
@@ -36,47 +16,7 @@
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  flutter_lints: ^3.0.1
 
-  # The "flutter_lints" package below contains a set of recommended lints to
-  # encourage good coding practices. The lint set provided by the package is
-  # activated in the `analysis_options.yaml` file located at the root of your
-  # package. See that file for information about deactivating specific lint
-  # rules and activating additional ones.
-  flutter_lints: ^1.0.0
-
-# For information on the generic Dart part of this file, see the
-# following page: https://dart.dev/tools/pub/pubspec
-
-# The following section is specific to Flutter.
 flutter:
-  # The following line ensures that the Material Icons font is
-  # included with your application, so that you can use the icons in
-  # the material Icons class.
   uses-material-design: true
-  # To add assets to your application, add an assets section, like this:
-  # assets:
-  #   - images/a_dot_burr.jpeg
-  #   - images/a_dot_ham.jpeg
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.dev/assets-and-images/#resolution-aware.
-  # For details regarding adding assets from package dependencies, see
-  # https://flutter.dev/assets-and-images/#from-packages
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
-  #
-  # For details regarding fonts from package dependencies,
-  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/third_party/packages/widget_icons/example/web/favicon.png b/third_party/packages/widget_icons/example/web/favicon.png
deleted file mode 100644
index 8aaa46a..0000000
--- a/third_party/packages/widget_icons/example/web/favicon.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/web/icons/Icon-192.png b/third_party/packages/widget_icons/example/web/icons/Icon-192.png
deleted file mode 100644
index b749bfe..0000000
--- a/third_party/packages/widget_icons/example/web/icons/Icon-192.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/web/icons/Icon-512.png b/third_party/packages/widget_icons/example/web/icons/Icon-512.png
deleted file mode 100644
index 88cfd48..0000000
--- a/third_party/packages/widget_icons/example/web/icons/Icon-512.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/web/icons/Icon-maskable-192.png b/third_party/packages/widget_icons/example/web/icons/Icon-maskable-192.png
deleted file mode 100644
index eb9b4d7..0000000
--- a/third_party/packages/widget_icons/example/web/icons/Icon-maskable-192.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/web/icons/Icon-maskable-512.png b/third_party/packages/widget_icons/example/web/icons/Icon-maskable-512.png
deleted file mode 100644
index d69c566..0000000
--- a/third_party/packages/widget_icons/example/web/icons/Icon-maskable-512.png
+++ /dev/null
Binary files differ
diff --git a/third_party/packages/widget_icons/example/web/index.html b/third_party/packages/widget_icons/example/web/index.html
deleted file mode 100644
index b6b9dd2..0000000
--- a/third_party/packages/widget_icons/example/web/index.html
+++ /dev/null
@@ -1,104 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <!--
-    If you are serving your web app in a path other than the root, change the
-    href value below to reflect the base path you are serving from.
-
-    The path provided below has to start and end with a slash "/" in order for
-    it to work correctly.
-
-    For more details:
-    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
-
-    This is a placeholder for base href that will be replaced by the value of
-    the `--base-href` argument provided to `flutter build`.
-  -->
-  <base href="$FLUTTER_BASE_HREF">
-
-  <meta charset="UTF-8">
-  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
-  <meta name="description" content="A new Flutter project.">
-
-  <!-- iOS meta tags & icons -->
-  <meta name="apple-mobile-web-app-capable" content="yes">
-  <meta name="apple-mobile-web-app-status-bar-style" content="black">
-  <meta name="apple-mobile-web-app-title" content="example">
-  <link rel="apple-touch-icon" href="icons/Icon-192.png">
-
-  <!-- Favicon -->
-  <link rel="icon" type="image/png" href="favicon.png"/>
-
-  <title>example</title>
-  <link rel="manifest" href="manifest.json">
-</head>
-<body>
-  <!-- This script installs service_worker.js to provide PWA functionality to
-       application. For more information, see:
-       https://developers.google.com/web/fundamentals/primers/service-workers -->
-  <script>
-    var serviceWorkerVersion = null;
-    var scriptLoaded = false;
-    function loadMainDartJs() {
-      if (scriptLoaded) {
-        return;
-      }
-      scriptLoaded = true;
-      var scriptTag = document.createElement('script');
-      scriptTag.src = 'main.dart.js';
-      scriptTag.type = 'application/javascript';
-      document.body.append(scriptTag);
-    }
-
-    if ('serviceWorker' in navigator) {
-      // Service workers are supported. Use them.
-      window.addEventListener('load', function () {
-        // Wait for registration to finish before dropping the <script> tag.
-        // Otherwise, the browser will load the script multiple times,
-        // potentially different versions.
-        var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
-        navigator.serviceWorker.register(serviceWorkerUrl)
-          .then((reg) => {
-            function waitForActivation(serviceWorker) {
-              serviceWorker.addEventListener('statechange', () => {
-                if (serviceWorker.state == 'activated') {
-                  console.log('Installed new service worker.');
-                  loadMainDartJs();
-                }
-              });
-            }
-            if (!reg.active && (reg.installing || reg.waiting)) {
-              // No active web worker and we have installed or are installing
-              // one for the first time. Simply wait for it to activate.
-              waitForActivation(reg.installing || reg.waiting);
-            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
-              // When the app updates the serviceWorkerVersion changes, so we
-              // need to ask the service worker to update.
-              console.log('New service worker available.');
-              reg.update();
-              waitForActivation(reg.installing);
-            } else {
-              // Existing service worker is still good.
-              console.log('Loading app from service worker.');
-              loadMainDartJs();
-            }
-          });
-
-        // If service worker doesn't succeed in a reasonable amount of time,
-        // fallback to plaint <script> tag.
-        setTimeout(() => {
-          if (!scriptLoaded) {
-            console.warn(
-              'Failed to load app from service worker. Falling back to plain <script> tag.',
-            );
-            loadMainDartJs();
-          }
-        }, 4000);
-      });
-    } else {
-      // Service workers not supported. Just drop the <script> tag.
-      loadMainDartJs();
-    }
-  </script>
-</body>
-</html>
diff --git a/third_party/packages/widget_icons/example/web/manifest.json b/third_party/packages/widget_icons/example/web/manifest.json
deleted file mode 100644
index 096edf8..0000000
--- a/third_party/packages/widget_icons/example/web/manifest.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-    "name": "example",
-    "short_name": "example",
-    "start_url": ".",
-    "display": "standalone",
-    "background_color": "#0175C2",
-    "theme_color": "#0175C2",
-    "description": "A new Flutter project.",
-    "orientation": "portrait-primary",
-    "prefer_related_applications": false,
-    "icons": [
-        {
-            "src": "icons/Icon-192.png",
-            "sizes": "192x192",
-            "type": "image/png"
-        },
-        {
-            "src": "icons/Icon-512.png",
-            "sizes": "512x512",
-            "type": "image/png"
-        },
-        {
-            "src": "icons/Icon-maskable-192.png",
-            "sizes": "192x192",
-            "type": "image/png",
-            "purpose": "maskable"
-        },
-        {
-            "src": "icons/Icon-maskable-512.png",
-            "sizes": "512x512",
-            "type": "image/png",
-            "purpose": "maskable"
-        }
-    ]
-}
diff --git a/third_party/packages/widget_icons/pubspec.yaml b/third_party/packages/widget_icons/pubspec.yaml
index bdcb4a5..c826926 100644
--- a/third_party/packages/widget_icons/pubspec.yaml
+++ b/third_party/packages/widget_icons/pubspec.yaml
@@ -4,8 +4,8 @@
 homepage: https://github.com/flutter/devtools/tree/master/third_party/packages/widget_icons
 
 environment:
-  sdk: ">=2.12.0 <3.0.0"
-  flutter: ">=1.17.0"
+  sdk: ^3.0.0
+  flutter: ">=3.7.0"
 
 dependencies:
   flutter:
@@ -14,7 +14,7 @@
 dev_dependencies:
   flutter_test:
     sdk: flutter
-  flutter_lints: ^1.0.0
+  flutter_lints: ^3.0.1
 
 flutter:
   fonts:
diff --git a/tool/RELEASE_INSTRUCTIONS.md b/tool/RELEASE_INSTRUCTIONS.md
index cf68c66..009530b 100644
--- a/tool/RELEASE_INSTRUCTIONS.md
+++ b/tool/RELEASE_INSTRUCTIONS.md
@@ -42,8 +42,8 @@
 
    c. The local checkout is at `main` branch: `git rebase-update`
 
-2. Your Flutter version is equal to latest candidate release branch:
-    - Run `devtools_tool update-flutter-sdk --from-path`
+2. Your Flutter SDK in `tool/flutter-sdk` and the one on PATH are updated to the latest candidate release branch:
+    - Run `devtools_tool update-flutter-sdk --update-on-path`
 3. You have goma [configured](http://go/ma-mac-setup)
 
 ### Prepare the release
@@ -54,7 +54,7 @@
 > If you need to install the [Github CLI](https://cli.github.com/manual/installation) you
 can run: `brew install gh`
 
-- Ensure that you have access to `devtools_tool` by adding the `tool/bin` folder to your
+1. Ensure that you have access to `devtools_tool` by adding the `tool/bin` folder to your
 `PATH` environment variable
   - **MacOS Users**
     - add the following to your `~/.bashrc` file.
@@ -70,18 +70,21 @@
       > Replace `<DEVTOOLS_DIR>` with the local path to your DevTools
       > repo path.
 
-- Run: `devtools_tool release-helper`
-- This will create a PR for you using the tip of master.
-- The branch for that PR will be checked out locally for you.
-- It will also update your local version of flutter to the Latest flutter candidate
-    - This is to facilitate testing in the next steps
+2. Run `devtools_tool release-helper` in order to:
+
+   - create a new branch using the tip of master and check out locally
+   - create a PR for the branch
+   - update your local version of flutter to the latest flutter candidate
+       - This is to facilitate testing in the next steps
+    
+   NOTE: Run the script from `/devtools/tool` while [the issue](https://github.com/dart-lang/sdk/issues/54493) is not adderessed.
 
 #### Verify the version changes for the Release PR
 
 Verify the code on the release PR:
-- updated the `devtools_app` and `devtools_test` pubspec versions
-- updated all references to those packages in other `pubspec.yaml` files
-- updated the version constant in `packages/devtools_app/lib/devtools.dart`
+1. updated the `devtools_app` and `devtools_test` pubspec versions
+2. updated all references to those packages in other `pubspec.yaml` files
+3. updated the version constant in `packages/devtools_app/lib/devtools.dart`
 
 `devtools_app` and `devtools_test` versions are updated in lock, so we don't
 have to worry about versioning between these two packages. The other DevTools
@@ -93,13 +96,13 @@
 
 ### Test the release PR
 
-- Build DevTools in release mode and serve it from a locally running DevTools
+1. Build DevTools in release mode and serve it from a locally running DevTools
 server instance:
    ```shell
    devtools_tool serve
    ```
 
-- Launch DevTools and verify that everything generally works.
+2. Launch DevTools and verify that everything generally works.
    - open the page in a browser (http://localhost:53432)
    - `flutter run` an application
    - connect to the running app from DevTools
@@ -113,7 +116,7 @@
          - the bug fixes,
       - use this commit hash for the following steps.
 
-- Once the build is in good shape,
+3. Once the build is in good shape,
    - revert any local changes.
       ```shell
       git checkout . && \
@@ -125,18 +128,14 @@
 Receive an LGTM for the PR, squash and commit.
 
 ### Tag the release
-- Checkout the commit from which you want to release DevTools
+1. Checkout the commit from which you want to release DevTools
    - This is likely the commit, on `master`, for the PR you just landed
    - You can run `git log -v` to see the commits.
-- Run the `tag_version.sh` script
+2. Run `devtools_tool tag-version`
    - this creates a tag on the `flutter/devtools` repo for this release.
    - This script will automatically determine the version from
    `packages/devtools/pubspec.yaml` so there is no need to manually enter the version.
 
-   ```shell
-   tool/tag_version.sh;
-   ```
-
 ### Wait for the binary to be uploaded CIPD
 
 On each DevTools commit, DevTools is built and uploaded to CIPD. You can check the
@@ -156,9 +155,9 @@
 ```
 
 This automatically creates a Gerrit CL with the DEPS update for DevTools.
-Quickly test the build and then add a reviewer.
+Quickly test the build and then add a reviewer:
 
-- Build the dart sdk locally
+1. Build the dart sdk locally
 
    ```shell
    cd $LOCAL_DART_SDK && \
@@ -166,7 +165,7 @@
    ./tools/build.py -mrelease -ax64 create_sdk;
    ```
 
-- Verify that running `dart devtools` launches the version of DevTools you just released.
+2. Verify that running `dart devtools` launches the version of DevTools you just released.
    - for OSX
       ```shell
       xcodebuild/ReleaseX64/dart-sdk/bin/dart devtools
@@ -176,31 +175,29 @@
       out/ReleaseX64/dart-sdk/bin/dart devtools
       ```
 
-- If the version of DevTools you just published to CIPD does not load properly, 
+3. If the version of DevTools you just published to CIPD does not load properly, 
 you may need to hard reload and clear your browser cache.
 
-- Add a reviewer and submit once approved.
+4. Add a reviewer and submit once approved.
 
 ### Publish DevTools pub packages
 
 If `package:devtools_app_shared`, `package:devtools_extensions`, or
 `package:devtools_shared` have unreleased changes, publish these packages to pub.
 
-- From the respective `devtools/packages/devtools_*` directories, run:
+From the respective `devtools/packages/devtools_*` directories, run:
    ```shell
    flutter pub publish
    ```
 
 ### Update to the next version
--  `gh workflow run daily-dev-bump.yaml -f updateType=minor+dev`
-   -  This will kick off a workflow that will automatically create a PR with a
-   `minor` + `dev` version bump.
-   -  That PR should then be auto-submitted.
--  See https://github.com/flutter/devtools/actions/workflows/daily-dev-bump.yaml
-   -  To see the workflow run
--  Go to https://github.com/flutter/devtools/pulls to see the pull request that
+1. `gh workflow run daily-dev-bump.yaml -f updateType=minor+dev`
+   This will kick off a workflow that will automatically create a PR with a
+   `minor` + `dev` version bump. That PR should then be auto-submitted.
+2. Make sure that the release PR goes through without issue:
+   - See the workflow run [here](https://github.com/flutter/devtools/actions/workflows/daily-dev-bump.yaml)
+   - Go to https://github.com/flutter/devtools/pulls to see the pull request that
    ends up being created
--  You should make sure that the release PR goes through without issue.
 
 ### Verify and Submit the release notes
 
@@ -303,11 +300,10 @@
 release. This will ensure that we have a tag we can branch from if we need to
 create another DevTools cherry-pick release from the tip of the one we just created. 
 
-Check out the cherry-pick branch you created earlier, and run the `tag_version.sh`
-script:
+Check out the cherry-pick branch you created earlier, and create a git tag:
 ```sh
 git checkout upstream/2.29.1
-tool/tag_version.sh
+devtools_tool tag-version
 ```
 
 ### Create the merge commit in the `flutter/devtools` repo
@@ -351,4 +347,4 @@
 - Flutter cherry-pick [Wiki](https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process)
 - Example cherry-pick cl: https://dart-review.googlesource.com/c/sdk/+/336827
 - Example cherry-pick issue: https://github.com/dart-lang/sdk/issues/54085
-- Example merge commit on `flutter/devtools`: https://github.com/flutter/devtools/pull/6812
\ No newline at end of file
+- Example merge commit on `flutter/devtools`: https://github.com/flutter/devtools/pull/6812
diff --git a/tool/json_to_map.dart b/tool/json_to_map.dart
index 4ba55d5..a00abc4 100644
--- a/tool/json_to_map.dart
+++ b/tool/json_to_map.dart
@@ -10,11 +10,11 @@
 
 /// Generates a dart file with a [Map] variable 'data' that contains the JSON
 /// data contained in the given json file (first argument).
-/// 
+///
 /// To use:
-/// 
+///
 /// `dart json_to_map.dart ~/path/to/my_file.json`
-/// 
+///
 /// This will generate a file with the same name but with a '.dart' extension
 /// instead of '.json' (e.g. ~/path/to/my_file.dart). This will be a valid dart
 /// file with a single [Map] variable [data].
diff --git a/tool/lib/commands/analyze.dart b/tool/lib/commands/analyze.dart
index d8a7e48..af6058c 100644
--- a/tool/lib/commands/analyze.dart
+++ b/tool/lib/commands/analyze.dart
@@ -20,7 +20,6 @@
 
   @override
   Future run() async {
-    final sdk = FlutterSdk.current;
     final log = Logger.standard();
     final repo = DevToolsRepo.getInstance();
     final processManager = ProcessManager();
@@ -38,8 +37,7 @@
       final progress = log.progress('  ${p.relativePath}');
 
       final process = await processManager.runProcess(
-        CliCommand.from(
-          sdk.dartToolPath,
+        CliCommand.dart(
           ['analyze', '--fatal-infos'],
           // Run all so we can see the full set of results instead of stopping
           // on the first error.
diff --git a/tool/lib/commands/build.dart b/tool/lib/commands/build.dart
index d313535..e2fbfa2 100644
--- a/tool/lib/commands/build.dart
+++ b/tool/lib/commands/build.dart
@@ -65,12 +65,12 @@
 
     if (updateFlutter) {
       logStatus('updating tool/flutter-sdk to the latest flutter candidate');
-      await processManager.runProcess(CliCommand.tool('update-flutter-sdk'));
+      await processManager.runProcess(CliCommand.tool(['update-flutter-sdk']));
     }
 
     if (updatePerfetto) {
       logStatus('updating the bundled Perfetto assets');
-      await processManager.runProcess(CliCommand.tool('update-perfetto'));
+      await processManager.runProcess(CliCommand.tool(['update-perfetto']));
     }
 
     logStatus('cleaning project');
@@ -78,14 +78,14 @@
       webBuildDir.deleteSync(recursive: true);
     }
     await processManager.runProcess(
-      CliCommand.flutter('clean'),
+      CliCommand.flutter(['clean']),
       workingDirectory: repo.devtoolsAppDirectoryPath,
     );
 
     logStatus('building DevTools in release mode');
     await processManager.runAll(
       commands: [
-        if (runPubGet) CliCommand.tool('pub-get --only-main'),
+        if (runPubGet) CliCommand.tool(['pub-get', '--only-main']),
         CliCommand.flutter(
           [
             'build',
@@ -95,7 +95,7 @@
             '--pwa-strategy=offline-first',
             if (buildMode != 'debug') '--$buildMode',
             '--no-tree-shake-icons',
-          ].join(' '),
+          ],
         ),
       ],
       workingDirectory: repo.devtoolsAppDirectoryPath,
@@ -108,7 +108,7 @@
       for (final file in canvaskitDir.listSync()) {
         if (RegExp(r'canvaskit\..*').hasMatch(file.path)) {
           await processManager
-              .runProcess(CliCommand('chmod 0755 ${file.path}'));
+              .runProcess(CliCommand('chmod', ['0755', file.path]));
         }
       }
     }
diff --git a/tool/lib/commands/fix_goldens.dart b/tool/lib/commands/fix_goldens.dart
index a6e6a31..764d702 100644
--- a/tool/lib/commands/fix_goldens.dart
+++ b/tool/lib/commands/fix_goldens.dart
@@ -41,7 +41,7 @@
     try {
       print('Downloading the artifacts to ${tmpDownloadDir.path}');
       await processManager.runProcess(
-        CliCommand.from(
+        CliCommand(
           'gh',
           [
             'run',
diff --git a/tool/lib/commands/generate_code.dart b/tool/lib/commands/generate_code.dart
index f84bed4..bafa5de 100644
--- a/tool/lib/commands/generate_code.dart
+++ b/tool/lib/commands/generate_code.dart
@@ -38,7 +38,7 @@
     final upgrade = argResults![_upgradeFlag];
     if (upgrade) {
       await processManager.runProcess(
-        CliCommand.tool('pub-get --only-main --upgrade'),
+        CliCommand.tool(['pub-get', '--only-main', '--upgrade']),
       );
     }
 
@@ -47,7 +47,13 @@
       final directoryPath = path.join(repo.repoPath, 'packages', packageName);
       await processManager.runProcess(
         CliCommand.flutter(
-          'pub run build_runner build --delete-conflicting-outputs',
+          [
+            'pub',
+            'run',
+            'build_runner',
+            'build',
+            '--delete-conflicting-outputs',
+          ],
         ),
         workingDirectory: directoryPath,
       );
diff --git a/tool/lib/commands/pub_get.dart b/tool/lib/commands/pub_get.dart
index 9df4ebd..a918599 100644
--- a/tool/lib/commands/pub_get.dart
+++ b/tool/lib/commands/pub_get.dart
@@ -59,7 +59,7 @@
 
       final process = await processManager.runProcess(
         CliCommand.flutter(
-          'pub $command',
+          ['pub', command],
           // Run all so we can see the full set of results instead of stopping
           // on the first error.
           throwOnException: false,
diff --git a/tool/lib/commands/release_helper.dart b/tool/lib/commands/release_helper.dart
index 29a3f4e..8f2c788 100644
--- a/tool/lib/commands/release_helper.dart
+++ b/tool/lib/commands/release_helper.dart
@@ -31,7 +31,7 @@
 
     final useCurrentBranch = argResults!['use-current-branch']!;
     final currentBranchResult = await processManager.runProcess(
-      CliCommand.git(cmd: 'rev-parse --abbrev-ref HEAD'),
+      CliCommand.git(['rev-parse', '--abbrev-ref', 'HEAD']),
     );
     final initialBranch = currentBranchResult.stdout.trim();
     String? releaseBranch;
@@ -44,7 +44,7 @@
       );
 
       final gitStatusResult = await processManager.runProcess(
-        CliCommand.git(cmd: 'status -s'),
+        CliCommand.git(['status', '-s']),
       );
       final gitStatus = gitStatusResult.stdout;
       if (gitStatus.isNotEmpty) {
@@ -57,33 +57,37 @@
       if (!useCurrentBranch) {
         print("Preparing the release branch.");
         await processManager.runProcess(
-          CliCommand.git(cmd: 'fetch $remoteUpstream master'),
+          CliCommand.git(['fetch', remoteUpstream, 'master']),
         );
       }
 
       await processManager.runProcess(
         CliCommand.git(
-          cmd: 'checkout -b $releaseBranch'
-              '${useCurrentBranch ? '' : ' $remoteUpstream/master'}',
+          [
+            'checkout',
+            '-b',
+            releaseBranch,
+            if (useCurrentBranch) '$remoteUpstream/master',
+          ],
         ),
       );
 
       print("Ensuring ./tool packages are ready.");
       Directory.current = pathFromRepoRoot("tool");
       await processManager.runProcess(
-        CliCommand.from(
-          'dart',
-          ['pub', 'get'],
-        ),
+        CliCommand.dart(['pub', 'get']),
         workingDirectory: pathFromRepoRoot("tool"),
       );
 
       print("Setting the release version.");
-      await processManager
-          .runProcess(CliCommand.tool('update-version auto --type release'));
+      await processManager.runProcess(
+        CliCommand.tool(['update-version', 'auto', '--type', 'release']),
+      );
 
       final getNewVersionResult = await processManager.runProcess(
-        CliCommand.tool('update-version current-version'),
+        CliCommand.tool(
+          ['update-version', 'current-version', '|', 'tail', '-1'],
+        ),
       );
 
       final newVersion = getNewVersionResult.stdout.trim();
@@ -92,23 +96,26 @@
 
       await processManager.runAll(
         commands: [
-          CliCommand.git(args: ['commit', '-a', '-m', commitMessage]),
-          CliCommand.git(cmd: 'push -u $remoteUpstream $releaseBranch'),
+          CliCommand.git(['commit', '-a', '-m', commitMessage]),
+          CliCommand.git(['push', '-u', remoteUpstream, releaseBranch]),
         ],
       );
 
       print('Creating the PR.');
       final prURL = await processManager.runProcess(
-        CliCommand.from('gh', [
-          'pr',
-          'create',
-          '--repo',
-          'flutter/devtools',
-          '--draft',
-          '--title',
-          commitMessage,
-          '--fill',
-        ]),
+        CliCommand(
+          'gh',
+          [
+            'pr',
+            'create',
+            '--repo',
+            'flutter/devtools',
+            '--draft',
+            '--title',
+            commitMessage,
+            '--fill',
+          ],
+        ),
       );
 
       print('Your Draft release PR can be found at: ${prURL.stdout.trim()}');
@@ -121,7 +128,7 @@
 
       // try to bring the caller back to their original branch
       await processManager.runProcess(
-        CliCommand.git(cmd: 'checkout $initialBranch'),
+        CliCommand.git(['checkout', initialBranch]),
       );
 
       // try to clean up the temporary branch we made
diff --git a/tool/lib/commands/serve.dart b/tool/lib/commands/serve.dart
index 24a1a95..b32b3d7 100644
--- a/tool/lib/commands/serve.dart
+++ b/tool/lib/commands/serve.dart
@@ -15,6 +15,7 @@
 const _buildAppFlag = 'build-app';
 const _machineFlag = 'machine';
 const _allowEmbeddingFlag = 'allow-embedding';
+const _debugServerFlag = 'debug-server';
 
 /// This command builds DevTools in release mode by running the
 /// `devtools_tool build` command and then serves DevTools with a locally
@@ -26,6 +27,12 @@
 /// to this command. All of the following commands are passed along to the
 /// `devtools_tool build` command.
 ///
+/// If the [_debugServerFlag] argument is present, the DevTools server will be
+/// started with the `--observe` flag. This will allow you to debug and profile
+/// the server with a local VM service connection. By default, this will set
+/// `--pause-isolates-on-start` and `--pause-isolates-on-unhandled-exceptions`
+/// for the DevTools server VM service connection.
+///
 /// If the [BuildCommandArgs.useFlutterFromPath] argument is present, the
 /// Flutter SDK will not be updated to the latest Flutter candidate before
 /// building DevTools. Use this flag to save the cost of updating the Flutter
@@ -57,6 +64,12 @@
             'Whether to build the DevTools web app before starting the DevTools'
             ' server.',
       )
+      ..addFlag(
+        _debugServerFlag,
+        negatable: false,
+        defaultsTo: false,
+        help: 'Enable debugging for the DevTools server.',
+      )
       ..addUpdateFlutterFlag()
       ..addUpdatePerfettoFlag()
       ..addPubGetFlag()
@@ -87,6 +100,7 @@
     final processManager = ProcessManager();
 
     final buildApp = argResults![_buildAppFlag];
+    final debugServer = argResults![_debugServerFlag];
     final updateFlutter = argResults![BuildCommandArgs.updateFlutter.flagName];
     final updatePerfetto =
         argResults![BuildCommandArgs.updatePerfetto.flagName];
@@ -100,6 +114,7 @@
       ..remove(BuildCommandArgs.updatePerfetto.asArg())
       ..remove(valueAsArg(_buildAppFlag))
       ..remove(valueAsArg(_buildAppFlag, negated: true))
+      ..remove(valueAsArg(_debugServerFlag))
       ..remove(BuildCommandArgs.pubGet.asArg())
       ..remove(BuildCommandArgs.pubGet.asArg(negated: true))
       ..removeWhere(
@@ -128,13 +143,13 @@
 
     if (buildApp) {
       final process = await processManager.runProcess(
-        CliCommand.tool(
-          'build'
-          ' ${BuildCommandArgs.updateFlutter.asArg(negated: !updateFlutter)}'
-          '${updatePerfetto ? ' ${BuildCommandArgs.updatePerfetto.asArg()}' : ''}'
-          ' ${BuildCommandArgs.buildMode.asArg()}=$devToolsAppBuildMode'
-          ' ${BuildCommandArgs.pubGet.asArg(negated: !runPubGet)}',
-        ),
+        CliCommand.tool([
+          'build',
+          BuildCommandArgs.updateFlutter.asArg(negated: !updateFlutter),
+          if (updatePerfetto) BuildCommandArgs.updatePerfetto.asArg(),
+          '${BuildCommandArgs.buildMode.asArg()}=$devToolsAppBuildMode',
+          BuildCommandArgs.pubGet.asArg(negated: !runPubGet),
+        ]),
       );
       if (process.exitCode == 1) {
         throw Exception(
@@ -146,7 +161,7 @@
 
     logStatus('running pub get for DDS in the local dart sdk');
     await processManager.runProcess(
-      CliCommand.from('dart', ['pub', 'get']),
+      CliCommand.dart(['pub', 'get']),
       workingDirectory: path.join(localDartSdkLocation, 'pkg', 'dds'),
     );
 
@@ -161,9 +176,12 @@
 
     // This call will not exit until explicitly terminated by the user.
     await processManager.runProcess(
-      CliCommand.from(
-        'dart',
+      CliCommand.dart(
         [
+          if (debugServer) ...[
+            'run',
+            '--observe=0',
+          ],
           serveLocalScriptPath,
           '--devtools-build=$devToolsBuildLocation',
           // Pass any args that were provided to our script along. This allows IDEs
diff --git a/tool/lib/commands/sync.dart b/tool/lib/commands/sync.dart
index 5f4e20f..f1c35ab 100644
--- a/tool/lib/commands/sync.dart
+++ b/tool/lib/commands/sync.dart
@@ -19,10 +19,10 @@
   Future run() async {
     final processManager = ProcessManager();
     await processManager.runProcess(
-      CliCommand.from('git', ['pull', 'upstream', 'master']),
+      CliCommand.git(['pull', 'upstream', 'master']),
     );
     await processManager.runProcess(
-      CliCommand.tool('generate-code --upgrade'),
+      CliCommand.tool(['generate-code', '--upgrade']),
     );
   }
 }
diff --git a/tool/lib/commands/tag_version.dart b/tool/lib/commands/tag_version.dart
new file mode 100644
index 0000000..0f44042
--- /dev/null
+++ b/tool/lib/commands/tag_version.dart
@@ -0,0 +1,53 @@
+// Copyright 2024 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:io';
+
+import 'package:args/command_runner.dart';
+import 'package:cli_util/cli_logging.dart';
+import 'package:devtools_tool/model.dart';
+import 'package:io/io.dart';
+import 'package:path/path.dart' as path;
+import 'package:yaml/yaml.dart';
+import '../utils.dart';
+
+class TagVersionCommand extends Command {
+  @override
+  String get name => 'tag-version';
+
+  @override
+  String get description =>
+      'Creates a git tag for the current version of DevTools and pushes it to '
+      'the DevTools Github repository.';
+
+  @override
+  Future run() async {
+    final log = Logger.standard();
+
+    final repo = DevToolsRepo.getInstance();
+    final devtoolsAppPubspecPath =
+        path.join(repo.devtoolsAppDirectoryPath, 'pubspec.yaml');
+    final devtoolsAppPubspec = File(devtoolsAppPubspecPath);
+    if (!devtoolsAppPubspec.existsSync()) {
+      throw FileSystemException(
+        'Could not find pubspec.yaml file at: $devtoolsAppPubspecPath',
+      );
+    }
+
+    final yamlMap = loadYaml(devtoolsAppPubspec.readAsStringSync()) as YamlMap;
+    final version = yamlMap['version'].toString();
+    log.stdout('Current DevTools version: $version');
+
+    final processManager = ProcessManager();
+
+    final gitTag = 'v$version';
+    log.stdout('Creating git tag: $gitTag');
+    await processManager.runAll(
+      commands: [
+        CliCommand.git(['tag', '-a', gitTag, '-m', 'DevTools $version']),
+        CliCommand.git(['push', 'upstream', gitTag]),
+      ],
+    );
+  }
+}
diff --git a/tool/lib/commands/update_dart_sdk_deps.dart b/tool/lib/commands/update_dart_sdk_deps.dart
index 0ada0ae..56bd7e9 100644
--- a/tool/lib/commands/update_dart_sdk_deps.dart
+++ b/tool/lib/commands/update_dart_sdk_deps.dart
@@ -50,10 +50,10 @@
       additionalErrorMessage: DartSdkHelper.commandDebugMessage,
       commands: [
         CliCommand.git(
-          cmd: 'branch -D devtools-$commit',
+          ['branch', '-D', 'devtools-$commit'],
           throwOnException: false,
         ),
-        CliCommand.git(cmd: 'new-branch devtools-$commit'),
+        CliCommand.git(['new-branch', 'devtools-$commit']),
       ],
     );
 
@@ -65,16 +65,15 @@
       workingDirectory: dartSdkLocation,
       additionalErrorMessage: DartSdkHelper.commandDebugMessage,
       commands: [
-        CliCommand.git(cmd: 'add DEPS'),
-        CliCommand.from(
-          'git',
+        CliCommand.git(['add', 'DEPS']),
+        CliCommand.git(
           [
             'commit',
             '-m',
             'Update DevTools rev to $commit',
           ],
         ),
-        CliCommand.git(cmd: 'cl upload -s -f'),
+        CliCommand.git(['cl', 'upload', '-s', '-f']),
       ],
     );
   }
diff --git a/tool/lib/commands/update_flutter_sdk.dart b/tool/lib/commands/update_flutter_sdk.dart
index 375ea85..57f39e2 100644
--- a/tool/lib/commands/update_flutter_sdk.dart
+++ b/tool/lib/commands/update_flutter_sdk.dart
@@ -85,7 +85,7 @@
           'tags/${repo.readFile(Uri.parse('flutter-candidate.txt')).trim()}';
     } else {
       flutterTag = (await processManager.runProcess(
-        CliCommand('sh latest_flutter_candidate.sh'),
+        CliCommand('sh', ['latest_flutter_candidate.sh']),
         workingDirectory: repo.toolDirectoryPath,
       ))
           .stdout
@@ -116,12 +116,12 @@
 
       await processManager.runAll(
         commands: [
-          CliCommand.git(cmd: 'stash'),
-          CliCommand.git(cmd: 'fetch upstream'),
-          CliCommand.git(cmd: 'checkout upstream/master'),
-          CliCommand.git(cmd: 'reset --hard upstream/master'),
-          CliCommand.git(cmd: 'checkout $flutterTag -f'),
-          CliCommand.flutter('--version'),
+          CliCommand.git(['stash']),
+          CliCommand.git(['fetch', 'upstream']),
+          CliCommand.git(['checkout', 'upstream/master']),
+          CliCommand.git(['reset', '--hard', 'upstream/master']),
+          CliCommand.git(['checkout', flutterTag, '-f']),
+          CliCommand.flutter(['--version']),
         ],
         workingDirectory: pathSdk.sdkPath,
       );
@@ -133,9 +133,9 @@
       log.stdout('Updating Flutter at $toolSdkPath');
       await processManager.runAll(
         commands: [
-          CliCommand.git(cmd: 'fetch'),
-          CliCommand.git(cmd: 'checkout $flutterTag -f'),
-          CliCommand.flutter('--version'),
+          CliCommand.git(['fetch']),
+          CliCommand.git(['checkout', flutterTag, '-f']),
+          CliCommand.flutter(['--version']),
         ],
         workingDirectory: toolSdkPath,
       );
@@ -143,14 +143,14 @@
       log.stdout('Cloning Flutter into $toolSdkPath');
       await processManager.runProcess(
         CliCommand.git(
-          cmd: 'clone https://github.com/flutter/flutter $flutterSdkDirName',
+          ['clone', 'https://github.com/flutter/flutter', flutterSdkDirName],
         ),
         workingDirectory: repo.toolDirectoryPath,
       );
       await processManager.runAll(
         commands: [
-          CliCommand.git(cmd: 'checkout $flutterTag -f'),
-          CliCommand.flutter('--version'),
+          CliCommand.git(['checkout', flutterTag, '-f']),
+          CliCommand.flutter(['--version']),
         ],
         workingDirectory: toolSdkPath,
       );
diff --git a/tool/lib/commands/update_perfetto.dart b/tool/lib/commands/update_perfetto.dart
index 8fc6715..a603687 100644
--- a/tool/lib/commands/update_perfetto.dart
+++ b/tool/lib/commands/update_perfetto.dart
@@ -76,8 +76,10 @@
           Directory.systemTemp.createTempSync('perfetto_clone');
       await processManager.runProcess(
         CliCommand.git(
-          cmd:
-              'clone https://android.googlesource.com/platform/external/perfetto',
+          [
+            'clone',
+            'https://android.googlesource.com/platform/external/perfetto',
+          ],
         ),
         workingDirectory: tempPerfettoClone.path,
       );
@@ -85,8 +87,8 @@
       logStatus('installing build deps and building the Perfetto UI');
       await processManager.runAll(
         commands: [
-          CliCommand('${path.join('tools', 'install-build-deps')} --ui'),
-          CliCommand(path.join('ui', 'build')),
+          CliCommand(path.join('tools', 'install-build-deps'), ['--ui']),
+          CliCommand(path.join('ui', 'build'), []),
         ],
         workingDirectory: path.join(tempPerfettoClone.path, 'perfetto'),
       );
diff --git a/tool/lib/devtools_command_runner.dart b/tool/lib/devtools_command_runner.dart
index ab17cf1..58c6b52 100644
--- a/tool/lib/devtools_command_runner.dart
+++ b/tool/lib/devtools_command_runner.dart
@@ -9,6 +9,7 @@
 import 'package:devtools_tool/commands/generate_code.dart';
 import 'package:devtools_tool/commands/serve.dart';
 import 'package:devtools_tool/commands/sync.dart';
+import 'package:devtools_tool/commands/tag_version.dart';
 import 'package:devtools_tool/commands/update_flutter_sdk.dart';
 import 'package:devtools_tool/commands/update_perfetto.dart';
 import 'package:devtools_tool/model.dart';
@@ -38,6 +39,7 @@
     addCommand(RollbackCommand());
     addCommand(ServeCommand());
     addCommand(SyncCommand());
+    addCommand(TagVersionCommand());
     addCommand(UpdateDartSdkDepsCommand());
     addCommand(UpdateDevToolsVersionCommand());
     addCommand(UpdateFlutterSdkCommand());
diff --git a/tool/lib/model.dart b/tool/lib/model.dart
index df89291..0191d71 100644
--- a/tool/lib/model.dart
+++ b/tool/lib/model.dart
@@ -206,10 +206,10 @@
   static String get dartWrapperExecutableName =>
       Platform.isWindows ? 'dart.bat' : 'dart';
 
-  String get flutterToolPath =>
+  String get flutterExePath =>
       path.join(sdkPath, 'bin', flutterExecutableName);
 
-  String get dartToolPath =>
+  String get dartExePath =>
       path.join(sdkPath, 'bin', dartWrapperExecutableName);
 
   String get dartSdkPath => path.join(sdkPath, 'bin', 'cache', 'dart-sdk');
diff --git a/tool/lib/utils.dart b/tool/lib/utils.dart
index 3a5a428..2776186 100644
--- a/tool/lib/utils.dart
+++ b/tool/lib/utils.dart
@@ -22,9 +22,9 @@
       workingDirectory: dartSdkLocation,
       additionalErrorMessage: commandDebugMessage,
       commands: [
-        CliCommand.git(cmd: 'fetch origin'),
-        CliCommand.git(cmd: 'rebase-update'),
-        CliCommand.git(cmd: 'checkout origin/main'),
+        CliCommand.git(['fetch', 'origin']),
+        CliCommand.git(['rebase-update']),
+        CliCommand.git(['checkout', 'origin/main']),
       ],
     );
   }
@@ -41,90 +41,63 @@
 }
 
 class CliCommand {
-  CliCommand._({
-    String? command,
-    String? exe,
-    List<String>? args,
-    this.throwOnException = true,
-  }) {
-    assert((command == null) != ((exe == null) && (args == null)));
-    final commandElements = command?.split(' ');
-    this.exe = exe ?? commandElements!.first;
-    this.args = args ?? commandElements!.sublist(1);
-  }
-
   CliCommand(
-    String command, {
+    this.exe,
+    // Args is mandatory to make it clearer to the caller that they should
+    // not be passing a full exe+args into the first string argument, because
+    // this can lead to bugs if paths have spaces and everything is not escaped.
+    this.args, {
     this.throwOnException = true,
-  })  : exe = command.split(' ').first,
-        args = command.split(' ').sublist(1);
+  });
 
-  factory CliCommand.from(
-    String exe,
+  factory CliCommand.flutter(
     List<String> args, {
     bool throwOnException = true,
   }) {
-    return CliCommand._(
-      exe: exe,
-      args: args,
+    return CliCommand(
+      FlutterSdk.current.flutterExePath,
+      args,
       throwOnException: throwOnException,
     );
   }
 
-  factory CliCommand.flutter(
-    String args, {
+  factory CliCommand.dart(
+    List<String> args, {
     bool throwOnException = true,
   }) {
-    final sdk = FlutterSdk.current;
-    return CliCommand._(
-      exe: sdk.flutterToolPath,
-      args: args.split(' '),
+    return CliCommand(
+      FlutterSdk.current.dartExePath,
+      args,
       throwOnException: throwOnException,
     );
   }
 
   /// CliCommand helper for running git commands.
-  ///
-  /// Arguments can be passed in as a single string using [cmd], this will split
-  /// the string into args using spaces. e.g. CliCommand.git(cmd: 'checkout test-branch')
-  ///
-  /// If you instead want to specify args explicitly, you can use the
-  /// [args] param. e.g. CliCommand.git(args: ['checkout', 'test-branch'])
-  factory CliCommand.git({
-    String? cmd,
-    List<String>? args,
+  factory CliCommand.git(
+    List<String> args, {
     bool throwOnException = true,
-    bool split = true,
   }) {
-    if ((cmd == null) == (args == null)) {
-      throw ('Only one of `cmd` and `args` must be specified.');
-    }
-
-    if (cmd != null) {
-      args = cmd.split(' ');
-    }
-
-    return CliCommand._(
-      exe: 'git',
-      args: args,
+    return CliCommand(
+      'git',
+      args,
       throwOnException: throwOnException,
     );
   }
 
   factory CliCommand.tool(
-    String args, {
+    List<String> args, {
     bool throwOnException = true,
   }) {
-    return CliCommand._(
+    return CliCommand(
       // We must use the Dart VM from FlutterSdk.current here to ensure we
       // consistently use the selected version for child invocations. We do
       // not need to pass the --flutter-from-path flag down because using the
       // tool will automatically select the one that's running the VM and we'll
       // have selected that here.
-      exe: FlutterSdk.current.dartToolPath,
-      args: [
+      FlutterSdk.current.dartExePath,
+      [
         Platform.script.toFilePath(),
-        ...args.split(' '),
+        ...args,
       ],
       throwOnException: throwOnException,
     );
@@ -209,7 +182,7 @@
 }) async {
   print('Searching for a remote that points to $remoteId.');
   final remotesResult = await processManager.runProcess(
-    CliCommand.git(cmd: 'remote -v'),
+    CliCommand.git(['remote', '-v']),
     workingDirectory: workingDirectory,
   );
   final String remotes = remotesResult.stdout;
diff --git a/tool/pubspec.yaml b/tool/pubspec.yaml
index 0a5d8ae..a8e3da4 100644
--- a/tool/pubspec.yaml
+++ b/tool/pubspec.yaml
@@ -3,20 +3,19 @@
 publish_to: none
 
 environment:
-  sdk: ">=3.0.0 <4.0.0"
+  sdk: ^3.2.0
 
 executables:
   devtools_tool: devtools_tool
 
 dependencies:
-  args: ^2.1.0
-  cli_util: ^0.3.3
-  devtools_shared: ^6.0.1
-  http: ^0.13.3
+  args: ^2.4.2
+  cli_util: ^0.4.1
   io: ^1.0.4
-  lints: any
-  path: ^1.8.0
+  path: ^1.9.0
+  yaml: ^3.1.2
 
 dependency_overrides:
   devtools_shared:
     path: ../packages/devtools_shared
+  lints: ^3.0.0
diff --git a/tool/tag_version.sh b/tool/tag_version.sh
deleted file mode 100755
index d3156e0..0000000
--- a/tool/tag_version.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-CURRENT_VERSION="$(sed -n -e 's/^.*version: //p' packages/devtools_app/pubspec.yaml)"
-
-echo "Tagging version: $CURRENT_VERSION"
-set -x #echo on
-
-git tag -a v$CURRENT_VERSION -m "DevTools $CURRENT_VERSION"
-
-git push upstream v$CURRENT_VERSION