[animations] Support for returning value from popped route (#153)
* Added support for returning value from popped route
* Example for `onClosed` added
diff --git a/packages/animations/example/lib/container_transition.dart b/packages/animations/example/lib/container_transition.dart
index 867ace0..155b5a4 100644
--- a/packages/animations/example/lib/container_transition.dart
+++ b/packages/animations/example/lib/container_transition.dart
@@ -49,6 +49,14 @@
class _OpenContainerTransformDemoState
extends State<OpenContainerTransformDemo> {
ContainerTransitionType _transitionType = ContainerTransitionType.fade;
+ final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
+
+ void _showMarkedAsDoneSnackbar(bool isMarkedAsDone) {
+ if (isMarkedAsDone ?? false)
+ scaffoldKey.currentState.showSnackBar(const SnackBar(
+ content: Text('Marked as done!'),
+ ));
+ }
void _showSettingsBottomModalSheet(BuildContext context) {
showModalBottomSheet<void>(
@@ -103,6 +111,7 @@
@override
Widget build(BuildContext context) {
return Scaffold(
+ key: scaffoldKey,
appBar: AppBar(
title: const Text('Container transform'),
actions: <Widget>[
@@ -122,6 +131,7 @@
closedBuilder: (BuildContext _, VoidCallback openContainer) {
return _ExampleCard(openContainer: openContainer);
},
+ onClosed: _showMarkedAsDoneSnackbar,
),
const SizedBox(height: 16.0),
_OpenContainerWrapper(
@@ -129,6 +139,7 @@
closedBuilder: (BuildContext _, VoidCallback openContainer) {
return _ExampleSingleTile(openContainer: openContainer);
},
+ onClosed: _showMarkedAsDoneSnackbar,
),
const SizedBox(height: 16.0),
Row(
@@ -142,6 +153,7 @@
subtitle: 'Secondary text',
);
},
+ onClosed: _showMarkedAsDoneSnackbar,
),
),
const SizedBox(width: 8.0),
@@ -154,6 +166,7 @@
subtitle: 'Secondary text',
);
},
+ onClosed: _showMarkedAsDoneSnackbar,
),
),
],
@@ -170,6 +183,7 @@
subtitle: 'Secondary',
);
},
+ onClosed: _showMarkedAsDoneSnackbar,
),
),
const SizedBox(width: 8.0),
@@ -182,6 +196,7 @@
subtitle: 'Secondary',
);
},
+ onClosed: _showMarkedAsDoneSnackbar,
),
),
const SizedBox(width: 8.0),
@@ -194,17 +209,19 @@
subtitle: 'Secondary',
);
},
+ onClosed: _showMarkedAsDoneSnackbar,
),
),
],
),
const SizedBox(height: 16.0),
...List<Widget>.generate(10, (int index) {
- return OpenContainer(
+ return OpenContainer<bool>(
transitionType: _transitionType,
openBuilder: (BuildContext _, VoidCallback openContainer) {
- return _DetailsPage();
+ return const _DetailsPage();
},
+ onClosed: _showMarkedAsDoneSnackbar,
tappable: false,
closedShape: const RoundedRectangleBorder(),
closedElevation: 0.0,
@@ -226,7 +243,9 @@
floatingActionButton: OpenContainer(
transitionType: _transitionType,
openBuilder: (BuildContext context, VoidCallback _) {
- return _DetailsPage();
+ return const _DetailsPage(
+ includeMarkAsDoneButton: false,
+ );
},
closedElevation: 6.0,
closedShape: const RoundedRectangleBorder(
@@ -256,18 +275,21 @@
const _OpenContainerWrapper({
this.closedBuilder,
this.transitionType,
+ this.onClosed,
});
final OpenContainerBuilder closedBuilder;
final ContainerTransitionType transitionType;
+ final ClosedCallback<bool> onClosed;
@override
Widget build(BuildContext context) {
- return OpenContainer(
+ return OpenContainer<bool>(
transitionType: transitionType,
openBuilder: (BuildContext context, VoidCallback _) {
- return _DetailsPage();
+ return const _DetailsPage();
},
+ onClosed: onClosed,
tappable: false,
closedBuilder: closedBuilder,
);
@@ -453,10 +475,24 @@
}
class _DetailsPage extends StatelessWidget {
+ const _DetailsPage({this.includeMarkAsDoneButton = true});
+
+ final bool includeMarkAsDoneButton;
+
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(title: const Text('Details page')),
+ appBar: AppBar(
+ title: const Text('Details page'),
+ actions: <Widget>[
+ if (includeMarkAsDoneButton)
+ IconButton(
+ icon: const Icon(Icons.done),
+ onPressed: () => Navigator.pop(context, true),
+ tooltip: 'Mark as done',
+ )
+ ],
+ ),
body: ListView(
children: <Widget>[
Container(
diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart
index c4a1f6f..5274322 100644
--- a/packages/animations/lib/src/open_container.dart
+++ b/packages/animations/lib/src/open_container.dart
@@ -5,13 +5,28 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
-/// Signature for a function that creates a [Widget] to be used within an
+/// Signature for `action` callback function provided to [OpenContainer.openBuilder].
+///
+/// Parameter `returnValue` is the value which will be provided to [OpenContainer.onClosed]
+/// when `action` is called.
+typedef CloseContainerActionCallback<S> = void Function({S returnValue});
+
+/// Signature for a function that creates a [Widget] in open state within an
/// [OpenContainer].
///
/// The `action` callback provided to [OpenContainer.openBuilder] can be used
-/// to close the container. The `action` callback provided to
-/// [OpenContainer.closedBuilder] can be used to open the container again.
-typedef OpenContainerBuilder = Widget Function(
+/// to close the container.
+typedef OpenContainerBuilder<S> = Widget Function(
+ BuildContext context,
+ CloseContainerActionCallback<S> action,
+);
+
+/// Signature for a function that creates a [Widget] in closed state within an
+/// [OpenContainer].
+///
+/// The `action` callback provided to [OpenContainer.closedBuilder] can be used
+/// to open the container.
+typedef CloseContainerBuilder = Widget Function(
BuildContext context,
VoidCallback action,
);
@@ -29,6 +44,10 @@
fadeThrough,
}
+/// Callback function which is called when the [OpenContainer]
+/// is closed.
+typedef ClosedCallback<S> = void Function(S data);
+
/// A container that grows to fill the screen to reveal new content when tapped.
///
/// While the container is closed, it shows the [Widget] returned by
@@ -45,17 +64,21 @@
/// [closedBuilder] exist in the tree at the same time. Therefore, the widgets
/// returned by these builders cannot include the same global key.
///
+/// `T` refers to the type of data returned by the route when the container
+/// is closed. This value can be accessed in the `onClosed` function.
+///
// TODO(goderbauer): Add example animations and sample code.
///
/// See also:
///
/// * [Transitions with animated containers](https://material.io/design/motion/choreography.html#transformation)
/// in the Material spec.
-class OpenContainer extends StatefulWidget {
+@optionalTypeArgs
+class OpenContainer<T extends Object> extends StatefulWidget {
/// Creates an [OpenContainer].
///
/// All arguments except for [key] must not be null. The arguments
- /// [closedBuilder] and [openBuilder] are required.
+ /// [openBuilder] and [closedBuilder] are required.
const OpenContainer({
Key key,
this.closedColor = Colors.white,
@@ -167,7 +190,13 @@
final ShapeBorder openShape;
/// Called when the container was popped and has returned to the closed state.
- final VoidCallback onClosed;
+ ///
+ /// The return value from the popped screen is passed to this function as an
+ /// argument.
+ ///
+ /// If no value is returned via [Navigator.pop] or [OpenContainer.openBuilder.action],
+ /// `null` will be returned by default.
+ final ClosedCallback<T> onClosed;
/// Called to obtain the child for the container in the closed state.
///
@@ -177,7 +206,7 @@
///
/// The `action` callback provided to the builder can be called to open the
/// container.
- final OpenContainerBuilder closedBuilder;
+ final CloseContainerBuilder closedBuilder;
/// Called to obtain the child for the container in the open state.
///
@@ -187,7 +216,7 @@
///
/// The `action` callback provided to the builder can be called to close the
/// container.
- final OpenContainerBuilder openBuilder;
+ final OpenContainerBuilder<T> openBuilder;
/// Whether the entire closed container can be tapped to open it.
///
@@ -218,10 +247,10 @@
final bool useRootNavigator;
@override
- _OpenContainerState createState() => _OpenContainerState();
+ _OpenContainerState<T> createState() => _OpenContainerState<T>();
}
-class _OpenContainerState extends State<OpenContainer> {
+class _OpenContainerState<T> extends State<OpenContainer<T>> {
// Key used in [_OpenContainerRoute] to hide the widget returned by
// [OpenContainer.openBuilder] in the source route while the container is
// opening/open. A copy of that widget is included in the
@@ -235,10 +264,10 @@
final GlobalKey _closedBuilderKey = GlobalKey();
Future<void> openContainer() async {
- await Navigator.of(
+ final T data = await Navigator.of(
context,
rootNavigator: widget.useRootNavigator,
- ).push(_OpenContainerRoute(
+ ).push(_OpenContainerRoute<T>(
closedColor: widget.closedColor,
openColor: widget.openColor,
closedElevation: widget.closedElevation,
@@ -254,7 +283,7 @@
useRootNavigator: widget.useRootNavigator,
));
if (widget.onClosed != null) {
- widget.onClosed();
+ widget.onClosed(data);
}
}
@@ -350,7 +379,7 @@
}
}
-class _OpenContainerRoute extends ModalRoute<void> {
+class _OpenContainerRoute<T> extends ModalRoute<T> {
_OpenContainerRoute({
@required this.closedColor,
@required this.openColor,
@@ -506,8 +535,8 @@
final Color openColor;
final double openElevation;
final ShapeBorder openShape;
- final OpenContainerBuilder closedBuilder;
- final OpenContainerBuilder openBuilder;
+ final CloseContainerBuilder closedBuilder;
+ final OpenContainerBuilder<T> openBuilder;
// See [_OpenContainerState._hideableKey].
final GlobalKey<_HideableState> hideableKey;
@@ -587,7 +616,7 @@
}
@override
- bool didPop(void result) {
+ bool didPop(T result) {
_takeMeasurements(
navigatorContext: subtreeContext,
delayForSourceRoute: true,
@@ -667,8 +696,8 @@
return wasInProgress && isInProgress;
}
- void closeContainer() {
- Navigator.of(subtreeContext).pop();
+ void closeContainer({T returnValue}) {
+ Navigator.of(subtreeContext).pop(returnValue);
}
@override
diff --git a/packages/animations/test/open_container_test.dart b/packages/animations/test/open_container_test.dart
index df54258..77e9eb9 100644
--- a/packages/animations/test/open_container_test.dart
+++ b/packages/animations/test/open_container_test.dart
@@ -1486,7 +1486,7 @@
(WidgetTester tester) async {
bool hasClosed = false;
final Widget openContainer = OpenContainer(
- onClosed: () {
+ onClosed: (dynamic _) {
hasClosed = true;
},
closedBuilder: (BuildContext context, VoidCallback action) {
@@ -1525,6 +1525,51 @@
expect(hasClosed, isTrue);
});
+ testWidgets(
+ 'onClosed callback receives popped value when container has closed',
+ (WidgetTester tester) async {
+ bool value = false;
+ final Widget openContainer = OpenContainer<bool>(
+ onClosed: (bool poppedValue) {
+ value = poppedValue;
+ },
+ closedBuilder: (BuildContext context, VoidCallback action) {
+ return GestureDetector(
+ onTap: action,
+ child: const Text('Closed'),
+ );
+ },
+ openBuilder:
+ (BuildContext context, CloseContainerActionCallback<bool> action) {
+ return GestureDetector(
+ onTap: () => action(returnValue: true),
+ child: const Text('Open'),
+ );
+ },
+ );
+
+ await tester.pumpWidget(
+ _boilerplate(child: openContainer),
+ );
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(value, isFalse);
+
+ await tester.tap(find.text('Closed'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsOneWidget);
+ expect(find.text('Closed'), findsNothing);
+
+ await tester.tap(find.text('Open'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Open'), findsNothing);
+ expect(find.text('Closed'), findsOneWidget);
+ expect(value, isTrue);
+ });
+
Widget _createRootNavigatorTest({
@required Key appKey,
@required Key nestedNavigatorKey,