Skip to content

Commit

Permalink
Add @widgetFactory annotation (#117455)
Browse files Browse the repository at this point in the history
* Add `@widgetFactory` annotation

* Simplify and fix example

* Specify `TargetKind`s for `widgetFactory`

* Explain why `library_private_types_in_public_api` is ignored.

* Trigger CI
  • Loading branch information
blaugold authored Feb 15, 2023
1 parent c6b636f commit 2b7d709
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 5 deletions.
66 changes: 66 additions & 0 deletions packages/flutter/lib/src/widgets/widget_inspector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
32 changes: 27 additions & 5 deletions packages/flutter/test/widgets/widget_inspector_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ int getChildLayerCount(OffsetLayer layer) {
return count;
}

extension TextFromString on String {
@widgetFactory
Widget text() {
return Text(this);
}
}

void main() {
_TestWidgetInspectorService.runTests();
}
Expand Down Expand Up @@ -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: <Widget>[
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();
Expand All @@ -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<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, 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',
Expand Down

0 comments on commit 2b7d709

Please sign in to comment.