Commit 0cadbce4 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added Navigator.removeRoute() (#10298)

parent 69c25424
......@@ -328,6 +328,10 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
child: menu,
);
}
void _dismiss() {
navigator?.removeRoute(this);
}
}
/// An item in a menu created by a [DropdownButton].
......@@ -486,7 +490,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
//TODO(hansmuller) if _dropDownRoute != null Navigator.remove(context, _dropdownRoute)
_removeDropdownRoute();
super.dispose();
}
......@@ -494,7 +498,11 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
// Defined by WidgetsBindingObserver
@override
void didChangeMetrics() {
//TODO(hansmuller) if _dropDownRoute != null Navigator.remove(context, _dropdownRoute)
_removeDropdownRoute();
}
void _removeDropdownRoute() {
_dropdownRoute?._dismiss();
_dropdownRoute = null;
}
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'binding.dart';
......@@ -231,12 +232,15 @@ class NavigatorObserver {
NavigatorState get navigator => _navigator;
NavigatorState _navigator;
/// The [Navigator] pushed the given route.
/// The [Navigator] pushed `route`.
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { }
/// The [Navigator] popped the given route.
/// The [Navigator] popped `route`.
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { }
/// The [Navigator] removed `route`.
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) { }
/// The [Navigator] is being controlled by a user gesture.
///
/// Used for the iOS back gesture.
......@@ -673,6 +677,19 @@ class Navigator extends StatefulWidget {
return Navigator.of(context).pushReplacement(route, result: result);
}
/// Immediately remove `route` and [Route.dispose] it.
///
/// The route's animation does not run and the future returned from pushing
/// the route will not complete. Ongoing input gestures are cancelled. If
/// the [Navigator] has any [Navigator.observers], they will be notified with
/// [NavigatorObserver.didRemove].
///
/// This method is used to dismiss dropdown menus that are up when the screen's
/// orientation changes.
static void removeRoute(BuildContext context, Route<dynamic> route) {
return Navigator.of(context).removeRoute(route);
}
/// The state from the closest instance of this class that encloses the given context.
///
/// Typical usage is as follows:
......@@ -1100,6 +1117,36 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return true;
}
/// Immediately remove `route` and [Route.dispose] it.
///
/// The route's animation does not run and the future returned from pushing
/// the route will not complete. Ongoing input gestures are cancelled. If
/// the [Navigator] has any [Navigator.observers], they will be notified with
/// [NavigatorObserver.didRemove].
///
/// This method is used to dismiss dropdown menus that are up when the screen's
/// orientation changes.
void removeRoute(Route<dynamic> route) {
assert(route != null);
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; });
assert(route._navigator == this);
final int index = _history.indexOf(route);
assert(index != -1);
final Route<dynamic> previousRoute = index > 0 ? _history[index - 1] : null;
final Route<dynamic> nextRoute = (index + 1 < _history.length) ? _history[index + 1] : null;
setState(() {
_history.removeAt(index);
previousRoute?.didChangeNext(nextRoute);
nextRoute?.didChangePrevious(previousRoute);
for (NavigatorObserver observer in widget.observers)
observer.didRemove(route, previousRoute);
route.dispose();
});
assert(() { _debugLocked = false; return true; });
_cancelActivePointers();
}
/// Complete the lifecycle for a route that has been popped off the navigator.
///
/// When the navigator pops a route, the navigator retains a reference to the
......@@ -1178,10 +1225,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
void _cancelActivePointers() {
// TODO(abarth): This mechanism is far from perfect. See https://github.com/flutter/flutter/issues/4770
final RenderAbsorbPointer absorber = _overlayKey.currentContext?.ancestorRenderObjectOfType(const TypeMatcher<RenderAbsorbPointer>());
setState(() {
absorber?.absorbing = true;
});
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
// If we're between frames (SchedulerPhase.idle) then absorb any
// subsequent pointers from this frame. The absorbing flag will be
// reset in the next frame, see build().
final RenderAbsorbPointer absorber = _overlayKey.currentContext?.ancestorRenderObjectOfType(const TypeMatcher<RenderAbsorbPointer>());
setState(() {
absorber?.absorbing = true;
});
}
for (int pointer in _activePointers.toList())
WidgetsBinding.instance.cancelPointer(pointer);
}
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' show window;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
......@@ -463,4 +464,16 @@ void main() {
expect(menuRect.bottomLeft, new Offset(800.0 - menuRect.width, 600.0));
expect(menuRect.bottomRight, const Offset(800.0, 600.0));
});
testWidgets('Dropdown menus are dismissed on screen orientation changes', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame());
await tester.tap(find.byType(dropdownButtonType));
await tester.pumpAndSettle();
expect(find.byType(ListView), findsOneWidget);
window.onMetricsChanged();
await tester.pump();
expect(find.byType(ListView, skipOffstage: false), findsNothing);
});
}
......@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
class FirstWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new GestureDetector(
return new GestureDetector(
onTap: () {
Navigator.pushNamed(context, '/second');
},
......@@ -85,12 +85,12 @@ class OnTapPage extends StatelessWidget {
}
}
typedef void OnPushed(Route<dynamic> route, Route<dynamic> previousRoute);
typedef void OnPopped(Route<dynamic> route, Route<dynamic> previousRoute);
typedef void OnObservation(Route<dynamic> route, Route<dynamic> previousRoute);
class TestObserver extends NavigatorObserver {
OnPushed onPushed;
OnPopped onPopped;
OnObservation onPushed;
OnObservation onPopped;
OnObservation onRemoved;
@override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
......@@ -105,6 +105,12 @@ class TestObserver extends NavigatorObserver {
onPopped(route, previousRoute);
}
}
@override
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
if (onRemoved != null)
onRemoved(route, previousRoute);
}
}
void main() {
......@@ -445,4 +451,120 @@ void main() {
final String replaceNamedValue = await value; // replaceNamed result was 'B'
expect(replaceNamedValue, 'B');
});
testWidgets('removeRoute', (WidgetTester tester) async {
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }),
'/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pushNamed(context, '/B'); }),
'/B': (BuildContext context) => const OnTapPage(id: 'B'),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
Route<String> removedRoute;
Route<String> previousRoute;
final TestObserver observer = new TestObserver()
..onRemoved = (Route<dynamic> route, Route<dynamic> previous) {
removedRoute = route;
previousRoute = previous;
};
await tester.pumpWidget(new MaterialApp(
navigatorObservers: <NavigatorObserver>[observer],
onGenerateRoute: (RouteSettings settings) {
routes[settings.name] = new PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
return pageBuilders[settings.name](context);
},
);
return routes[settings.name];
}
));
expect(find.text('/'), findsOneWidget);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsNothing);
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
expect(find.text('/'), findsNothing);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
await tester.tap(find.text('A')); // pushNamed('/B'), stack becomes /, /A, /B
await tester.pumpAndSettle();
expect(find.text('/'), findsNothing);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
// Verify that the navigator's stack is ordered as expected.
expect(routes['/'].isActive, true);
expect(routes['/A'].isActive, true);
expect(routes['/B'].isActive, true);
expect(routes['/'].isFirst, true);
expect(routes['/B'].isCurrent, true);
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
navigator.removeRoute(routes['/B']); // stack becomes /, /A
await tester.pump();
expect(find.text('/'), findsNothing);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
// Verify that the navigator's stack no longer includes /B
expect(routes['/'].isActive, true);
expect(routes['/A'].isActive, true);
expect(routes['/B'].isActive, false);
expect(routes['/'].isFirst, true);
expect(routes['/A'].isCurrent, true);
expect(removedRoute, routes['/B']);
expect(previousRoute, routes['/A']);
navigator.removeRoute(routes['/A']); // stack becomes just /
await tester.pump();
expect(find.text('/'), findsOneWidget);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsNothing);
// Verify that the navigator's stack no longer includes /A
expect(routes['/'].isActive, true);
expect(routes['/A'].isActive, false);
expect(routes['/B'].isActive, false);
expect(routes['/'].isFirst, true);
expect(routes['/'].isCurrent, true);
expect(removedRoute, routes['/A']);
expect(previousRoute, routes['/']);
});
testWidgets('remove a route whose value is awaited', (WidgetTester tester) async {
Future<String> pageValue;
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { pageValue = Navigator.pushNamed(context, '/A'); }),
'/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pop(context, 'A'); }),
};
final Map<String, Route<String>> routes = <String, Route<String>>{};
await tester.pumpWidget(new MaterialApp(
onGenerateRoute: (RouteSettings settings) {
routes[settings.name] = new PageRouteBuilder<String>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
return pageBuilders[settings.name](context);
},
);
return routes[settings.name];
}
));
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
await tester.pumpAndSettle();
pageValue.then((String value) { assert(false); });
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
navigator.removeRoute(routes['/A']); // stack becomes /, pageValue will not complete
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment