mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add a "replace()" API for the navigator.
This lets you dynamically replace one route with another.
This commit is contained in:
parent
d293bb8cc8
commit
d12539793a
@ -13,12 +13,21 @@ abstract class Route<T> {
|
||||
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
|
||||
|
||||
/// 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<T> {
|
||||
/// 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<Navigator> {
|
||||
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]).
|
||||
///
|
||||
|
||||
@ -83,6 +83,26 @@ class OverlayState extends State<Overlay> {
|
||||
});
|
||||
}
|
||||
|
||||
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<Widget> backwardsChildren = <Widget>[];
|
||||
|
||||
|
||||
@ -119,6 +119,13 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
||||
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();
|
||||
|
||||
160
packages/unit/test/widget/routes_test.dart
Normal file
160
packages/unit/test/widget/routes_test.dart
Normal file
@ -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<String> results = <String>[];
|
||||
|
||||
class TestRoute extends Route<String> {
|
||||
TestRoute(this.name);
|
||||
final String name;
|
||||
|
||||
List<OverlayEntry> get overlayEntries => _entries;
|
||||
|
||||
List<OverlayEntry> _entries = <OverlayEntry>[];
|
||||
|
||||
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<String> expectations
|
||||
) {
|
||||
expect(host, isNotNull);
|
||||
test(host);
|
||||
expect(results, equals(expectations));
|
||||
results.clear();
|
||||
tester.pump();
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Route management', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
|
||||
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',
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user