From 2b7d709fdc8223668182ce15e39cc76e5ec2c61d Mon Sep 17 00:00:00 2001 From: Gabriel Terwesten Date: Wed, 15 Feb 2023 20:10:58 +0100 Subject: [PATCH] Add `@widgetFactory` annotation (#117455) * Add `@widgetFactory` annotation * Simplify and fix example * Specify `TargetKind`s for `widgetFactory` * Explain why `library_private_types_in_public_api` is ignored. * Trigger CI --- .../lib/src/widgets/widget_inspector.dart | 66 +++++++++++++++++++ .../test/widgets/widget_inspector_test.dart | 32 +++++++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index fb854f16f1..83a28d9726 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -23,6 +23,7 @@ import 'dart:ui' as ui import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'package:meta/meta_meta.dart'; import 'app.dart'; import 'basic.dart'; @@ -3684,3 +3685,68 @@ class InspectorSerializationDelegate implements DiagnosticsSerializationDelegate ); } } + +@Target({TargetKind.method}) +class _WidgetFactory { + const _WidgetFactory(); +} + +/// Annotation which marks a function as a widget factory for the purpose of +/// widget creation tracking. +/// +/// When widget creation tracking is enabled, the framework tracks the source +/// code location of the constructor call for each widget instance. This +/// information is used by the DevTools to provide an improved developer +/// experience. For example, it allows the Flutter inspector to present the +/// widget tree in a manner similar to how the UI was defined in your source +/// code. +/// +/// [Widget] constructors are automatically instrumented to track the source +/// code location of constructor calls. However, there are cases where +/// a function acts as a sort of a constructor for a widget and a call to such +/// a function should be considered as the creation location for the returned +/// widget instance. +/// +/// Annotating a function with this annotation marks the function as a widget +/// factory. The framework will then instrument that function in the same way +/// as it does for [Widget] constructors. +/// +/// Note that the function **must not** have optional positional parameters for +/// tracking to work correctly. +/// +/// Currently this annotation is only supported on extension methods. +/// +/// {@tool snippet} +/// +/// This example shows how to use the [widgetFactory] annotation to mark an +/// extension method as a widget factory: +/// +/// ```dart +/// extension PaddingModifier on Widget { +/// @widgetFactory +/// Widget padding(EdgeInsetsGeometry padding) { +/// return Padding(padding: padding, child: this); +/// } +/// } +/// ``` +/// +/// When using the above extension method, the framework will track the +/// creation location of the [Padding] widget instance as the source code +/// location where the `padding` extension method was called: +/// +/// ```dart +/// // continuing from previous example... +/// const Text('Hello World!') +/// .padding(const EdgeInsets.all(8)); +/// ``` +/// +/// {@end-tool} +/// +/// See also: +/// +/// * the documentation for [Track widget creation](https://docs.flutter.dev/development/tools/devtools/inspector#track-widget-creation). +// The below ignore is needed because the static type of the annotation is used +// by the CFE kernel transformer that implements the instrumentation to +// recognize the annotation. +// ignore: library_private_types_in_public_api +const _WidgetFactory widgetFactory = _WidgetFactory(); diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index e6b4a71847..216a6c8039 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -238,6 +238,13 @@ int getChildLayerCount(OffsetLayer layer) { return count; } +extension TextFromString on String { + @widgetFactory + Widget text() { + return Text(this); + } +} + void main() { _TestWidgetInspectorService.runTests(); } @@ -944,19 +951,20 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async { await tester.pumpWidget( - const Directionality( + Directionality( textDirection: TextDirection.ltr, child: Stack( children: [ - Text('a'), - Text('b', textDirection: TextDirection.ltr), - Text('c', textDirection: TextDirection.ltr), + const Text('a'), + const Text('b', textDirection: TextDirection.ltr), + 'c'.text(), ], ), ), ); final Element elementA = find.text('a').evaluate().first; final Element elementB = find.text('b').evaluate().first; + final Element elementC = find.text('c').evaluate().first; service.disposeAllGroups(); service.resetPubRootDirectories(); @@ -979,14 +987,28 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { final int columnB = creationLocationB['column']! as int; final String? nameB = creationLocationB['name'] as String?; expect(nameB, equals('Text')); + + service.setSelection(elementC, 'my-group'); + final Map jsonC = json.decode(service.getSelectedWidget(null, 'my-group')) as Map; + final Map creationLocationC = jsonC['creationLocation']! as Map; + expect(creationLocationC, isNotNull); + final String fileC = creationLocationC['file']! as String; + final int lineC = creationLocationC['line']! as int; + final int columnC = creationLocationC['column']! as int; + final String? nameC = creationLocationC['name'] as String?; + expect(nameC, equals('TextFromString|text')); + expect(fileA, endsWith('widget_inspector_test.dart')); expect(fileA, equals(fileB)); + expect(fileA, equals(fileC)); // We don't hardcode the actual lines the widgets are created on as that // would make this test fragile. expect(lineA + 1, equals(lineB)); + expect(lineB + 1, equals(lineC)); // Column numbers are more stable than line numbers. - expect(columnA, equals(15)); + expect(columnA, equals(21)); expect(columnA, equals(columnB)); + expect(columnC, equals(19)); }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag. testWidgets('WidgetInspectorService setSelection notifiers for an Element',