diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 087cc9e4f37..eaaf61d78d3 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -13,12 +13,21 @@ abstract class Route { List get overlayEntries => const []; /// Called when the route is inserted into the navigator. - /// Use this to install any overlays. + /// + /// Use this to populate overlayEntries and add them to the overlay. + /// (The reason the Route is responsible for doing this, rather than the + /// Navigator, is that the Route will be responsible for _removing_ the + /// entries and this way it's symmetric. + /// + /// The overlay argument will be null if this is the first route inserted. void install(OverlayState overlay, OverlayEntry insertionPoint) { } /// Called after install() when the route is pushed onto the navigator. void didPush() { } + /// Called after install() when the route replaced another in the navigator. + void didReplace(Route oldRoute) { } + /// A request was made to pop this route. If the route can handle it /// internally (e.g. because it has its own stack of internal state) then /// return false, otherwise return true. Returning false will prevent the @@ -35,6 +44,10 @@ abstract class Route { /// navigator. void didPopNext(Route nextRoute) { } + /// The given old route, which was the route that came after this one, has + /// been replaced with the given new route. + void didReplaceNext(Route oldNextRoute, Route newNextRoute) { } + /// The route should remove its overlays and free any other resources. /// /// A call to didPop() implies that the Route should call dispose() itself, @@ -173,6 +186,44 @@ class NavigatorState extends State { assert(() { _debugLocked = false; return true; }); } + /// Replaces one given route with another, but does not call didPush/didPop. + /// Instead, this calls install() on the new route, then didReplace() on the + /// new route passing the old route, then dispose() on the old route. + /// + /// The old route must have overlays, otherwise we won't know where to insert + /// the overlays of the new route. The old route must not be currently visible + /// (i.e. a later route have overlays that are currently opaque), otherwise + /// the replacement would have a jarring effect. + /// + /// It is safe to call this redundantly (replacing a route with itself). Such + /// calls are ignored. + void replace({ Route oldRoute, Route newRoute }) { + assert(!_debugLocked); + assert(oldRoute != null); + assert(newRoute != null); + if (oldRoute == newRoute) + return; + assert(() { _debugLocked = true; return true; }); + assert(oldRoute._navigator == this); + assert(newRoute._navigator == null); + assert(oldRoute.overlayEntries.isNotEmpty); + assert(newRoute.overlayEntries.isEmpty); + assert(!overlay.debugIsVisible(oldRoute.overlayEntries.last)); + setState(() { + int index = _history.indexOf(oldRoute); + assert(index >= 0); + newRoute._navigator = this; + newRoute.install(overlay, oldRoute.overlayEntries.last); + _history[index] = newRoute; + newRoute.didReplace(oldRoute); + if (index > 0) + _history[index - 1].didReplaceNext(oldRoute, newRoute); + oldRoute.dispose(); + oldRoute._navigator = null; + }); + assert(() { _debugLocked = false; return true; }); + } + /// Removes the current route, notifying the observer (if any), and the /// previous routes (using [Route.didPopNext]). /// diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index ab60513af8b..0a6c037a818 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -83,6 +83,26 @@ class OverlayState extends State { }); } + bool debugIsVisible(OverlayEntry entry) { + bool result = false; + assert(_entries.contains(entry)); + assert(() { + // This is an O(N) algorithm, and should not be necessary except for debug asserts. + // To avoid people depending on it, we only implement it in checked mode. + for (int i = _entries.length - 1; i > 0; i -= 1) { + OverlayEntry candidate = _entries[i]; + if (candidate == entry) { + result = true; + break; + } + if (entry.opaque) + break; + } + return true; + }); + return result; + } + Widget build(BuildContext context) { List backwardsChildren = []; diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index c27fa45539c..df2372a19d8 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -119,6 +119,13 @@ abstract class TransitionRoute extends OverlayRoute { super.didPush(); } + void didReplace(Route oldRoute) { + if (oldRoute is TransitionRoute) + _performance.progress = oldRoute._performance.progress; + _performance.addStatusListener(handleStatusChanged); + super.didReplace(oldRoute); + } + bool didPop(T result) { _result = result; _performance.reverse(); diff --git a/packages/unit/test/widget/routes_test.dart b/packages/unit/test/widget/routes_test.dart new file mode 100644 index 00000000000..7dc0d7ca54f --- /dev/null +++ b/packages/unit/test/widget/routes_test.dart @@ -0,0 +1,160 @@ +// Copyright 2015 The Chromium 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_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; +import 'package:test/test.dart'; + +final List results = []; + +class TestRoute extends Route { + TestRoute(this.name); + final String name; + + List get overlayEntries => _entries; + + List _entries = []; + + void log(String s) { + results.add('$name: $s'); + } + + void install(OverlayState overlay, OverlayEntry insertionPoint) { + log('install'); + OverlayEntry entry = new OverlayEntry( + builder: (BuildContext context) => new Container(), + opaque: true + ); + _entries.add(entry); + overlay?.insert(entry, above: insertionPoint); + } + + void didPush() { + log('didPush'); + } + + void didReplace(TestRoute oldRoute) { + log('didReplace ${oldRoute.name}'); + } + + bool didPop(String result) { + log('didPop $result'); + return super.didPop(result); + } + + void didPushNext(TestRoute nextRoute) { + log('didPushNext ${nextRoute.name}'); + } + + void didPopNext(TestRoute nextRoute) { + log('didPopNext ${nextRoute.name}'); + } + + void didReplaceNext(TestRoute oldNextRoute, TestRoute newNextRoute) { + log('didReplaceNext ${oldNextRoute.name} ${newNextRoute.name}'); + } + + void dispose() { + log('dispose'); + _entries.forEach((OverlayEntry entry) { entry.remove(); }); + _entries.clear(); + } + +} + +void runNavigatorTest( + WidgetTester tester, + NavigatorState host, + void test(NavigatorState transaction), + List expectations +) { + expect(host, isNotNull); + test(host); + expect(results, equals(expectations)); + results.clear(); + tester.pump(); +} + +void main() { + test('Route management', () { + testWidgets((WidgetTester tester) { + GlobalKey navigatorKey = new GlobalKey(); + tester.pumpWidget(new Navigator( + key: navigatorKey, + onGenerateRoute: (_) => new TestRoute('initial') + )); + NavigatorState host = navigatorKey.currentState; + runNavigatorTest( + tester, + host, + (NavigatorState transaction) { + }, + [ + 'initial: install', + 'initial: didPush', + ] + ); + TestRoute second; + runNavigatorTest( + tester, + host, + (NavigatorState transaction) { + transaction.push(second = new TestRoute('second')); + }, + [ + 'second: install', + 'second: didPush', + 'initial: didPushNext second', + ] + ); + runNavigatorTest( + tester, + host, + (NavigatorState transaction) { + transaction.push(new TestRoute('third')); + }, + [ + 'third: install', + 'third: didPush', + 'second: didPushNext third', + ] + ); + runNavigatorTest( + tester, + host, + (NavigatorState transaction) { + transaction.replace(oldRoute: second, newRoute: new TestRoute('two')); + }, + [ + 'two: install', + 'two: didReplace second', + 'initial: didReplaceNext second two', + 'second: dispose', + ] + ); + runNavigatorTest( + tester, + host, + (NavigatorState transaction) { + transaction.pop('hello'); + }, + [ + 'third: didPop hello', + 'two: didPopNext third', + ] + ); + runNavigatorTest( + tester, + host, + (NavigatorState transaction) { + transaction.pop('good bye'); + }, + [ + 'two: didPop good bye', + 'initial: didPopNext two', + ] + ); + }); + }); +}