Unverified Commit 2b7d709f authored by Gabriel Terwesten's avatar Gabriel Terwesten Committed by GitHub

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
parent c6b636fa
...@@ -23,6 +23,7 @@ import 'dart:ui' as ui ...@@ -23,6 +23,7 @@ import 'dart:ui' as ui
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:meta/meta_meta.dart';
import 'app.dart'; import 'app.dart';
import 'basic.dart'; import 'basic.dart';
...@@ -3684,3 +3685,68 @@ class InspectorSerializationDelegate implements DiagnosticsSerializationDelegate ...@@ -3684,3 +3685,68 @@ class InspectorSerializationDelegate implements DiagnosticsSerializationDelegate
); );
} }
} }
@Target(<TargetKind>{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();
...@@ -238,6 +238,13 @@ int getChildLayerCount(OffsetLayer layer) { ...@@ -238,6 +238,13 @@ int getChildLayerCount(OffsetLayer layer) {
return count; return count;
} }
extension TextFromString on String {
@widgetFactory
Widget text() {
return Text(this);
}
}
void main() { void main() {
_TestWidgetInspectorService.runTests(); _TestWidgetInspectorService.runTests();
} }
...@@ -944,19 +951,20 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -944,19 +951,20 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async { testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
Text('a'), const Text('a'),
Text('b', textDirection: TextDirection.ltr), const Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr), 'c'.text(),
], ],
), ),
), ),
); );
final Element elementA = find.text('a').evaluate().first; final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first; final Element elementB = find.text('b').evaluate().first;
final Element elementC = find.text('c').evaluate().first;
service.disposeAllGroups(); service.disposeAllGroups();
service.resetPubRootDirectories(); service.resetPubRootDirectories();
...@@ -979,14 +987,28 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -979,14 +987,28 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
final int columnB = creationLocationB['column']! as int; final int columnB = creationLocationB['column']! as int;
final String? nameB = creationLocationB['name'] as String?; final String? nameB = creationLocationB['name'] as String?;
expect(nameB, equals('Text')); expect(nameB, equals('Text'));
service.setSelection(elementC, 'my-group');
final Map<String, Object?> jsonC = json.decode(service.getSelectedWidget(null, 'my-group')) as Map<String, Object?>;
final Map<String, Object?> creationLocationC = jsonC['creationLocation']! as Map<String, Object?>;
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, endsWith('widget_inspector_test.dart'));
expect(fileA, equals(fileB)); expect(fileA, equals(fileB));
expect(fileA, equals(fileC));
// We don't hardcode the actual lines the widgets are created on as that // We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile. // would make this test fragile.
expect(lineA + 1, equals(lineB)); expect(lineA + 1, equals(lineB));
expect(lineB + 1, equals(lineC));
// Column numbers are more stable than line numbers. // Column numbers are more stable than line numbers.
expect(columnA, equals(15)); expect(columnA, equals(21));
expect(columnA, equals(columnB)); expect(columnA, equals(columnB));
expect(columnC, equals(19));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag. }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
testWidgets('WidgetInspectorService setSelection notifiers for an Element', testWidgets('WidgetInspectorService setSelection notifiers for an Element',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment