Merge pull request #310 from HansMuller/snack_bar

showSnackBar() returns a Future, clears its placeholder

The returned Future completes after the snack bar has been dismissed.

Revised BottomSheet to ensure that its Future only runs after the bottom sheet has been dismissed.
This commit is contained in:
Hans Muller 2015-11-12 09:10:36 -08:00
commit 3f58d9b7c9
5 changed files with 56 additions and 13 deletions

View File

@ -77,10 +77,14 @@ class _BottomSheetRoute extends OverlayRoute {
} }
void didPop(dynamic result) { void didPop(dynamic result) {
completer.complete(result); void finish() {
performance.reverse().then((_) {
super.didPop(result); // clear the overlay entries super.didPop(result); // clear the overlay entries
}); completer.complete(result);
}
if (performance.isDismissed)
finish();
else
performance.reverse().then((_) { finish(); });
} }
String get debugLabel => '$runtimeType'; String get debugLabel => '$runtimeType';

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -92,12 +94,15 @@ class _SnackBar extends StatelessComponent {
} }
class _SnackBarRoute extends TransitionRoute { class _SnackBarRoute extends TransitionRoute {
_SnackBarRoute({ Completer completer }) : super(completer: completer);
bool get opaque => false; bool get opaque => false;
Duration get transitionDuration => const Duration(milliseconds: 200); Duration get transitionDuration => const Duration(milliseconds: 200);
} }
void showSnackBar({ BuildContext context, GlobalKey<PlaceholderState> placeholderKey, Widget content, List<SnackBarAction> actions }) { Future showSnackBar({ BuildContext context, GlobalKey<PlaceholderState> placeholderKey, Widget content, List<SnackBarAction> actions }) {
_SnackBarRoute route = new _SnackBarRoute(); final Completer completer = new Completer();
_SnackBarRoute route = new _SnackBarRoute(completer: completer);
_SnackBar snackBar = new _SnackBar( _SnackBar snackBar = new _SnackBar(
route: route, route: route,
content: content, content: content,
@ -105,4 +110,7 @@ void showSnackBar({ BuildContext context, GlobalKey<PlaceholderState> placeholde
); );
placeholderKey.currentState.child = snackBar; placeholderKey.currentState.child = snackBar;
Navigator.of(context).pushEphemeral(route); Navigator.of(context).pushEphemeral(route);
return completer.future.then((_) {
placeholderKey.currentState.child = null;
});
} }

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'basic.dart'; import 'basic.dart';
@ -46,6 +48,10 @@ class OverlayRoute extends Route {
// TODO(abarth): Should we add a type for the result? // TODO(abarth): Should we add a type for the result?
abstract class TransitionRoute extends OverlayRoute { abstract class TransitionRoute extends OverlayRoute {
TransitionRoute({ this.completer });
final Completer completer;
Duration get transitionDuration; Duration get transitionDuration;
bool get opaque; bool get opaque;
@ -86,6 +92,9 @@ abstract class TransitionRoute extends OverlayRoute {
void didPop(dynamic result) { void didPop(dynamic result) {
_result = result; _result = result;
if (completer != null)
_performance.reverse().then((_) { completer.complete(_result); });
else
_performance.reverse(); _performance.reverse();
} }

View File

@ -8,6 +8,8 @@ void main() {
test('Verify that a tap dismisses a modal BottomSheet', () { test('Verify that a tap dismisses a modal BottomSheet', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
BuildContext context; BuildContext context;
bool showBottomSheetThenCalled = false;
tester.pumpWidget(new MaterialApp( tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{ routes: <String, RouteBuilder>{
'/': (RouteArguments args) { '/': (RouteArguments args) {
@ -20,7 +22,10 @@ void main() {
tester.pump(); tester.pump();
expect(tester.findText('BottomSheet'), isNull); expect(tester.findText('BottomSheet'), isNull);
showModalBottomSheet(context: context, child: new Text('BottomSheet')); showModalBottomSheet(context: context, child: new Text('BottomSheet')).then((_) {
showBottomSheetThenCalled = true;
});
tester.pump(); // bottom sheet show animation starts tester.pump(); // bottom sheet show animation starts
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
expect(tester.findText('BottomSheet'), isNotNull); expect(tester.findText('BottomSheet'), isNotNull);
@ -29,7 +34,8 @@ void main() {
tester.tap(tester.findText('BottomSheet')); tester.tap(tester.findText('BottomSheet'));
tester.pump(); // bottom sheet dismiss animation starts tester.pump(); // bottom sheet dismiss animation starts
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
tester.pump(new Duration(seconds: 2)); // rebuild frame tester.pump(new Duration(seconds: 1)); // rebuild frame
expect(showBottomSheetThenCalled, isTrue);
expect(tester.findText('BottomSheet'), isNull); expect(tester.findText('BottomSheet'), isNull);
showModalBottomSheet(context: context, child: new Text('BottomSheet')); showModalBottomSheet(context: context, child: new Text('BottomSheet'));
@ -41,7 +47,7 @@ void main() {
tester.tapAt(new Point(20.0, 20.0)); tester.tapAt(new Point(20.0, 20.0));
tester.pump(); // bottom sheet dismiss animation starts tester.pump(); // bottom sheet dismiss animation starts
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
tester.pump(new Duration(seconds: 2)); // rebuild frame tester.pump(new Duration(seconds: 1)); // rebuild frame
expect(tester.findText('BottomSheet'), isNull); expect(tester.findText('BottomSheet'), isNull);
}); });
}); });
@ -50,6 +56,8 @@ void main() {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>(); GlobalKey<PlaceholderState> _bottomSheetPlaceholderKey = new GlobalKey<PlaceholderState>();
BuildContext context; BuildContext context;
bool showBottomSheetThenCalled = false;
tester.pumpWidget(new MaterialApp( tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{ routes: <String, RouteBuilder>{
'/': (RouteArguments args) { '/': (RouteArguments args) {
@ -69,7 +77,9 @@ void main() {
context: context, context: context,
child: new Container(child: new Text('BottomSheet'), margin: new EdgeDims.all(40.0)), child: new Container(child: new Text('BottomSheet'), margin: new EdgeDims.all(40.0)),
placeholderKey: _bottomSheetPlaceholderKey placeholderKey: _bottomSheetPlaceholderKey
); ).then((_) {
showBottomSheetThenCalled = true;
});
expect(_bottomSheetPlaceholderKey.currentState.child, isNotNull); expect(_bottomSheetPlaceholderKey.currentState.child, isNotNull);
tester.pump(); // bottom sheet show animation starts tester.pump(); // bottom sheet show animation starts
@ -79,7 +89,8 @@ void main() {
tester.fling(tester.findText('BottomSheet'), const Offset(0.0, 20.0), 1000.0); tester.fling(tester.findText('BottomSheet'), const Offset(0.0, 20.0), 1000.0);
tester.pump(); // bottom sheet dismiss animation starts tester.pump(); // bottom sheet dismiss animation starts
tester.pump(new Duration(seconds: 1)); // animation done tester.pump(new Duration(seconds: 1)); // animation done
tester.pump(new Duration(seconds: 2)); // rebuild frame without the bottom sheet tester.pump(new Duration(seconds: 1)); // rebuild frame without the bottom sheet
expect(showBottomSheetThenCalled, isTrue);
expect(tester.findText('BottomSheet'), isNull); expect(tester.findText('BottomSheet'), isNull);
expect(_bottomSheetPlaceholderKey.currentState.child, isNull); expect(_bottomSheetPlaceholderKey.currentState.child, isNull);
}); });

View File

@ -9,17 +9,22 @@ void main() {
String helloSnackBar = 'Hello SnackBar'; String helloSnackBar = 'Hello SnackBar';
GlobalKey<PlaceholderState> placeholderKey = new GlobalKey<PlaceholderState>(); GlobalKey<PlaceholderState> placeholderKey = new GlobalKey<PlaceholderState>();
Key tapTarget = new Key('tap-target'); Key tapTarget = new Key('tap-target');
BuildContext context;
bool showSnackBarThenCalled = false;
tester.pumpWidget(new MaterialApp( tester.pumpWidget(new MaterialApp(
routes: <String, RouteBuilder>{ routes: <String, RouteBuilder>{
'/': (RouteArguments args) { '/': (RouteArguments args) {
context = args.context;
return new GestureDetector( return new GestureDetector(
onTap: () { onTap: () {
showSnackBar( showSnackBar(
context: args.context, context: args.context,
placeholderKey: placeholderKey, placeholderKey: placeholderKey,
content: new Text(helloSnackBar) content: new Text(helloSnackBar)
); ).then((_) {
showSnackBarThenCalled = true;
});
}, },
child: new Container( child: new Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
@ -36,10 +41,16 @@ void main() {
)); ));
tester.tap(tester.findElementByKey(tapTarget)); tester.tap(tester.findElementByKey(tapTarget));
expect(tester.findText(helloSnackBar), isNull); expect(tester.findText(helloSnackBar), isNull);
tester.pump(); tester.pump();
expect(tester.findText(helloSnackBar), isNotNull); expect(tester.findText(helloSnackBar), isNotNull);
Navigator.of(context).pop();
expect(tester.findText(helloSnackBar), isNotNull);
tester.pump(new Duration(seconds: 1));
expect(showSnackBarThenCalled, isTrue);
expect(tester.findText(helloSnackBar), isNull);
expect(placeholderKey.currentState.child, isNull);
}); });
}); });
} }