Add documentation links to app size screen and handle null files (#9689)
diff --git a/packages/devtools_app/lib/devtools.dart b/packages/devtools_app/lib/devtools.dart
index 199647b..44658c3 100644
--- a/packages/devtools_app/lib/devtools.dart
+++ b/packages/devtools_app/lib/devtools.dart
@@ -10,4 +10,4 @@
 /// Note: a regexp in the `dt update-version' command logic matches the constant
 /// declaration `const version =`. If you change the declaration you must also
 /// modify the regex in the `dt update-version' command logic.
-const version = '2.56.0';
+const version = '2.57.0-dev.0';
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 36e3406..ff9deef 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
@@ -116,7 +116,14 @@
         testAppSizeFile = await server.requestTestAppSizeFile(testFilePath);
       }
 
-      // TODO(kenz): add error handling if the files are null
+      final errorMessages = <String>[
+        if (baseAppSizeFile == null) 'base app size file: $baseFilePath',
+        if (testAppSizeFile == null) 'test app size file: $testFilePath',
+      ];
+      if (errorMessages.isNotEmpty) {
+        _pushErrorMessage('Failed to load ${errorMessages.join(' and ')}.');
+      }
+
       if (baseAppSizeFile != null) {
         if (testAppSizeFile != null) {
           controller.loadDiffTreeFromJsonFiles(
@@ -306,15 +313,78 @@
   }
 }
 
+enum ImportInstructionType { analysis, diffOld, diffNew }
+
+class _ImportInstructions extends StatelessWidget {
+  const _ImportInstructions({this.type = ImportInstructionType.analysis});
+
+  final ImportInstructionType type;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+
+    List<InlineSpan> buildInstructionText() {
+      final article = type == ImportInstructionType.diffNew ? 'a' : 'an';
+      final boldText = type == ImportInstructionType.diffOld
+          ? 'original (old)'
+          : type == ImportInstructionType.diffNew
+          ? 'modified (new)'
+          : null;
+
+      return [
+        TextSpan(
+          text: 'Drag and drop $article ',
+          style: theme.regularTextStyle,
+        ),
+        if (boldText != null)
+          TextSpan(text: boldText, style: theme.boldTextStyle),
+        TextSpan(
+          text:
+              '${boldText != null ? ' ' : ''}AOT snapshot or size analysis file'
+              ' for debugging, or click "Open file".',
+          style: theme.regularTextStyle,
+        ),
+      ];
+    }
+
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        RichText(
+          textAlign: TextAlign.center,
+          text: TextSpan(children: buildInstructionText()),
+        ),
+        const SizedBox(height: densePadding),
+        RichText(
+          textAlign: TextAlign.center,
+          text: TextSpan(
+            children: [
+              TextSpan(text: 'Read ', style: theme.regularTextStyle),
+              LinkTextSpan(
+                link: const Link(
+                  display: 'documentation',
+                  url:
+                      'https://docs.flutter.dev/tools/devtools/app-size#generating-size-files',
+                ),
+                context: context,
+              ),
+              TextSpan(
+                text: ' to learn how to generate these files.',
+                style: theme.regularTextStyle,
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
+
 class AnalysisView extends StatefulWidget {
   const AnalysisView({super.key});
 
-  // TODO(kenz): add links to documentation on how to generate these files, and
-  // mention the import file button once it is hooked up to a file picker.
-  static const importInstructions =
-      'Drag and drop an AOT snapshot or'
-      ' size analysis file for debugging';
-
   @override
   State<AnalysisView> createState() => _AnalysisViewState();
 }
@@ -382,7 +452,7 @@
                 children: [
                   Flexible(
                     child: FileImportContainer(
-                      instructions: AnalysisView.importInstructions,
+                      instructionsWidget: const _ImportInstructions(),
                       actionText: 'Analyze Size',
                       gaScreen: gac.appSize,
                       gaSelectionImport: gac.importFileSingle,
@@ -407,15 +477,6 @@
 class DiffView extends StatefulWidget {
   const DiffView({super.key});
 
-  // TODO(kenz): add links to documentation on how to generate these files, and
-  // mention the import file button once it is hooked up to a file picker.
-  static const importOldInstructions =
-      'Drag and drop an original (old) AOT '
-      'snapshot or size analysis file for debugging';
-  static const importNewInstructions =
-      'Drag and drop a modified (new) AOT '
-      'snapshot or size analysis file for debugging';
-
   @override
   State<DiffView> createState() => _DiffViewState();
 }
@@ -490,9 +551,12 @@
                     child: DualFileImportContainer(
                       firstFileTitle: 'Old',
                       secondFileTitle: 'New',
-                      // TODO(kenz): perhaps bold "original" and "modified".
-                      firstInstructions: DiffView.importOldInstructions,
-                      secondInstructions: DiffView.importNewInstructions,
+                      firstInstructionsWidget: const _ImportInstructions(
+                        type: ImportInstructionType.diffOld,
+                      ),
+                      secondInstructionsWidget: const _ImportInstructions(
+                        type: ImportInstructionType.diffNew,
+                      ),
                       actionText: 'Analyze Diff',
                       gaScreen: gac.appSize,
                       gaSelectionImportFirst: gac.importFileDiffFirst,
diff --git a/packages/devtools_app/lib/src/shared/ui/file_import.dart b/packages/devtools_app/lib/src/shared/ui/file_import.dart
index 3ba61c4..3a425f6 100644
--- a/packages/devtools_app/lib/src/shared/ui/file_import.dart
+++ b/packages/devtools_app/lib/src/shared/ui/file_import.dart
@@ -218,9 +218,10 @@
 
 class FileImportContainer extends StatefulWidget {
   const FileImportContainer({
-    required this.instructions,
     required this.gaScreen,
     required this.gaSelectionImport,
+    this.instructions,
+    this.instructionsWidget,
     this.title,
     this.backgroundColor,
     this.gaSelectionAction,
@@ -230,13 +231,15 @@
     this.onFileCleared,
     this.extensions = const ['json'],
     super.key,
-  });
+  }) : assert((instructions == null) != (instructionsWidget == null));
 
   final String? title;
 
   final Color? backgroundColor;
 
-  final String instructions;
+  final String? instructions;
+
+  final Widget? instructionsWidget;
 
   /// The title of the action button.
   final String? actionText;
@@ -276,7 +279,10 @@
           Text(title, style: const TextStyle(fontSize: 18.0)),
           const SizedBox(height: extraLargeSpacing),
         ],
-        CenteredMessage(message: widget.instructions),
+        if (widget.instructionsWidget != null)
+          widget.instructionsWidget!
+        else if (widget.instructions != null)
+          CenteredMessage(message: widget.instructions),
         const SizedBox(height: denseSpacing),
         _buildImportFileRow(),
         if (widget.actionText != null && widget.onAction != null)
@@ -477,8 +483,10 @@
     super.key,
     required this.firstFileTitle,
     required this.secondFileTitle,
-    required this.firstInstructions,
-    required this.secondInstructions,
+    this.firstInstructions,
+    this.secondInstructions,
+    this.firstInstructionsWidget,
+    this.secondInstructionsWidget,
     required this.actionText,
     required this.onAction,
     required this.gaScreen,
@@ -489,8 +497,10 @@
 
   final String firstFileTitle;
   final String secondFileTitle;
-  final String firstInstructions;
-  final String secondInstructions;
+  final String? firstInstructions;
+  final String? secondInstructions;
+  final Widget? firstInstructionsWidget;
+  final Widget? secondInstructionsWidget;
   final String gaScreen;
   final String gaSelectionImportFirst;
   final String gaSelectionImportSecond;
@@ -525,6 +535,7 @@
             title: widget.firstFileTitle,
             backgroundColor: backgroundColor,
             instructions: widget.firstInstructions,
+            instructionsWidget: widget.firstInstructionsWidget,
             onFileSelected: onFirstFileSelected,
             onFileCleared: onFirstFileCleared,
             gaScreen: widget.gaScreen,
@@ -539,6 +550,7 @@
             title: widget.secondFileTitle,
             backgroundColor: backgroundColor,
             instructions: widget.secondInstructions,
+            instructionsWidget: widget.secondInstructionsWidget,
             onFileSelected: onSecondFileSelected,
             onFileCleared: onSecondFileCleared,
             gaScreen: widget.gaScreen,
diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml
index 17dbf52..ce47ca0 100644
--- a/packages/devtools_app/pubspec.yaml
+++ b/packages/devtools_app/pubspec.yaml
@@ -7,7 +7,7 @@
 
 # Note: this version should only be updated by running the 'dt update-version'
 # command that updates the version here and in 'devtools.dart'.
-version: 2.56.0
+version: 2.57.0-dev.0
 
 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app
 
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index b98ca75..9963ddc 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -6,9 +6,9 @@
 This is a draft for future release notes that are going to land on
 [the Flutter website](https://docs.flutter.dev/tools/devtools/release-notes).
 
-# DevTools 2.56.0 release notes
+# DevTools 2.57.0 release notes
 
-The 2.56.0 release of the Dart and Flutter DevTools
+The 2.57.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](/tools/devtools).
@@ -39,7 +39,8 @@
 
 ## Network profiler updates
 
-- Fix crash in the Network tab when viewing binary multipart request or response bodies (#9978)
+- Fix crash in the Network tab when viewing binary multipart request or 
+response bodies. [#9680](https://github.com/flutter/devtools/pull/9680)
 
 ## Logging updates
 
@@ -47,7 +48,7 @@
 
 ## App size tool updates
 
-TODO: Remove this section if there are not any updates.
+- Added documentation links and improved handling for null files. [#9689](https://github.com/flutter/devtools/pull/9689)
 
 ## Deep links tool updates
 
@@ -68,4 +69,4 @@
 ## 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.56.0).
+[DevTools git log](https://github.com/flutter/devtools/tree/v2.57.0).
diff --git a/packages/devtools_app/test/screens/app_size/app_size_screen_test.dart b/packages/devtools_app/test/screens/app_size/app_size_screen_test.dart
index 695439d..1351278 100644
--- a/packages/devtools_app/test/screens/app_size/app_size_screen_test.dart
+++ b/packages/devtools_app/test/screens/app_size/app_size_screen_test.dart
@@ -214,7 +214,9 @@
       expect(find.byType(ClearButton), findsOneWidget);
 
       expect(find.byType(FileImportContainer), findsOneWidget);
-      expect(find.text(AnalysisView.importInstructions), findsOneWidget);
+      const importInstructions =
+          'Drag and drop an AOT snapshot or size analysis file for debugging, or click "Open file".';
+      expect(find.richText(importInstructions), findsOneWidget);
       expect(find.text('No File Selected'), findsOneWidget);
 
       appSizeController.loadTreeFromJsonFile(
@@ -227,7 +229,7 @@
       await tester.pumpAndSettle();
 
       expect(find.byType(FileImportContainer), findsNothing);
-      expect(find.text(AnalysisView.importInstructions), findsNothing);
+      expect(find.richText(importInstructions), findsNothing);
       expect(find.text('No File Selected'), findsNothing);
       expect(find.byType(AnalysisView), findsOneWidget);
       expect(
@@ -261,7 +263,9 @@
       await tester.pumpAndSettle();
 
       expect(find.byType(FileImportContainer), findsOneWidget);
-      expect(find.text(AnalysisView.importInstructions), findsOneWidget);
+      const importInstructions =
+          'Drag and drop an AOT snapshot or size analysis file for debugging, or click "Open file".';
+      expect(find.richText(importInstructions), findsOneWidget);
       expect(find.text('No File Selected'), findsOneWidget);
     });
   });
@@ -302,8 +306,12 @@
 
       expect(find.byType(DualFileImportContainer), findsOneWidget);
       expect(find.byType(FileImportContainer), findsNWidgets(2));
-      expect(find.text(DiffView.importOldInstructions), findsOneWidget);
-      expect(find.text(DiffView.importNewInstructions), findsOneWidget);
+      const importOldInstructions =
+          'Drag and drop an original (old) AOT snapshot or size analysis file for debugging, or click "Open file".';
+      const importNewInstructions =
+          'Drag and drop a modified (new) AOT snapshot or size analysis file for debugging, or click "Open file".';
+      expect(find.richText(importOldInstructions), findsOneWidget);
+      expect(find.richText(importNewInstructions), findsOneWidget);
       expect(find.text('No File Selected'), findsNWidgets(2));
     });
 
@@ -323,8 +331,12 @@
       await tester.pumpAndSettle();
 
       expect(find.byType(FileImportContainer), findsNothing);
-      expect(find.text(DiffView.importOldInstructions), findsNothing);
-      expect(find.text(DiffView.importNewInstructions), findsNothing);
+      const importOldInstructions =
+          'Drag and drop an original (old) AOT snapshot or size analysis file for debugging, or click "Open file".';
+      const importNewInstructions =
+          'Drag and drop a modified (new) AOT snapshot or size analysis file for debugging, or click "Open file".';
+      expect(find.richText(importOldInstructions), findsNothing);
+      expect(find.richText(importNewInstructions), findsNothing);
       expect(find.text('No File Selected'), findsNothing);
 
       expect(find.byType(DiffView), findsOneWidget);
@@ -393,8 +405,12 @@
 
       expect(find.byType(DualFileImportContainer), findsOneWidget);
       expect(find.byType(FileImportContainer), findsNWidgets(2));
-      expect(find.text(DiffView.importOldInstructions), findsOneWidget);
-      expect(find.text(DiffView.importNewInstructions), findsOneWidget);
+      const importOldInstructions =
+          'Drag and drop an original (old) AOT snapshot or size analysis file for debugging, or click "Open file".';
+      const importNewInstructions =
+          'Drag and drop a modified (new) AOT snapshot or size analysis file for debugging, or click "Open file".';
+      expect(find.richText(importOldInstructions), findsOneWidget);
+      expect(find.richText(importNewInstructions), findsOneWidget);
       expect(find.text('No File Selected'), findsNWidgets(2));
     });
   });