diff --git a/packages/devtools_app/lib/devtools.dart b/packages/devtools_app/lib/devtools.dart index 199647bc197..44658c325e1 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 36e3406bc0a..ff9deef67f9 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 @@ class _AppSizeBodyState extends State testAppSizeFile = await server.requestTestAppSizeFile(testFilePath); } - // TODO(kenz): add error handling if the files are null + final errorMessages = [ + 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 @@ class DiffTreeTypeDropdown extends StatelessWidget { } } +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 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 createState() => _AnalysisViewState(); } @@ -382,7 +452,7 @@ class _AnalysisViewState extends State with AutoDisposeMixin { children: [ Flexible( child: FileImportContainer( - instructions: AnalysisView.importInstructions, + instructionsWidget: const _ImportInstructions(), actionText: 'Analyze Size', gaScreen: gac.appSize, gaSelectionImport: gac.importFileSingle, @@ -407,15 +477,6 @@ class _AnalysisViewState extends State with AutoDisposeMixin { 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 createState() => _DiffViewState(); } @@ -490,9 +551,12 @@ class _DiffViewState extends State with AutoDisposeMixin { 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 3ba61c40715..3a425f6cc6d 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 __DropdownSaveButtonState extends State<_DropdownSaveButton> { 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 @@ class FileImportContainer extends StatefulWidget { 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 @@ class _FileImportContainerState extends State { 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 @@ class DualFileImportContainer extends StatefulWidget { 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 @@ class DualFileImportContainer extends StatefulWidget { 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 @@ class _DualFileImportContainerState extends State { title: widget.firstFileTitle, backgroundColor: backgroundColor, instructions: widget.firstInstructions, + instructionsWidget: widget.firstInstructionsWidget, onFileSelected: onFirstFileSelected, onFileCleared: onFirstFileCleared, gaScreen: widget.gaScreen, @@ -539,6 +550,7 @@ class _DualFileImportContainerState extends State { 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 17dbf52a81b..ce47ca06774 100644 --- a/packages/devtools_app/pubspec.yaml +++ b/packages/devtools_app/pubspec.yaml @@ -7,7 +7,7 @@ publish_to: none # 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 b98ca752ea0..9963ddc8ee3 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 @@ found in the LICENSE file or at https://developers.google.com/open-source/licens 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 @@ TODO: Remove this section if there are not any updates. ## 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 @@ TODO: Remove this section if there are not any updates. ## 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 @@ TODO: Remove this section if there are not any 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.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 695439d3f8c..135127828e7 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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)); }); });