mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[ Widget Preview ] Add support for "soft" restart of individual previews (#166846)
A soft restart simply removes the previewed widget from the widget tree for a frame before re-inserting it on the next frame. This has the effect of re-running local initializers in `State` objects, which would normally require a hot restart to accomplish. This reduces the number of cases where a full hot restart of the preview environment would be needed to pick up state-related changes. Fixes https://github.com/flutter/flutter/issues/166450 **Updated Controls:**  **Demo:** updates a string initialized in `initState`, triggering a hot reload. The updated string isn't rendered until the restart button is pressed, causing the widget to be reloaded with the new state initialized. https://github.com/user-attachments/assets/ab4abf8a-7823-491a-ad90-f83d877600ec
This commit is contained in:
parent
97b5264fcc
commit
6dbbfe671f
@ -65,7 +65,7 @@ class ZoomControls extends StatelessWidget {
|
||||
_WidgetPreviewIconButton(
|
||||
tooltip: 'Reset zoom',
|
||||
onPressed: enabled ? _reset : null,
|
||||
icon: Icons.refresh,
|
||||
icon: Icons.zoom_out_map,
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -95,3 +95,32 @@ class ZoomControls extends StatelessWidget {
|
||||
_transformationController.value = Matrix4.identity();
|
||||
}
|
||||
}
|
||||
|
||||
/// A button that triggers a "soft" restart of a previewed widget.
|
||||
///
|
||||
/// A soft restart removes the previewed widget from the widget tree for a frame before
|
||||
/// re-inserting it on the next frame. This has the effect of re-running local initializers in
|
||||
/// State objects, which normally requires a hot restart to accomplish in a normal application.
|
||||
class SoftRestartButton extends StatelessWidget {
|
||||
const SoftRestartButton({
|
||||
super.key,
|
||||
required this.enabled,
|
||||
required this.softRestartListenable,
|
||||
});
|
||||
|
||||
final ValueNotifier<bool> softRestartListenable;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _WidgetPreviewIconButton(
|
||||
tooltip: 'Hot restart',
|
||||
onPressed: enabled ? _onRestart : null,
|
||||
icon: Icons.refresh,
|
||||
);
|
||||
}
|
||||
|
||||
void _onRestart() {
|
||||
softRestartListenable.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,12 +117,18 @@ class WidgetPreviewWidget extends StatefulWidget {
|
||||
final WidgetPreview preview;
|
||||
|
||||
@override
|
||||
State<WidgetPreviewWidget> createState() => _WidgetPreviewWidgetState();
|
||||
State<WidgetPreviewWidget> createState() => WidgetPreviewWidgetState();
|
||||
}
|
||||
|
||||
class _WidgetPreviewWidgetState extends State<WidgetPreviewWidget> {
|
||||
class WidgetPreviewWidgetState extends State<WidgetPreviewWidget> {
|
||||
final transformationController = TransformationController();
|
||||
final deviceOrientation = ValueNotifier<Orientation>(Orientation.portrait);
|
||||
final softRestartListenable = ValueNotifier<bool>(false);
|
||||
final key = GlobalKey();
|
||||
|
||||
/// Returns the last size of the previewed widget.
|
||||
Size get lastChildSize =>
|
||||
(key.currentContext!.findRenderObject() as RenderBox).size;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -140,19 +146,41 @@ class _WidgetPreviewWidgetState extends State<WidgetPreviewWidget> {
|
||||
);
|
||||
|
||||
bool errorThrownDuringTreeConstruction = false;
|
||||
Widget preview;
|
||||
// Catch any unhandled exceptions and display an error widget instead of taking
|
||||
// down the entire preview environment.
|
||||
try {
|
||||
preview = widget.preview.builder();
|
||||
} on Object catch (error, stackTrace) {
|
||||
errorThrownDuringTreeConstruction = true;
|
||||
preview = _WidgetPreviewErrorWidget(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
size: maxSizeConstraints.biggest,
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap the previewed widget with a ValueListenableBuilder responsible for performing a "soft"
|
||||
// restart.
|
||||
//
|
||||
// A soft restart simply removes the previewed widget from the widget tree for a frame before
|
||||
// re-inserting it on the next frame. This has the effect of re-running local initializers in
|
||||
// State objects, which normally requires a hot restart to accomplish in a normal application.
|
||||
Widget preview = ValueListenableBuilder<bool>(
|
||||
valueListenable: softRestartListenable,
|
||||
builder: (context, performRestart, _) {
|
||||
try {
|
||||
final previewWidget = Container(
|
||||
key: key,
|
||||
child: widget.preview.builder(),
|
||||
);
|
||||
if (performRestart) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Trigger a rebuild on the next frame to re-insert previewWidget.
|
||||
softRestartListenable.value = false;
|
||||
}, debugLabel: 'Soft Restart');
|
||||
return SizedBox.fromSize(size: lastChildSize);
|
||||
}
|
||||
return previewWidget;
|
||||
} on Object catch (error, stackTrace) {
|
||||
// Catch any unhandled exceptions and display an error widget instead of taking
|
||||
// down the entire preview environment.
|
||||
errorThrownDuringTreeConstruction = true;
|
||||
return _WidgetPreviewErrorWidget(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
size: maxSizeConstraints.biggest,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
preview = _WidgetPreviewWrapper(
|
||||
previewerConstraints: maxSizeConstraints,
|
||||
@ -184,13 +212,19 @@ class _WidgetPreviewWidgetState extends State<WidgetPreviewWidget> {
|
||||
const VerticalSpacer(),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
// If an unhandled exception was caught and we're displaying an error
|
||||
// widget, these controls should be disabled.
|
||||
// TODO(bkonyi): improve layout of controls.
|
||||
children: [
|
||||
ZoomControls(
|
||||
transformationController: transformationController,
|
||||
// If an unhandled exception was caught and we're displaying an error
|
||||
// widget, these controls should be disabled.
|
||||
enabled: !errorThrownDuringTreeConstruction,
|
||||
),
|
||||
const SizedBox(width: 30),
|
||||
SoftRestartButton(
|
||||
enabled: !errorThrownDuringTreeConstruction,
|
||||
softRestartListenable: softRestartListenable,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@ -6,11 +6,11 @@ import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path; // flutter_ignore: package_path_import
|
||||
|
||||
import 'widget_preview_scaffold/test/widget_preview_scaffold_test_utils.dart';
|
||||
import 'widget_preview_scaffold/test/widget_preview_scaffold_change_detector.dart';
|
||||
|
||||
/// Regenerates the widget_preview_scaffold if needed.
|
||||
void main() {
|
||||
if (WidgetPreviewScaffoldTestUtils.checkForTemplateUpdates(
|
||||
if (WidgetPreviewScaffoldChangeDetector.checkForTemplateUpdates(
|
||||
widgetPreviewScaffoldProject: Directory(
|
||||
Platform.script.resolve('widget_preview_scaffold/').path,
|
||||
),
|
||||
|
||||
@ -65,7 +65,7 @@ class ZoomControls extends StatelessWidget {
|
||||
_WidgetPreviewIconButton(
|
||||
tooltip: 'Reset zoom',
|
||||
onPressed: enabled ? _reset : null,
|
||||
icon: Icons.refresh,
|
||||
icon: Icons.zoom_out_map,
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -95,3 +95,32 @@ class ZoomControls extends StatelessWidget {
|
||||
_transformationController.value = Matrix4.identity();
|
||||
}
|
||||
}
|
||||
|
||||
/// A button that triggers a "soft" restart of a previewed widget.
|
||||
///
|
||||
/// A soft restart removes the previewed widget from the widget tree for a frame before
|
||||
/// re-inserting it on the next frame. This has the effect of re-running local initializers in
|
||||
/// State objects, which normally requires a hot restart to accomplish in a normal application.
|
||||
class SoftRestartButton extends StatelessWidget {
|
||||
const SoftRestartButton({
|
||||
super.key,
|
||||
required this.enabled,
|
||||
required this.softRestartListenable,
|
||||
});
|
||||
|
||||
final ValueNotifier<bool> softRestartListenable;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _WidgetPreviewIconButton(
|
||||
tooltip: 'Hot restart',
|
||||
onPressed: enabled ? _onRestart : null,
|
||||
icon: Icons.refresh,
|
||||
);
|
||||
}
|
||||
|
||||
void _onRestart() {
|
||||
softRestartListenable.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,12 +117,18 @@ class WidgetPreviewWidget extends StatefulWidget {
|
||||
final WidgetPreview preview;
|
||||
|
||||
@override
|
||||
State<WidgetPreviewWidget> createState() => _WidgetPreviewWidgetState();
|
||||
State<WidgetPreviewWidget> createState() => WidgetPreviewWidgetState();
|
||||
}
|
||||
|
||||
class _WidgetPreviewWidgetState extends State<WidgetPreviewWidget> {
|
||||
class WidgetPreviewWidgetState extends State<WidgetPreviewWidget> {
|
||||
final transformationController = TransformationController();
|
||||
final deviceOrientation = ValueNotifier<Orientation>(Orientation.portrait);
|
||||
final softRestartListenable = ValueNotifier<bool>(false);
|
||||
final key = GlobalKey();
|
||||
|
||||
/// Returns the last size of the previewed widget.
|
||||
Size get lastChildSize =>
|
||||
(key.currentContext!.findRenderObject() as RenderBox).size;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -140,19 +146,41 @@ class _WidgetPreviewWidgetState extends State<WidgetPreviewWidget> {
|
||||
);
|
||||
|
||||
bool errorThrownDuringTreeConstruction = false;
|
||||
Widget preview;
|
||||
// Catch any unhandled exceptions and display an error widget instead of taking
|
||||
// down the entire preview environment.
|
||||
try {
|
||||
preview = widget.preview.builder();
|
||||
} on Object catch (error, stackTrace) {
|
||||
errorThrownDuringTreeConstruction = true;
|
||||
preview = _WidgetPreviewErrorWidget(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
size: maxSizeConstraints.biggest,
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap the previewed widget with a ValueListenableBuilder responsible for performing a "soft"
|
||||
// restart.
|
||||
//
|
||||
// A soft restart simply removes the previewed widget from the widget tree for a frame before
|
||||
// re-inserting it on the next frame. This has the effect of re-running local initializers in
|
||||
// State objects, which normally requires a hot restart to accomplish in a normal application.
|
||||
Widget preview = ValueListenableBuilder<bool>(
|
||||
valueListenable: softRestartListenable,
|
||||
builder: (context, performRestart, _) {
|
||||
try {
|
||||
final previewWidget = Container(
|
||||
key: key,
|
||||
child: widget.preview.builder(),
|
||||
);
|
||||
if (performRestart) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Trigger a rebuild on the next frame to re-insert previewWidget.
|
||||
softRestartListenable.value = false;
|
||||
}, debugLabel: 'Soft Restart');
|
||||
return SizedBox.fromSize(size: lastChildSize);
|
||||
}
|
||||
return previewWidget;
|
||||
} on Object catch (error, stackTrace) {
|
||||
// Catch any unhandled exceptions and display an error widget instead of taking
|
||||
// down the entire preview environment.
|
||||
errorThrownDuringTreeConstruction = true;
|
||||
return _WidgetPreviewErrorWidget(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
size: maxSizeConstraints.biggest,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
preview = _WidgetPreviewWrapper(
|
||||
previewerConstraints: maxSizeConstraints,
|
||||
@ -184,13 +212,19 @@ class _WidgetPreviewWidgetState extends State<WidgetPreviewWidget> {
|
||||
const VerticalSpacer(),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
// If an unhandled exception was caught and we're displaying an error
|
||||
// widget, these controls should be disabled.
|
||||
// TODO(bkonyi): improve layout of controls.
|
||||
children: [
|
||||
ZoomControls(
|
||||
transformationController: transformationController,
|
||||
// If an unhandled exception was caught and we're displaying an error
|
||||
// widget, these controls should be disabled.
|
||||
enabled: !errorThrownDuringTreeConstruction,
|
||||
),
|
||||
const SizedBox(width: 30),
|
||||
SoftRestartButton(
|
||||
enabled: !errorThrownDuringTreeConstruction,
|
||||
softRestartListenable: softRestartListenable,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:widget_preview_scaffold/src/controls.dart';
|
||||
import 'package:widget_preview_scaffold/src/widget_preview.dart';
|
||||
import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart';
|
||||
|
||||
import 'widget_preview_scaffold_test_utils.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'Soft restart removes and re-inserts previewed widget into the widget tree',
|
||||
(tester) async {
|
||||
const String kTestText = 'Foo';
|
||||
final WidgetPreviewerWidgetScaffolding widgetPreview =
|
||||
WidgetPreviewerWidgetScaffolding(
|
||||
child: WidgetPreviewWidget(
|
||||
preview: WidgetPreview(builder: () => const Text(kTestText)),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(widgetPreview);
|
||||
final Finder softRestartButton = find.byType(SoftRestartButton);
|
||||
final WidgetPreviewWidgetState state = tester
|
||||
.state<WidgetPreviewWidgetState>(find.byWidget(widgetPreview.child));
|
||||
|
||||
bool removedFromTree = false;
|
||||
final Completer<void> completer = Completer<void>();
|
||||
state.softRestartListenable.addListener(() {
|
||||
if (state.softRestartListenable.value) {
|
||||
expect(removedFromTree, false);
|
||||
removedFromTree = true;
|
||||
} else {
|
||||
expect(removedFromTree, true);
|
||||
completer.complete();
|
||||
}
|
||||
});
|
||||
|
||||
// Start with the widget in the tree.
|
||||
expect(removedFromTree, false);
|
||||
final Size originalSize = state.lastChildSize;
|
||||
final Finder fooTextFinder = find.text(kTestText);
|
||||
expect(fooTextFinder, findsOne);
|
||||
|
||||
// Perform a "soft" restart and render a single frame.
|
||||
await tester.tap(softRestartButton);
|
||||
await tester.pump();
|
||||
|
||||
// The previewed widget should be replaced by a SizedBox of the same size for a single frame.
|
||||
final Finder placeholderBoxFinder = find.byWidgetPredicate(
|
||||
(Widget widget) =>
|
||||
widget is SizedBox &&
|
||||
widget.height == originalSize.height &&
|
||||
widget.width == originalSize.width,
|
||||
);
|
||||
expect(placeholderBoxFinder, findsOne);
|
||||
expect(fooTextFinder, findsNothing);
|
||||
expect(removedFromTree, true);
|
||||
|
||||
// Render another frame and verify the previewed widget is added back to the tree.
|
||||
await tester.pump();
|
||||
expect(placeholderBoxFinder, findsNothing);
|
||||
expect(fooTextFinder, findsOne);
|
||||
|
||||
await completer.future;
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -6,11 +6,11 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'widget_preview_scaffold_test_utils.dart';
|
||||
import 'widget_preview_scaffold_change_detector.dart';
|
||||
|
||||
void main() {
|
||||
test('Widget Preview Scaffold template change detection', () {
|
||||
if (WidgetPreviewScaffoldTestUtils.checkForTemplateUpdates(
|
||||
if (WidgetPreviewScaffoldChangeDetector.checkForTemplateUpdates(
|
||||
widgetPreviewScaffoldProject: Directory(
|
||||
Platform.script.resolve('.').path,
|
||||
),
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
// 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 'dart:io';
|
||||
|
||||
abstract class WidgetPreviewScaffoldChangeDetector {
|
||||
static final Set<String> _ignoreDiffSet = <String>{
|
||||
// The pubspec can't be compared directly to the template since the SDK version is populated
|
||||
// when the template is hydrated based on the current SDK version.
|
||||
'pubspec.yaml',
|
||||
'lib/src/generated_preview.dart',
|
||||
};
|
||||
|
||||
/// Checks to see if the widget_preview_scaffold template files have been updated.
|
||||
///
|
||||
/// Returns true if the widget_preview_scaffold project should be regenerated.
|
||||
static bool checkForTemplateUpdates({
|
||||
required Directory widgetPreviewScaffoldProject,
|
||||
required Directory widgetPreviewScaffoldTemplateDir,
|
||||
}) {
|
||||
bool updateDetected = false;
|
||||
for (final FileSystemEntity entity in Directory(
|
||||
widgetPreviewScaffoldTemplateDir.absolute.path,
|
||||
).listSync(recursive: true)) {
|
||||
final String scaffoldPath =
|
||||
entity.path
|
||||
.replaceAll('.tmpl', '')
|
||||
.split('widget_preview_scaffold/')
|
||||
.last;
|
||||
if (_ignoreDiffSet.contains(scaffoldPath)) {
|
||||
continue;
|
||||
}
|
||||
final String resolvedScaffoldPath =
|
||||
'${widgetPreviewScaffoldProject.absolute.path}$scaffoldPath';
|
||||
if (entity is Directory) {
|
||||
if (!Directory(resolvedScaffoldPath).existsSync()) {
|
||||
stdout.writeln(
|
||||
'ERROR: Failed to find directory at $resolvedScaffoldPath.',
|
||||
);
|
||||
updateDetected = true;
|
||||
}
|
||||
} else if (entity is File) {
|
||||
final File scaffoldFile = File(resolvedScaffoldPath);
|
||||
if (!scaffoldFile.existsSync()) {
|
||||
stdout.writeln(
|
||||
'ERROR: Failed to find file at $resolvedScaffoldPath.',
|
||||
);
|
||||
updateDetected = true;
|
||||
continue;
|
||||
}
|
||||
final String templateContent = entity.readAsStringSync();
|
||||
final String scaffoldContent = scaffoldFile.readAsStringSync();
|
||||
if (templateContent != scaffoldContent) {
|
||||
stdout.writeln(
|
||||
'ERROR: The contents of $resolvedScaffoldPath do not match the contents of the template at '
|
||||
'${entity.path}.',
|
||||
);
|
||||
updateDetected = true;
|
||||
}
|
||||
} else {
|
||||
throw StateError(
|
||||
'Unexpected FileSystemEntity type: ${entity.runtimeType}',
|
||||
);
|
||||
}
|
||||
}
|
||||
return updateDetected;
|
||||
}
|
||||
}
|
||||
@ -2,68 +2,37 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart';
|
||||
|
||||
abstract class WidgetPreviewScaffoldTestUtils {
|
||||
static final Set<String> _ignoreDiffSet = <String>{
|
||||
// The pubspec can't be compared directly to the template since the SDK version is populated
|
||||
// when the template is hydrated based on the current SDK version.
|
||||
'pubspec.yaml',
|
||||
'lib/src/generated_preview.dart',
|
||||
};
|
||||
class WidgetPreviewerWidgetScaffolding extends StatelessWidget {
|
||||
const WidgetPreviewerWidgetScaffolding({super.key, required this.child});
|
||||
|
||||
/// Checks to see if the widget_preview_scaffold template files have been updated.
|
||||
///
|
||||
/// Returns true if the widget_preview_scaffold project should be regenerated.
|
||||
static bool checkForTemplateUpdates({
|
||||
required Directory widgetPreviewScaffoldProject,
|
||||
required Directory widgetPreviewScaffoldTemplateDir,
|
||||
}) {
|
||||
bool updateDetected = false;
|
||||
for (final FileSystemEntity entity in Directory(
|
||||
widgetPreviewScaffoldTemplateDir.absolute.path,
|
||||
).listSync(recursive: true)) {
|
||||
final String scaffoldPath =
|
||||
entity.path
|
||||
.replaceAll('.tmpl', '')
|
||||
.split('widget_preview_scaffold/')
|
||||
.last;
|
||||
if (_ignoreDiffSet.contains(scaffoldPath)) {
|
||||
continue;
|
||||
}
|
||||
final String resolvedScaffoldPath =
|
||||
'${widgetPreviewScaffoldProject.absolute.path}$scaffoldPath';
|
||||
if (entity is Directory) {
|
||||
if (!Directory(resolvedScaffoldPath).existsSync()) {
|
||||
stdout.writeln(
|
||||
'ERROR: Failed to find directory at $resolvedScaffoldPath.',
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WidgetsApp(
|
||||
color: Colors.blue,
|
||||
home: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return WidgetPreviewerWindowConstraints(
|
||||
constraints: constraints,
|
||||
child: child,
|
||||
);
|
||||
updateDetected = true;
|
||||
}
|
||||
} else if (entity is File) {
|
||||
final File scaffoldFile = File(resolvedScaffoldPath);
|
||||
if (!scaffoldFile.existsSync()) {
|
||||
stdout.writeln(
|
||||
'ERROR: Failed to find file at $resolvedScaffoldPath.',
|
||||
);
|
||||
updateDetected = true;
|
||||
continue;
|
||||
}
|
||||
final String templateContent = entity.readAsStringSync();
|
||||
final String scaffoldContent = scaffoldFile.readAsStringSync();
|
||||
if (templateContent != scaffoldContent) {
|
||||
stdout.writeln(
|
||||
'ERROR: The contents of $resolvedScaffoldPath do not match the contents of the template at '
|
||||
'${entity.path}.',
|
||||
);
|
||||
updateDetected = true;
|
||||
}
|
||||
} else {
|
||||
throw StateError(
|
||||
'Unexpected FileSystemEntity type: ${entity.runtimeType}',
|
||||
);
|
||||
}
|
||||
}
|
||||
return updateDetected;
|
||||
},
|
||||
),
|
||||
pageRouteBuilder:
|
||||
<T>(RouteSettings settings, WidgetBuilder builder) =>
|
||||
PageRouteBuilder<T>(
|
||||
settings: settings,
|
||||
pageBuilder:
|
||||
(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) => builder(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user