[ Widget Preview ] Remove WidgetPreview
in favor of using annotation properties (#165500)
This change reworks how users define previews in their code, expands the number of valid 'functions' that can be used to create previews, and allows for specifying a 'wrapper' function to wrap the previewed widget with The `WidgetPreview` class has been removed from the framework, with its properties being added to the `Preview` annotation class instead to remove some boilerplate from the preview declaration workflow. Before: ```dart @Preview() List<WidgetPreview> previews() => <WidgetPreview>[ WidgetPreview( name: 'Top-level preview', child: Text('Foo'), ), ]; ``` After: ```dart @Preview(name: 'Top-level preview') Widget previews() => Text('Foo'); ``` Previews can now be defined using top-level functions, constructors and factories which take no arguments, and static methods within classes: Examples: ```dart @Preview(name: 'Top-level preview') Widget previews() => Text('Foo'); class MyWidget extends StatelessWidget { @Preview(name: 'Constructor preview') MyWidget.preview(); @Preview(name: 'Factory preview') factory MyWidget.factoryPreview() => MyWidget.preview(); @Preview(name: 'Static preview') static Widget previewStatic() => Text('Static'); @override Widget build(BuildContext context) { return Text('MyWidget'); } } ``` Users can also provide a `wrapper` function with the signature `Widget Function(Widget)` to easily wrap previewed widget with shared bootstrapping logic. Example: ```dart Widget testWrapper(Widget child) { return Provider<int>.value( value: 42, child: child, ); } @Preview(name: 'Preview with wrapper', wrapper: testWrapper) Widget preview() { return Text('Attributes'); } ``` Which is effectively the same as: ```dart @Preview(name: 'Preview with wrapper') Widget preview() { return Provider<int>.value( value: 42, child: Text('Attributes'), ); } ``` Finally, for situations where a `BuildContext` is needed, users can return a `WidgetBuilder` from their preview function: ```dart @Preview('Builder preview') WidgetBuilder builderPreview() { return (BuildContext context) { // TODO: retrieve state from context. return Text('Foo'); }; } ```
This commit is contained in:
parent
31ff6497f1
commit
41c427c6de
@ -13,70 +13,71 @@ import 'framework.dart';
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// Functions annotated with `@Preview()` must return a `WidgetPreview`
|
||||
/// and be public, top-level functions.
|
||||
/// Functions annotated with `@Preview()` must return a `Widget` or
|
||||
/// `WidgetBuilder` and be public. This annotation can only be applied
|
||||
/// to top-level functions, static methods defined within a class, and
|
||||
/// public `Widget` constructors and factories with no required arguments.
|
||||
///
|
||||
/// ```dart
|
||||
/// @Preview()
|
||||
/// WidgetPreview widgetPreview() {
|
||||
/// return const WidgetPreview(name: 'Preview 1', child: Text('Foo'));
|
||||
/// @Preview(name: 'Top-level preview')
|
||||
/// Widget preview() => const Text('Foo');
|
||||
///
|
||||
/// @Preview(name: 'Builder preview')
|
||||
/// WidgetBuilder builderPreview() {
|
||||
/// return (BuildContext context) {
|
||||
/// return const Text('Builder');
|
||||
/// };
|
||||
/// }
|
||||
///
|
||||
/// class MyWidget extends StatelessWidget {
|
||||
/// @Preview(name: 'Constructor preview')
|
||||
/// const MyWidget.preview({super.key});
|
||||
///
|
||||
/// @Preview(name: 'Factory constructor preview')
|
||||
/// factory MyWidget.factoryPreview() => const MyWidget.preview();
|
||||
///
|
||||
/// @Preview(name: 'Static preview')
|
||||
/// static Widget previewStatic() => const Text('Static');
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return const Text('MyWidget');
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [WidgetPreview], a data class used to specify widget previews.
|
||||
// TODO(bkonyi): link to actual documentation when available.
|
||||
class Preview {
|
||||
base class Preview {
|
||||
/// Annotation used to mark functions that return widget previews.
|
||||
const Preview();
|
||||
}
|
||||
|
||||
/// Wraps a [Widget], initializing various state and properties to allow for
|
||||
/// previewing of the [Widget] in the widget previewer.
|
||||
///
|
||||
/// WARNING: This interface is not stable and **will change**.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Preview], an annotation class used to mark functions returning widget
|
||||
/// previews.
|
||||
// TODO(bkonyi): link to actual documentation when available.
|
||||
class WidgetPreview {
|
||||
/// Wraps [child] in a [WidgetPreview] instance that applies some set of
|
||||
/// properties.
|
||||
const WidgetPreview({
|
||||
required this.child,
|
||||
this.name,
|
||||
this.width,
|
||||
this.height,
|
||||
this.textScaleFactor,
|
||||
});
|
||||
const Preview({this.name, this.width, this.height, this.textScaleFactor, this.wrapper});
|
||||
|
||||
/// A description to be displayed alongside the preview.
|
||||
///
|
||||
/// If not provided, no name will be associated with the preview.
|
||||
final String? name;
|
||||
|
||||
/// The [Widget] to be rendered in the preview.
|
||||
final Widget child;
|
||||
|
||||
/// Artificial width constraint to be applied to the [child].
|
||||
/// Artificial width constraint to be applied to the previewed widget.
|
||||
///
|
||||
/// If not provided, the previewed widget will attempt to set its own width
|
||||
/// constraints and may result in an unbounded constraint error.
|
||||
final double? width;
|
||||
|
||||
/// Artificial height constraint to be applied to the [child].
|
||||
/// Artificial height constraint to be applied to the previewed widget.
|
||||
///
|
||||
/// If not provided, the previewed widget will attempt to set its own height
|
||||
/// constraints and may result in an unbounded constraint error.
|
||||
final double? height;
|
||||
|
||||
/// Applies font scaling to text within the [child].
|
||||
/// Applies font scaling to text within the previewed widget.
|
||||
///
|
||||
/// If not provided, the default text scaling factor provided by [MediaQuery]
|
||||
/// will be used.
|
||||
final double? textScaleFactor;
|
||||
|
||||
/// Wraps the previewed [Widget] in a [Widget] tree.
|
||||
///
|
||||
/// This function can be used to perform dependency injection or setup
|
||||
/// additional scaffolding needed to correctly render the preview.
|
||||
// TODO(bkonyi): provide an example.
|
||||
final Widget Function(Widget)? wrapper;
|
||||
}
|
||||
|
@ -181,6 +181,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
late final FlutterProject rootProject = getRootProject();
|
||||
|
||||
late final PreviewDetector _previewDetector = PreviewDetector(
|
||||
projectRoot: rootProject.directory,
|
||||
logger: logger,
|
||||
fs: fs,
|
||||
onChangeDetected: onChangeDetected,
|
||||
@ -260,7 +261,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
|
||||
await _populatePreviewPubspec(rootProject: rootProject);
|
||||
}
|
||||
|
||||
final PreviewMapping initialPreviews = await _previewDetector.initialize(rootProject.directory);
|
||||
final PreviewMapping initialPreviews = await _previewDetector.initialize();
|
||||
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(initialPreviews);
|
||||
|
||||
if (boolArg(kLaunchPreviewer)) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:built_collection/built_collection.dart';
|
||||
import 'package:code_builder/code_builder.dart';
|
||||
|
||||
import '../base/file_system.dart';
|
||||
@ -30,37 +31,100 @@ class PreviewCodeGenerator {
|
||||
/// An example of a formatted generated file containing previews from two files could be:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'package:foo/foo.dart' as _i1;
|
||||
/// import 'package:foo/src/bar.dart' as _i2;
|
||||
/// import 'package:widget_preview/widget_preview.dart';
|
||||
/// import 'widget_preview.dart' as _i1;
|
||||
/// import 'package:splash/foo.dart' as _i2;
|
||||
/// import 'package:splash/main.dart' as _i3;
|
||||
/// import 'package:flutter/widgets.dart' as _i4;
|
||||
///
|
||||
/// List<WidgetPreview> previews() => [
|
||||
/// _i1.fooPreview(),
|
||||
/// _i2.barPreview1(),
|
||||
/// _i3.barPreview2(),
|
||||
/// List<_i1.WidgetPreview> previews() => [
|
||||
/// _i1.WidgetPreview(height: 100.0, width: 10000.0, child: _i2.preview()),
|
||||
/// _i1.WidgetPreview(
|
||||
/// name: 'Foo',
|
||||
/// height: 50 + 20,
|
||||
/// width: 200.0,
|
||||
/// textScaleFactor: 2.0,
|
||||
/// child: _i3.preview(),
|
||||
/// ),
|
||||
/// _i1.WidgetPreview(
|
||||
/// name: 'Baz',
|
||||
/// height: 50.0,
|
||||
/// width: 200.0,
|
||||
/// textScaleFactor: 3.0,
|
||||
/// child: _i2.stateInjector(_i3.preview()),
|
||||
/// ),
|
||||
/// _i1.WidgetPreview(name: 'Bar', child: _i4.Builder(builder: _i3.preview2())),
|
||||
/// _i1.WidgetPreview(name: 'Constructor preview', height: 50.0, width: 100.0, child: _i3.MyWidget()),
|
||||
/// _i1.WidgetPreview(
|
||||
/// name: 'Named constructor preview',
|
||||
/// height: 50.0,
|
||||
/// width: 100.0,
|
||||
/// child: _i3.MyWidget.preview(),
|
||||
/// ),
|
||||
/// _i1.WidgetPreview(
|
||||
/// name: 'Static preview',
|
||||
/// height: 50.0,
|
||||
/// width: 100.0,
|
||||
/// child: _i3.MyWidget.staticPreview(),
|
||||
/// ),
|
||||
/// ];
|
||||
/// ```
|
||||
void populatePreviewsInGeneratedPreviewScaffold(PreviewMapping previews) {
|
||||
final TypeReference returnType =
|
||||
(TypeReferenceBuilder()
|
||||
..symbol = 'List'
|
||||
..types = ListBuilder<Reference>(<Reference>[
|
||||
refer('WidgetPreview', 'widget_preview.dart'),
|
||||
]))
|
||||
.build();
|
||||
final Library lib = Library(
|
||||
(LibraryBuilder b) => b.body.addAll(<Spec>[
|
||||
Directive.import('package:flutter/widgets.dart'),
|
||||
Method(
|
||||
(MethodBuilder b) =>
|
||||
b
|
||||
..body =
|
||||
literalList(<Object?>[
|
||||
for (final MapEntry<PreviewPath, List<String>>(
|
||||
key: (path: String _, :Uri uri),
|
||||
value: List<String> previewMethods,
|
||||
)
|
||||
in previews.entries) ...<Object?>[
|
||||
for (final String method in previewMethods)
|
||||
refer(method, uri.toString()).call(<Expression>[]),
|
||||
],
|
||||
]).code
|
||||
..name = 'previews'
|
||||
..returns = refer('List<WidgetPreview>'),
|
||||
),
|
||||
Method((MethodBuilder b) {
|
||||
final List<Expression> previewExpressions = <Expression>[];
|
||||
for (final MapEntry<PreviewPath, List<PreviewDetails>>(
|
||||
key: (path: String _, :Uri uri),
|
||||
value: List<PreviewDetails> previewMethods,
|
||||
)
|
||||
in previews.entries) {
|
||||
for (final PreviewDetails preview in previewMethods) {
|
||||
Expression previewWidget = refer(
|
||||
preview.functionName,
|
||||
uri.toString(),
|
||||
).call(<Expression>[]);
|
||||
|
||||
if (preview.isBuilder) {
|
||||
previewWidget = refer(
|
||||
'Builder',
|
||||
'package:flutter/widgets.dart',
|
||||
).newInstance(<Expression>[], <String, Expression>{'builder': previewWidget});
|
||||
}
|
||||
if (preview.hasWrapper) {
|
||||
previewWidget = refer(
|
||||
preview.wrapper!,
|
||||
preview.wrapperLibraryUri,
|
||||
).call(<Expression>[previewWidget]);
|
||||
}
|
||||
previewExpressions.add(
|
||||
refer(
|
||||
'WidgetPreview',
|
||||
'widget_preview.dart',
|
||||
).newInstance(<Expression>[], <String, Expression>{
|
||||
if (preview.name != null) PreviewDetails.kName: refer(preview.name!).expression,
|
||||
...?_buildDoubleParameters(key: PreviewDetails.kHeight, property: preview.height),
|
||||
...?_buildDoubleParameters(key: PreviewDetails.kWidth, property: preview.width),
|
||||
...?_buildDoubleParameters(
|
||||
key: PreviewDetails.kTextScaleFactor,
|
||||
property: preview.textScaleFactor,
|
||||
),
|
||||
'child': previewWidget,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
b
|
||||
..body = literalList(previewExpressions).code
|
||||
..name = 'previews'
|
||||
..returns = returnType;
|
||||
}),
|
||||
]),
|
||||
);
|
||||
final DartEmitter emitter = DartEmitter.scoped(useNullSafetySyntax: true);
|
||||
@ -70,4 +134,16 @@ class PreviewCodeGenerator {
|
||||
// TODO(bkonyi): do we want to bother with formatting this?
|
||||
generatedPreviewFile.writeAsStringSync(lib.accept(emitter).toString());
|
||||
}
|
||||
|
||||
Map<String, Expression>? _buildDoubleParameters({
|
||||
required String key,
|
||||
required String? property,
|
||||
}) {
|
||||
if (property == null) {
|
||||
return null;
|
||||
}
|
||||
return <String, Expression>{
|
||||
key: CodeExpression(Code('${double.tryParse(property) ?? property}')),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,16 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:analyzer/dart/analysis/analysis_context.dart';
|
||||
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
|
||||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/ast/token.dart';
|
||||
import 'package:analyzer/dart/ast/visitor.dart';
|
||||
import 'package:analyzer/file_system/physical_file_system.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import '../base/file_system.dart';
|
||||
@ -25,11 +28,14 @@ import 'preview_code_generator.dart';
|
||||
typedef PreviewPath = ({String path, Uri uri});
|
||||
|
||||
/// Represents a set of previews for a given file.
|
||||
typedef PreviewMapping = Map<PreviewPath, List<String>>;
|
||||
typedef PreviewMapping = Map<PreviewPath, List<PreviewDetails>>;
|
||||
|
||||
extension on Token {
|
||||
/// Convenience getter to identify tokens for private fields and functions.
|
||||
bool get isPrivate => toString().startsWith('_');
|
||||
|
||||
/// Convenience getter to identify WidgetBuilder types.
|
||||
bool get isWidgetBuilder => toString() == 'WidgetBuilder';
|
||||
}
|
||||
|
||||
extension on Annotation {
|
||||
@ -50,27 +56,191 @@ extension on ParsedUnitResult {
|
||||
PreviewPath toPreviewPath() => (path: path, uri: uri);
|
||||
}
|
||||
|
||||
/// Contains details related to a single preview instance.
|
||||
final class PreviewDetails {
|
||||
PreviewDetails({required this.functionName, required this.isBuilder});
|
||||
|
||||
@visibleForTesting
|
||||
PreviewDetails.test({
|
||||
required this.functionName,
|
||||
required this.isBuilder,
|
||||
String? name,
|
||||
String? width,
|
||||
String? height,
|
||||
String? textScaleFactor,
|
||||
String? wrapper,
|
||||
String? wrapperLibraryUri = '',
|
||||
}) : _name = name,
|
||||
_width = width,
|
||||
_height = height,
|
||||
_textScaleFactor = textScaleFactor,
|
||||
_wrapper = wrapper,
|
||||
_wrapperLibraryUri = wrapperLibraryUri;
|
||||
|
||||
@visibleForTesting
|
||||
PreviewDetails copyWith({
|
||||
String? functionName,
|
||||
bool? isBuilder,
|
||||
String? name,
|
||||
String? width,
|
||||
String? height,
|
||||
String? textScaleFactor,
|
||||
String? wrapper,
|
||||
String? wrapperLibraryUri,
|
||||
}) {
|
||||
return PreviewDetails.test(
|
||||
functionName: functionName ?? this.functionName,
|
||||
isBuilder: isBuilder ?? this.isBuilder,
|
||||
name: name ?? this.name,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
textScaleFactor: textScaleFactor ?? this.textScaleFactor,
|
||||
wrapper: wrapper ?? this.wrapper,
|
||||
wrapperLibraryUri: wrapperLibraryUri ?? this.wrapperLibraryUri,
|
||||
);
|
||||
}
|
||||
|
||||
static const String kName = 'name';
|
||||
static const String kWidth = 'width';
|
||||
static const String kHeight = 'height';
|
||||
static const String kTextScaleFactor = 'textScaleFactor';
|
||||
static const String kWrapper = 'wrapper';
|
||||
static const String kWrapperLibraryUri = 'wrapperLibraryUrl';
|
||||
|
||||
/// The name of the function returning the preview.
|
||||
final String functionName;
|
||||
|
||||
/// Set to `true` if the preview function is returning a [WidgetBuilder]
|
||||
/// instead of a [Widget].
|
||||
final bool isBuilder;
|
||||
|
||||
/// A description to be displayed alongside the preview.
|
||||
///
|
||||
/// If not provided, no name will be associated with the preview.
|
||||
String? get name => _name;
|
||||
String? _name;
|
||||
|
||||
/// Artificial width constraint to be applied to the [child].
|
||||
///
|
||||
/// If not provided, the previewed widget will attempt to set its own width
|
||||
/// constraints and may result in an unbounded constraint error.
|
||||
String? get width => _width;
|
||||
String? _width;
|
||||
|
||||
/// Artificial height constraint to be applied to the [child].
|
||||
///
|
||||
/// If not provided, the previewed widget will attempt to set its own height
|
||||
/// constraints and may result in an unbounded constraint error.
|
||||
String? get height => _height;
|
||||
String? _height;
|
||||
|
||||
/// Applies font scaling to text within the [child].
|
||||
///
|
||||
/// If not provided, the default text scaling factor provided by [MediaQuery]
|
||||
/// will be used.
|
||||
String? get textScaleFactor => _textScaleFactor;
|
||||
String? _textScaleFactor;
|
||||
|
||||
/// The name of a tear-off used to wrap the [Widget] returned by the preview
|
||||
/// function defined by [functionName].
|
||||
///
|
||||
/// If not provided, the [Widget] returned by [functionName] will be used by
|
||||
/// the previewer directly.
|
||||
String? get wrapper => _wrapper;
|
||||
String? _wrapper;
|
||||
|
||||
/// The URI for the library containing the declaration of [wrapper].
|
||||
String? get wrapperLibraryUri => _wrapperLibraryUri;
|
||||
String? _wrapperLibraryUri;
|
||||
|
||||
bool get hasWrapper => _wrapper != null;
|
||||
|
||||
void _setField({required NamedExpression node}) {
|
||||
final String key = node.name.label.name;
|
||||
final Expression expression = node.expression;
|
||||
final String source = expression.toSource();
|
||||
switch (key) {
|
||||
case kName:
|
||||
_name = source;
|
||||
case kWidth:
|
||||
_width = source;
|
||||
case kHeight:
|
||||
_height = source;
|
||||
case kTextScaleFactor:
|
||||
_textScaleFactor = source;
|
||||
case kWrapper:
|
||||
_wrapper = source;
|
||||
_wrapperLibraryUri = (node.expression as SimpleIdentifier).element!.library2!.identifier;
|
||||
default:
|
||||
throw StateError('Unknown Preview field "$name": $source');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (identical(other, this)) {
|
||||
return true;
|
||||
}
|
||||
return other.runtimeType == runtimeType &&
|
||||
other is PreviewDetails &&
|
||||
other.functionName == functionName &&
|
||||
other.isBuilder == isBuilder &&
|
||||
other.height == height &&
|
||||
other.width == width &&
|
||||
other.textScaleFactor == textScaleFactor &&
|
||||
other.wrapper == wrapper &&
|
||||
other.wrapperLibraryUri == wrapperLibraryUri;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'PreviewDetails(function: $functionName isBuilder: $isBuilder $kName: $name '
|
||||
'$kWidth: $width $kHeight: $height $kTextScaleFactor: $textScaleFactor $kWrapper: $wrapper '
|
||||
'$kWrapperLibraryUri: $wrapperLibraryUri)';
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(<Object?>[
|
||||
functionName,
|
||||
isBuilder,
|
||||
height,
|
||||
width,
|
||||
textScaleFactor,
|
||||
wrapper,
|
||||
wrapperLibraryUri,
|
||||
]);
|
||||
}
|
||||
|
||||
class PreviewDetector {
|
||||
PreviewDetector({
|
||||
required this.projectRoot,
|
||||
required this.fs,
|
||||
required this.logger,
|
||||
required this.onChangeDetected,
|
||||
required this.onPubspecChangeDetected,
|
||||
});
|
||||
|
||||
final Directory projectRoot;
|
||||
final FileSystem fs;
|
||||
final Logger logger;
|
||||
final void Function(PreviewMapping) onChangeDetected;
|
||||
final void Function() onPubspecChangeDetected;
|
||||
|
||||
StreamSubscription<WatchEvent>? _fileWatcher;
|
||||
final PreviewDetectorMutex _mutex = PreviewDetectorMutex();
|
||||
late final PreviewMapping _pathToPreviews;
|
||||
|
||||
late final AnalysisContextCollection collection = AnalysisContextCollection(
|
||||
includedPaths: <String>[projectRoot.absolute.path],
|
||||
resourceProvider: PhysicalResourceProvider.INSTANCE,
|
||||
);
|
||||
|
||||
/// Starts listening for changes to Dart sources under [projectRoot] and returns
|
||||
/// the initial [PreviewMapping] for the project.
|
||||
Future<PreviewMapping> initialize(Directory projectRoot) async {
|
||||
Future<PreviewMapping> initialize() async {
|
||||
// Find the initial set of previews.
|
||||
_pathToPreviews = findPreviewFunctions(projectRoot);
|
||||
_pathToPreviews = await findPreviewFunctions(projectRoot);
|
||||
|
||||
final Watcher watcher = Watcher(projectRoot.path);
|
||||
_fileWatcher = watcher.events.listen((WatchEvent event) async {
|
||||
@ -87,7 +257,7 @@ class PreviewDetector {
|
||||
return;
|
||||
}
|
||||
logger.printStatus('Detected change in $eventPath.');
|
||||
final PreviewMapping filePreviewsMapping = findPreviewFunctions(
|
||||
final PreviewMapping filePreviewsMapping = await findPreviewFunctions(
|
||||
fs.file(Uri.file(event.path)),
|
||||
);
|
||||
final bool hasExistingPreviews =
|
||||
@ -102,13 +272,13 @@ class PreviewDetector {
|
||||
}
|
||||
if (filePreviewsMapping.isNotEmpty) {
|
||||
// The set of previews has changed, but there are still previews in the file.
|
||||
final MapEntry<PreviewPath, List<String>>(
|
||||
final MapEntry<PreviewPath, List<PreviewDetails>>(
|
||||
key: PreviewPath location,
|
||||
value: List<String> filePreviews,
|
||||
value: List<PreviewDetails> filePreviews,
|
||||
) = filePreviewsMapping.entries.first;
|
||||
logger.printStatus('Updated previews for ${location.uri}: $filePreviews');
|
||||
if (filePreviews.isNotEmpty) {
|
||||
final List<String>? currentPreviewsForFile = _pathToPreviews[location];
|
||||
final List<PreviewDetails>? currentPreviewsForFile = _pathToPreviews[location];
|
||||
if (filePreviews != currentPreviewsForFile) {
|
||||
_pathToPreviews[location] = filePreviews;
|
||||
}
|
||||
@ -127,63 +297,201 @@ class PreviewDetector {
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _fileWatcher?.cancel();
|
||||
// Guard disposal behind a mutex to make sure the analyzer has finished
|
||||
// processing the latest file updates to avoid throwing an exception.
|
||||
await _mutex.runGuarded(() async {
|
||||
await _fileWatcher?.cancel();
|
||||
await collection.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/// Search for functions annotated with `@Preview` in the current project.
|
||||
PreviewMapping findPreviewFunctions(FileSystemEntity entity) {
|
||||
final AnalysisContextCollection collection = AnalysisContextCollection(
|
||||
includedPaths: <String>[entity.absolute.path],
|
||||
resourceProvider: PhysicalResourceProvider.INSTANCE,
|
||||
);
|
||||
|
||||
Future<PreviewMapping> findPreviewFunctions(FileSystemEntity entity) async {
|
||||
final PreviewMapping previews = PreviewMapping();
|
||||
for (final AnalysisContext context in collection.contexts) {
|
||||
logger.printStatus('Finding previews in ${context.contextRoot.root.path}...');
|
||||
// Only process one FileSystemEntity at a time so we don't invalidate an AnalysisSession that's
|
||||
// in use when we call context.changeFile(...).
|
||||
await _mutex.runGuarded(() async {
|
||||
// TODO(bkonyi): this can probably be replaced by a call to collection.contextFor(...),
|
||||
// but we need to figure out the right path format for Windows.
|
||||
for (final AnalysisContext context in collection.contexts) {
|
||||
logger.printStatus('Finding previews in ${entity.path}...');
|
||||
|
||||
for (final String filePath in context.contextRoot.analyzedFiles()) {
|
||||
logger.printTrace('Checking file: $filePath');
|
||||
if (!filePath.isDartFile) {
|
||||
continue;
|
||||
// If we're processing a single file, it means the file watcher detected a
|
||||
// change in a Dart source. We need to notify the analyzer that this file
|
||||
// has changed so it can reanalyze the file.
|
||||
if (entity is File) {
|
||||
context.changeFile(entity.path);
|
||||
await context.applyPendingFileChanges();
|
||||
}
|
||||
|
||||
final SomeParsedLibraryResult lib = context.currentSession.getParsedLibrary(filePath);
|
||||
if (lib is ParsedLibraryResult) {
|
||||
for (final ParsedUnitResult libUnit in lib.units) {
|
||||
final List<String> previewEntries = previews[libUnit.toPreviewPath()] ?? <String>[];
|
||||
for (final CompilationUnitMember entity in libUnit.unit.declarations) {
|
||||
if (entity is FunctionDeclaration && !entity.name.isPrivate) {
|
||||
bool foundPreview = false;
|
||||
for (final Annotation annotation in entity.metadata) {
|
||||
if (annotation.isPreview) {
|
||||
// What happens if the annotation is applied multiple times?
|
||||
foundPreview = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundPreview) {
|
||||
logger.printStatus('Found preview at:');
|
||||
logger.printStatus('File path: ${libUnit.uri}');
|
||||
logger.printStatus('Preview function: ${entity.name}');
|
||||
logger.printStatus('');
|
||||
previewEntries.add(entity.name.toString());
|
||||
}
|
||||
for (final String filePath in context.contextRoot.analyzedFiles()) {
|
||||
logger.printTrace('Checking file: $filePath');
|
||||
if (!filePath.isDartFile || !filePath.startsWith(entity.path)) {
|
||||
logger.printTrace('Skipping $filePath');
|
||||
continue;
|
||||
}
|
||||
final SomeResolvedLibraryResult lib = await context.currentSession.getResolvedLibrary(
|
||||
filePath,
|
||||
);
|
||||
// TODO(bkonyi): ensure this can handle part files.
|
||||
if (lib is ResolvedLibraryResult) {
|
||||
for (final ResolvedUnitResult libUnit in lib.units) {
|
||||
final List<PreviewDetails> previewEntries =
|
||||
previews[libUnit.toPreviewPath()] ?? <PreviewDetails>[];
|
||||
final PreviewVisitor visitor = PreviewVisitor();
|
||||
libUnit.unit.visitChildren(visitor);
|
||||
previewEntries.addAll(visitor.previewEntries);
|
||||
if (previewEntries.isNotEmpty) {
|
||||
previews[libUnit.toPreviewPath()] = previewEntries;
|
||||
}
|
||||
}
|
||||
if (previewEntries.isNotEmpty) {
|
||||
previews[libUnit.toPreviewPath()] = previewEntries;
|
||||
}
|
||||
} else {
|
||||
logger.printWarning('Unknown library type at $filePath: $lib');
|
||||
}
|
||||
} else {
|
||||
logger.printWarning('Unknown library type at $filePath: $lib');
|
||||
}
|
||||
}
|
||||
}
|
||||
final int previewCount = previews.values.fold<int>(
|
||||
0,
|
||||
(int count, List<String> value) => count + value.length,
|
||||
);
|
||||
logger.printStatus('Found $previewCount ${pluralize('preview', previewCount)}.');
|
||||
final int previewCount = previews.values.fold<int>(
|
||||
0,
|
||||
(int count, List<PreviewDetails> value) => count + value.length,
|
||||
);
|
||||
logger.printStatus('Found $previewCount ${pluralize('preview', previewCount)}.');
|
||||
});
|
||||
return previews;
|
||||
}
|
||||
}
|
||||
|
||||
/// Visitor which detects previews and extracts [PreviewDetails] for later code
|
||||
/// generation.
|
||||
// TODO(bkonyi): this visitor needs better error detection to identify invalid
|
||||
// previews and report them to the previewer without causing the entire
|
||||
// environment to shutdown or fail to render valid previews.
|
||||
class PreviewVisitor extends RecursiveAstVisitor<void> {
|
||||
final List<PreviewDetails> previewEntries = <PreviewDetails>[];
|
||||
|
||||
FunctionDeclaration? _currentFunction;
|
||||
ConstructorDeclaration? _currentConstructor;
|
||||
MethodDeclaration? _currentMethod;
|
||||
PreviewDetails? _currentPreview;
|
||||
|
||||
/// Handles previews defined on top-level functions.
|
||||
@override
|
||||
void visitFunctionDeclaration(FunctionDeclaration node) {
|
||||
assert(_currentFunction == null);
|
||||
if (node.name.isPrivate) {
|
||||
return;
|
||||
}
|
||||
|
||||
final TypeAnnotation? returnType = node.returnType;
|
||||
if (returnType == null || returnType.question != null) {
|
||||
return;
|
||||
}
|
||||
_scopedVisitChildren(node, (FunctionDeclaration? node) => _currentFunction = node);
|
||||
}
|
||||
|
||||
/// Handles previews defined on constructors.
|
||||
@override
|
||||
void visitConstructorDeclaration(ConstructorDeclaration node) {
|
||||
_scopedVisitChildren(node, (ConstructorDeclaration? node) => _currentConstructor = node);
|
||||
}
|
||||
|
||||
/// Handles previews defined on static methods within classes.
|
||||
@override
|
||||
void visitMethodDeclaration(MethodDeclaration node) {
|
||||
if (!node.isStatic) {
|
||||
return;
|
||||
}
|
||||
_scopedVisitChildren(node, (MethodDeclaration? node) => _currentMethod = node);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitAnnotation(Annotation node) {
|
||||
if (!node.isPreview) {
|
||||
return;
|
||||
}
|
||||
assert(_currentFunction != null || _currentConstructor != null || _currentMethod != null);
|
||||
if (_currentFunction != null) {
|
||||
final NamedType returnType = _currentFunction!.returnType! as NamedType;
|
||||
_currentPreview = PreviewDetails(
|
||||
functionName: _currentFunction!.name.toString(),
|
||||
isBuilder: returnType.name2.isWidgetBuilder,
|
||||
);
|
||||
} else if (_currentConstructor != null) {
|
||||
final SimpleIdentifier returnType = _currentConstructor!.returnType as SimpleIdentifier;
|
||||
final Token? name = _currentConstructor!.name;
|
||||
_currentPreview = PreviewDetails(
|
||||
functionName: '$returnType${name == null ? '' : '.$name'}',
|
||||
isBuilder: false,
|
||||
);
|
||||
} else if (_currentMethod != null) {
|
||||
final NamedType returnType = _currentMethod!.returnType! as NamedType;
|
||||
final ClassDeclaration parentClass = _currentMethod!.parent! as ClassDeclaration;
|
||||
_currentPreview = PreviewDetails(
|
||||
functionName: '${parentClass.name}.${_currentMethod!.name}',
|
||||
isBuilder: returnType.name2.isWidgetBuilder,
|
||||
);
|
||||
}
|
||||
node.visitChildren(this);
|
||||
previewEntries.add(_currentPreview!);
|
||||
_currentPreview = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void visitNamedExpression(NamedExpression node) {
|
||||
// Extracts named properties from the @Preview annotation.
|
||||
_currentPreview?._setField(node: node);
|
||||
}
|
||||
|
||||
void _scopedVisitChildren<T extends AstNode>(T node, void Function(T?) setter) {
|
||||
setter(node);
|
||||
node.visitChildren(this);
|
||||
setter(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to protect global state accessed in blocks containing calls to
|
||||
/// asynchronous methods.
|
||||
///
|
||||
/// Originally from DDS:
|
||||
/// https://github.com/dart-lang/sdk/blob/3fe58da3cfe2c03fb9ee691a7a4709082fad3e73/pkg/dds/lib/src/utils/mutex.dart
|
||||
class PreviewDetectorMutex {
|
||||
/// Executes a block of code containing asynchronous calls atomically.
|
||||
///
|
||||
/// If no other asynchronous context is currently executing within
|
||||
/// [criticalSection], it will immediately be called. Otherwise, the
|
||||
/// caller will be suspended and entered into a queue to be resumed once the
|
||||
/// lock is released.
|
||||
Future<T> runGuarded<T>(FutureOr<T> Function() criticalSection) async {
|
||||
try {
|
||||
await _acquireLock();
|
||||
return await criticalSection();
|
||||
} finally {
|
||||
_releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _acquireLock() async {
|
||||
if (!_locked) {
|
||||
_locked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
final Completer<void> request = Completer<void>();
|
||||
_outstandingRequests.add(request);
|
||||
await request.future;
|
||||
}
|
||||
|
||||
void _releaseLock() {
|
||||
if (_outstandingRequests.isNotEmpty) {
|
||||
final Completer<void> request = _outstandingRequests.removeFirst();
|
||||
request.complete();
|
||||
return;
|
||||
}
|
||||
// Only release the lock if no other requests are pending to prevent races
|
||||
// between the next request from the queue to be handled and incoming
|
||||
// requests.
|
||||
_locked = false;
|
||||
}
|
||||
|
||||
bool _locked = false;
|
||||
final Queue<Completer<void>> _outstandingRequests = Queue<Completer<void>>();
|
||||
}
|
||||
|
@ -353,6 +353,7 @@
|
||||
"templates/plugin_swift_package_manager/macos.tmpl/projectName.tmpl/Sources/projectName.tmpl/PrivacyInfo.xcprivacy",
|
||||
|
||||
"templates/widget_preview_scaffold/lib/main.dart.tmpl",
|
||||
"templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl",
|
||||
"templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl",
|
||||
"templates/widget_preview_scaffold/lib/src/controls.dart.tmpl",
|
||||
"templates/widget_preview_scaffold/lib/src/generated_preview.dart.tmpl",
|
||||
|
@ -3,5 +3,6 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'widget_preview.dart';
|
||||
|
||||
List<WidgetPreview> previews() => <WidgetPreview>[];
|
||||
|
@ -0,0 +1,53 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Wraps a [Widget], initializing various state and properties to allow for
|
||||
/// previewing of the [Widget] in the widget previewer.
|
||||
///
|
||||
/// WARNING: This interface is not stable and **will change**.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Preview], an annotation class used to mark functions returning widget
|
||||
/// previews.
|
||||
// TODO(bkonyi): link to actual documentation when available.
|
||||
class WidgetPreview {
|
||||
/// Wraps [child] in a [WidgetPreview] instance that applies some set of
|
||||
/// properties.
|
||||
const WidgetPreview({
|
||||
required this.child,
|
||||
this.name,
|
||||
this.width,
|
||||
this.height,
|
||||
this.textScaleFactor,
|
||||
});
|
||||
|
||||
/// A description to be displayed alongside the preview.
|
||||
///
|
||||
/// If not provided, no name will be associated with the preview.
|
||||
final String? name;
|
||||
|
||||
/// The [Widget] to be rendered in the preview.
|
||||
final Widget child;
|
||||
|
||||
/// Artificial width constraint to be applied to the [child].
|
||||
///
|
||||
/// If not provided, the previewed widget will attempt to set its own width
|
||||
/// constraints and may result in an unbounded constraint error.
|
||||
final double? width;
|
||||
|
||||
/// Artificial height constraint to be applied to the [child].
|
||||
///
|
||||
/// If not provided, the previewed widget will attempt to set its own height
|
||||
/// constraints and may result in an unbounded constraint error.
|
||||
final double? height;
|
||||
|
||||
/// Applies font scaling to text within the [child].
|
||||
///
|
||||
/// If not provided, the default text scaling factor provided by [MediaQuery]
|
||||
/// will be used.
|
||||
final double? textScaleFactor;
|
||||
}
|
@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
|
||||
import 'controls.dart';
|
||||
import 'generated_preview.dart';
|
||||
import 'utils.dart';
|
||||
import 'widget_preview.dart';
|
||||
|
||||
class WidgetPreviewWidget extends StatefulWidget {
|
||||
const WidgetPreviewWidget({
|
||||
|
@ -70,6 +70,7 @@ void main() {
|
||||
projectRoot = createBasicProjectStructure(fs);
|
||||
logger = BufferLogger.test();
|
||||
previewDetector = PreviewDetector(
|
||||
projectRoot: projectRoot,
|
||||
logger: logger,
|
||||
fs: fs,
|
||||
onChangeDetected: onChangeDetectedRoot,
|
||||
@ -89,15 +90,48 @@ void main() {
|
||||
addPreviewContainingFile(projectRoot, <String>['src', 'bar.dart']),
|
||||
];
|
||||
addNonPreviewContainingFile(projectRoot, <String>['baz.dart']);
|
||||
final PreviewMapping mapping = previewDetector.findPreviewFunctions(projectRoot);
|
||||
final PreviewMapping mapping = await previewDetector.findPreviewFunctions(projectRoot);
|
||||
expect(mapping.keys.toSet(), previewFiles.toSet());
|
||||
});
|
||||
|
||||
testUsingContext('can detect previews in updated files', () async {
|
||||
final List<PreviewDetails> expectedPreviewDetails = <PreviewDetails>[
|
||||
PreviewDetails.test(functionName: 'previews', isBuilder: false, name: 'Top-level preview'),
|
||||
PreviewDetails.test(
|
||||
functionName: 'builderPreview',
|
||||
isBuilder: true,
|
||||
name: 'Builder preview',
|
||||
),
|
||||
PreviewDetails.test(
|
||||
functionName: 'attributesPreview',
|
||||
isBuilder: false,
|
||||
name: 'Attributes preview',
|
||||
width: '100.0',
|
||||
height: '100',
|
||||
textScaleFactor: '2',
|
||||
wrapper: 'testWrapper',
|
||||
),
|
||||
PreviewDetails.test(
|
||||
functionName: 'MyWidget.preview',
|
||||
isBuilder: false,
|
||||
name: 'Constructor preview',
|
||||
),
|
||||
PreviewDetails.test(
|
||||
functionName: 'MyWidget.factoryPreview',
|
||||
isBuilder: false,
|
||||
name: 'Factory constructor preview',
|
||||
),
|
||||
PreviewDetails.test(
|
||||
functionName: 'MyWidget.previewStatic',
|
||||
isBuilder: false,
|
||||
name: 'Static preview',
|
||||
),
|
||||
];
|
||||
|
||||
// Create two files with existing previews and one without.
|
||||
final PreviewMapping expectedInitialMapping = <PreviewPath, List<String>>{
|
||||
addPreviewContainingFile(projectRoot, <String>['foo.dart']): <String>['previews'],
|
||||
addPreviewContainingFile(projectRoot, <String>['src', 'bar.dart']): <String>['previews'],
|
||||
final PreviewMapping expectedInitialMapping = <PreviewPath, List<PreviewDetails>>{
|
||||
addPreviewContainingFile(projectRoot, <String>['foo.dart']): expectedPreviewDetails,
|
||||
addPreviewContainingFile(projectRoot, <String>['src', 'bar.dart']): expectedPreviewDetails,
|
||||
};
|
||||
final PreviewPath nonPreviewContainingFile = addNonPreviewContainingFile(
|
||||
projectRoot,
|
||||
@ -107,15 +141,15 @@ void main() {
|
||||
Completer<void> completer = Completer<void>();
|
||||
onChangeDetected = (PreviewMapping updated) {
|
||||
// The new preview in baz.dart should be included in the preview mapping.
|
||||
expect(updated, <PreviewPath, List<String>>{
|
||||
expect(stripNonDeterministicFields(updated), <PreviewPath, List<PreviewDetails>>{
|
||||
...expectedInitialMapping,
|
||||
nonPreviewContainingFile: <String>['previews'],
|
||||
nonPreviewContainingFile: expectedPreviewDetails,
|
||||
});
|
||||
completer.complete();
|
||||
};
|
||||
// Initialize the file watcher.
|
||||
final PreviewMapping initialPreviews = await previewDetector.initialize(projectRoot);
|
||||
expect(initialPreviews, expectedInitialMapping);
|
||||
final PreviewMapping initialPreviews = await previewDetector.initialize();
|
||||
expect(stripNonDeterministicFields(initialPreviews), expectedInitialMapping);
|
||||
|
||||
// Update the file without an existing preview to include a preview and ensure it triggers
|
||||
// the preview detector.
|
||||
@ -125,7 +159,7 @@ void main() {
|
||||
completer = Completer<void>();
|
||||
onChangeDetected = (PreviewMapping updated) {
|
||||
// The removed preview in baz.dart should not longer be included in the preview mapping.
|
||||
expect(updated, expectedInitialMapping);
|
||||
expect(stripNonDeterministicFields(updated), expectedInitialMapping);
|
||||
completer.complete();
|
||||
};
|
||||
|
||||
@ -135,6 +169,65 @@ void main() {
|
||||
await completer.future;
|
||||
});
|
||||
|
||||
testUsingContext('can detect previews in newly added files', () async {
|
||||
final List<PreviewDetails> expectedPreviewDetails = <PreviewDetails>[
|
||||
PreviewDetails.test(functionName: 'previews', isBuilder: false, name: 'Top-level preview'),
|
||||
PreviewDetails.test(
|
||||
functionName: 'builderPreview',
|
||||
isBuilder: true,
|
||||
name: 'Builder preview',
|
||||
),
|
||||
PreviewDetails.test(
|
||||
functionName: 'attributesPreview',
|
||||
isBuilder: false,
|
||||
name: 'Attributes preview',
|
||||
width: '100.0',
|
||||
height: '100',
|
||||
textScaleFactor: '2',
|
||||
wrapper: 'testWrapper',
|
||||
),
|
||||
PreviewDetails.test(
|
||||
functionName: 'MyWidget.preview',
|
||||
isBuilder: false,
|
||||
name: 'Constructor preview',
|
||||
),
|
||||
PreviewDetails.test(
|
||||
functionName: 'MyWidget.factoryPreview',
|
||||
isBuilder: false,
|
||||
name: 'Factory constructor preview',
|
||||
),
|
||||
PreviewDetails.test(
|
||||
functionName: 'MyWidget.previewStatic',
|
||||
isBuilder: false,
|
||||
name: 'Static preview',
|
||||
),
|
||||
];
|
||||
|
||||
// The initial mapping should be empty as there's no files containing previews.
|
||||
final PreviewMapping expectedInitialMapping = <PreviewPath, List<PreviewDetails>>{};
|
||||
|
||||
final Completer<void> completer = Completer<void>();
|
||||
late final PreviewPath previewContainingFilePath;
|
||||
onChangeDetected = (PreviewMapping updated) {
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
// The new previews in baz.dart should be included in the preview mapping.
|
||||
expect(stripNonDeterministicFields(updated), <PreviewPath, List<PreviewDetails>>{
|
||||
previewContainingFilePath: expectedPreviewDetails,
|
||||
});
|
||||
completer.complete();
|
||||
};
|
||||
|
||||
// Initialize the file watcher.
|
||||
final PreviewMapping initialPreviews = await previewDetector.initialize();
|
||||
expect(stripNonDeterministicFields(initialPreviews), expectedInitialMapping);
|
||||
|
||||
// Create baz.dart, which contains previews.
|
||||
previewContainingFilePath = addPreviewContainingFile(projectRoot, <String>['baz.dart']);
|
||||
await completer.future;
|
||||
});
|
||||
|
||||
testUsingContext('can detect changes in the pubspec.yaml', () async {
|
||||
// Create an initial pubspec.
|
||||
populatePubspec(projectRoot, 'abc');
|
||||
@ -144,7 +237,7 @@ void main() {
|
||||
completer.complete();
|
||||
};
|
||||
// Initialize the file watcher.
|
||||
final PreviewMapping initialPreviews = await previewDetector.initialize(projectRoot);
|
||||
final PreviewMapping initialPreviews = await previewDetector.initialize();
|
||||
expect(initialPreviews, isEmpty);
|
||||
|
||||
// Change the contents of the pubspec and verify the callback is invoked.
|
||||
@ -154,10 +247,55 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a copy of [mapping] with [PreviewDetails] entries that have non-deterministic values
|
||||
/// that differ per run (e.g., temporary file paths).
|
||||
PreviewMapping stripNonDeterministicFields(PreviewMapping mapping) {
|
||||
return mapping.map<PreviewPath, List<PreviewDetails>>((
|
||||
PreviewPath key,
|
||||
List<PreviewDetails> value,
|
||||
) {
|
||||
return MapEntry<PreviewPath, List<PreviewDetails>>(
|
||||
key,
|
||||
value.map((PreviewDetails details) => details.copyWith(wrapperLibraryUri: '')).toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const String previewContainingFileContents = '''
|
||||
@Preview()
|
||||
// This isn't necessarily valid code. We're just looking for the annotation
|
||||
WidgetPreview previews() => WidgetPreview();
|
||||
@Preview(name: 'Top-level preview')
|
||||
Widget previews() => Text('Foo');
|
||||
|
||||
@Preview(name: 'Builder preview')
|
||||
WidgetBuilder builderPreview() {
|
||||
return (BuildContext context) {
|
||||
return Text('Builder');
|
||||
};
|
||||
}
|
||||
|
||||
Widget testWrapper(Widget child) {
|
||||
return child;
|
||||
}
|
||||
|
||||
@Preview(name: 'Attributes preview', height: 100, width: 100.0, textScaleFactor: 2, wrapper: testWrapper)
|
||||
Widget attributesPreview() {
|
||||
return Text('Attributes');
|
||||
}
|
||||
|
||||
class MyWidget extends StatelessWidget {
|
||||
@Preview(name: 'Constructor preview')
|
||||
MyWidget.preview();
|
||||
|
||||
@Preview(name: 'Factory constructor preview')
|
||||
MyWidget.factoryPreview() => MyWidget.preview();
|
||||
|
||||
@Preview(name: 'Static preview')
|
||||
static Widget previewStatic() => Text('Static');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text('MyWidget');
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String nonPreviewContainingFileContents = '''
|
||||
|
@ -190,12 +190,12 @@ void main() {
|
||||
);
|
||||
|
||||
const String samplePreviewFile = '''
|
||||
@Preview()
|
||||
WidgetPreview preview() => const WidgetPreview(child: Text('Foo'));''';
|
||||
@Preview(name: 'preview')
|
||||
Widget preview() => Text('Foo');''';
|
||||
|
||||
const String expectedGeneratedFileContents = '''
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:flutter_project/foo.dart' as _i1;import 'package:flutter/widgets.dart';List<WidgetPreview> previews() => [_i1.preview()];''';
|
||||
import 'widget_preview.dart' as _i1;import 'package:flutter_project/foo.dart' as _i2;List<_i1.WidgetPreview> previews() => [_i1.WidgetPreview(name: 'preview', child: _i2.preview(), )];''';
|
||||
|
||||
testUsingContext(
|
||||
'start finds existing previews and injects them into ${PreviewCodeGenerator.generatedPreviewFilePath}',
|
||||
|
@ -40,28 +40,45 @@ void main() {
|
||||
expect(generatedPreviewFile, isNot(exists));
|
||||
|
||||
// Populate the generated preview file.
|
||||
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(<PreviewPath, List<String>>{
|
||||
(path: '', uri: Uri(path: 'foo.dart')): <String>['preview'],
|
||||
(path: '', uri: Uri(path: 'src/bar.dart')): <String>['barPreview1', 'barPreview2'],
|
||||
});
|
||||
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(
|
||||
<PreviewPath, List<PreviewDetails>>{
|
||||
(path: '', uri: Uri(path: 'foo.dart')): <PreviewDetails>[
|
||||
PreviewDetails(functionName: 'preview', isBuilder: false),
|
||||
],
|
||||
(path: '', uri: Uri(path: 'src/bar.dart')): <PreviewDetails>[
|
||||
PreviewDetails(functionName: 'barPreview1', isBuilder: false),
|
||||
PreviewDetails(functionName: 'barPreview2', isBuilder: false),
|
||||
PreviewDetails.test(
|
||||
functionName: 'barPreview3',
|
||||
isBuilder: true,
|
||||
name: 'Foo',
|
||||
width: '123',
|
||||
height: '456',
|
||||
textScaleFactor: '50',
|
||||
wrapper: 'wrapper',
|
||||
wrapperLibraryUri: 'wrapper.dart',
|
||||
),
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(generatedPreviewFile, exists);
|
||||
|
||||
// Check that the generated file contains:
|
||||
// - An import of the widget preview library
|
||||
// - Prefixed imports for both 'foo.dart' and 'src/bar.dart'
|
||||
// - A top-level function 'List<WidgetPreview> previews()'
|
||||
// - A returned list containing function calls to 'preview()' from 'foo.dart' and 'barPreview1()'
|
||||
// and 'barPreview2()' from 'src/bar.dart'
|
||||
// - A returned list containing function calls to 'preview()' from 'foo.dart' and
|
||||
// 'barPreview1()', 'barPreview2()', and 'barPreview3()' from 'src/bar.dart'
|
||||
//
|
||||
// The generated file is unfortunately unformatted.
|
||||
const String expectedGeneratedPreviewFileContents = '''
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'foo.dart' as _i1;import 'src/bar.dart' as _i2;import 'package:flutter/widgets.dart';List<WidgetPreview> previews() => [_i1.preview(), _i2.barPreview1(), _i2.barPreview2(), ];''';
|
||||
import 'widget_preview.dart' as _i1;import 'foo.dart' as _i2;import 'src/bar.dart' as _i3;import 'wrapper.dart' as _i4;import 'package:flutter/widgets.dart' as _i5;List<_i1.WidgetPreview> previews() => [_i1.WidgetPreview(child: _i2.preview()), _i1.WidgetPreview(child: _i3.barPreview1()), _i1.WidgetPreview(child: _i3.barPreview2()), _i1.WidgetPreview(name: Foo, height: 456.0, width: 123.0, textScaleFactor: 50.0, child: _i4.wrapper(_i5.Builder(builder: _i3.barPreview3())), ), ];''';
|
||||
expect(generatedPreviewFile.readAsStringSync(), expectedGeneratedPreviewFileContents);
|
||||
|
||||
// Regenerate the generated file with no previews.
|
||||
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(
|
||||
const <PreviewPath, List<String>>{},
|
||||
const <PreviewPath, List<PreviewDetails>>{},
|
||||
);
|
||||
expect(generatedPreviewFile, exists);
|
||||
|
||||
@ -69,7 +86,8 @@ import 'foo.dart' as _i1;import 'src/bar.dart' as _i2;import 'package:flutter/wi
|
||||
// - An import of the widget preview library
|
||||
// - A top-level function 'List<WidgetPreview> previews()' that returns an empty list.
|
||||
const String emptyGeneratedPreviewFileContents = '''
|
||||
import 'package:flutter/widgets.dart';List<WidgetPreview> previews() => [];''';
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'widget_preview.dart' as _i1;List<_i1.WidgetPreview> previews() => [];''';
|
||||
expect(generatedPreviewFile.readAsStringSync(), emptyGeneratedPreviewFileContents);
|
||||
},
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user