Skip to content

Commit 2b7d709

Browse files
authored
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
1 parent c6b636f commit 2b7d709

File tree

2 files changed

+93
-5
lines changed

2 files changed

+93
-5
lines changed

packages/flutter/lib/src/widgets/widget_inspector.dart

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import 'dart:ui' as ui
2323
import 'package:flutter/foundation.dart';
2424
import 'package:flutter/rendering.dart';
2525
import 'package:flutter/scheduler.dart';
26+
import 'package:meta/meta_meta.dart';
2627

2728
import 'app.dart';
2829
import 'basic.dart';
@@ -3684,3 +3685,68 @@ class InspectorSerializationDelegate implements DiagnosticsSerializationDelegate
36843685
);
36853686
}
36863687
}
3688+
3689+
@Target(<TargetKind>{TargetKind.method})
3690+
class _WidgetFactory {
3691+
const _WidgetFactory();
3692+
}
3693+
3694+
/// Annotation which marks a function as a widget factory for the purpose of
3695+
/// widget creation tracking.
3696+
///
3697+
/// When widget creation tracking is enabled, the framework tracks the source
3698+
/// code location of the constructor call for each widget instance. This
3699+
/// information is used by the DevTools to provide an improved developer
3700+
/// experience. For example, it allows the Flutter inspector to present the
3701+
/// widget tree in a manner similar to how the UI was defined in your source
3702+
/// code.
3703+
///
3704+
/// [Widget] constructors are automatically instrumented to track the source
3705+
/// code location of constructor calls. However, there are cases where
3706+
/// a function acts as a sort of a constructor for a widget and a call to such
3707+
/// a function should be considered as the creation location for the returned
3708+
/// widget instance.
3709+
///
3710+
/// Annotating a function with this annotation marks the function as a widget
3711+
/// factory. The framework will then instrument that function in the same way
3712+
/// as it does for [Widget] constructors.
3713+
///
3714+
/// Note that the function **must not** have optional positional parameters for
3715+
/// tracking to work correctly.
3716+
///
3717+
/// Currently this annotation is only supported on extension methods.
3718+
///
3719+
/// {@tool snippet}
3720+
///
3721+
/// This example shows how to use the [widgetFactory] annotation to mark an
3722+
/// extension method as a widget factory:
3723+
///
3724+
/// ```dart
3725+
/// extension PaddingModifier on Widget {
3726+
/// @widgetFactory
3727+
/// Widget padding(EdgeInsetsGeometry padding) {
3728+
/// return Padding(padding: padding, child: this);
3729+
/// }
3730+
/// }
3731+
/// ```
3732+
///
3733+
/// When using the above extension method, the framework will track the
3734+
/// creation location of the [Padding] widget instance as the source code
3735+
/// location where the `padding` extension method was called:
3736+
///
3737+
/// ```dart
3738+
/// // continuing from previous example...
3739+
/// const Text('Hello World!')
3740+
/// .padding(const EdgeInsets.all(8));
3741+
/// ```
3742+
///
3743+
/// {@end-tool}
3744+
///
3745+
/// See also:
3746+
///
3747+
/// * the documentation for [Track widget creation](https://docs.flutter.dev/development/tools/devtools/inspector#track-widget-creation).
3748+
// The below ignore is needed because the static type of the annotation is used
3749+
// by the CFE kernel transformer that implements the instrumentation to
3750+
// recognize the annotation.
3751+
// ignore: library_private_types_in_public_api
3752+
const _WidgetFactory widgetFactory = _WidgetFactory();

packages/flutter/test/widgets/widget_inspector_test.dart

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ int getChildLayerCount(OffsetLayer layer) {
238238
return count;
239239
}
240240

241+
extension TextFromString on String {
242+
@widgetFactory
243+
Widget text() {
244+
return Text(this);
245+
}
246+
}
247+
241248
void main() {
242249
_TestWidgetInspectorService.runTests();
243250
}
@@ -944,19 +951,20 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
944951

945952
testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
946953
await tester.pumpWidget(
947-
const Directionality(
954+
Directionality(
948955
textDirection: TextDirection.ltr,
949956
child: Stack(
950957
children: <Widget>[
951-
Text('a'),
952-
Text('b', textDirection: TextDirection.ltr),
953-
Text('c', textDirection: TextDirection.ltr),
958+
const Text('a'),
959+
const Text('b', textDirection: TextDirection.ltr),
960+
'c'.text(),
954961
],
955962
),
956963
),
957964
);
958965
final Element elementA = find.text('a').evaluate().first;
959966
final Element elementB = find.text('b').evaluate().first;
967+
final Element elementC = find.text('c').evaluate().first;
960968

961969
service.disposeAllGroups();
962970
service.resetPubRootDirectories();
@@ -979,14 +987,28 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
979987
final int columnB = creationLocationB['column']! as int;
980988
final String? nameB = creationLocationB['name'] as String?;
981989
expect(nameB, equals('Text'));
990+
991+
service.setSelection(elementC, 'my-group');
992+
final Map<String, Object?> jsonC = json.decode(service.getSelectedWidget(null, 'my-group')) as Map<String, Object?>;
993+
final Map<String, Object?> creationLocationC = jsonC['creationLocation']! as Map<String, Object?>;
994+
expect(creationLocationC, isNotNull);
995+
final String fileC = creationLocationC['file']! as String;
996+
final int lineC = creationLocationC['line']! as int;
997+
final int columnC = creationLocationC['column']! as int;
998+
final String? nameC = creationLocationC['name'] as String?;
999+
expect(nameC, equals('TextFromString|text'));
1000+
9821001
expect(fileA, endsWith('widget_inspector_test.dart'));
9831002
expect(fileA, equals(fileB));
1003+
expect(fileA, equals(fileC));
9841004
// We don't hardcode the actual lines the widgets are created on as that
9851005
// would make this test fragile.
9861006
expect(lineA + 1, equals(lineB));
1007+
expect(lineB + 1, equals(lineC));
9871008
// Column numbers are more stable than line numbers.
988-
expect(columnA, equals(15));
1009+
expect(columnA, equals(21));
9891010
expect(columnA, equals(columnB));
1011+
expect(columnC, equals(19));
9901012
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
9911013

9921014
testWidgets('WidgetInspectorService setSelection notifiers for an Element',

0 commit comments

Comments
 (0)