Reland "Fixes zero route transition duration crash" (#94575)
* Reland "Fixes zero route transition duration crash (#90461)" This reverts commit 403c1de573f38104ba7e26623884081973435c88. * Fix paged base route pop before push finishes crashes
This commit is contained in:
parent
f4a44a9d68
commit
b4023c3439
@ -2697,7 +2697,10 @@ class Navigator extends StatefulWidget {
|
|||||||
// \ / /
|
// \ / /
|
||||||
// idle--+-----+
|
// idle--+-----+
|
||||||
// / \
|
// / \
|
||||||
// / \
|
// / +------+
|
||||||
|
// / | |
|
||||||
|
// / | complete*
|
||||||
|
// | | /
|
||||||
// pop* remove*
|
// pop* remove*
|
||||||
// / \
|
// / \
|
||||||
// / removing#
|
// / removing#
|
||||||
@ -2735,6 +2738,7 @@ enum _RouteLifecycle {
|
|||||||
//
|
//
|
||||||
// routes that should be included in route announcement and should still listen to transition changes.
|
// routes that should be included in route announcement and should still listen to transition changes.
|
||||||
pop, // we'll want to call didPop
|
pop, // we'll want to call didPop
|
||||||
|
complete, // we'll want to call didComplete,
|
||||||
remove, // we'll want to run didReplace/didRemove etc
|
remove, // we'll want to run didReplace/didRemove etc
|
||||||
// routes should not be included in route announcement but should still listen to transition changes.
|
// routes should not be included in route announcement but should still listen to transition changes.
|
||||||
popping, // we're waiting for the route to call finalizeRoute to switch to dispose
|
popping, // we're waiting for the route to call finalizeRoute to switch to dispose
|
||||||
@ -2870,14 +2874,38 @@ class _RouteEntry extends RouteTransitionRecord {
|
|||||||
lastAnnouncedPoppedNextRoute = poppedRoute;
|
lastAnnouncedPoppedNextRoute = poppedRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handlePop({ required NavigatorState navigator, required Route<dynamic>? previousPresent }) {
|
/// Process the to-be-popped route.
|
||||||
|
///
|
||||||
|
/// A route can be marked for pop by transition delegate or Navigator.pop,
|
||||||
|
/// this method actually pops the route by calling Route.didPop.
|
||||||
|
///
|
||||||
|
/// Returns true if the route is popped; otherwise, returns false if the route
|
||||||
|
/// refuses to be popped.
|
||||||
|
bool handlePop({ required NavigatorState navigator, required Route<dynamic>? previousPresent }) {
|
||||||
assert(navigator != null);
|
assert(navigator != null);
|
||||||
assert(navigator._debugLocked);
|
assert(navigator._debugLocked);
|
||||||
assert(route._navigator == navigator);
|
assert(route._navigator == navigator);
|
||||||
currentState = _RouteLifecycle.popping;
|
currentState = _RouteLifecycle.popping;
|
||||||
navigator._observedRouteDeletions.add(
|
if (route._popCompleter.isCompleted) {
|
||||||
_NavigatorPopObservation(route, previousPresent),
|
// This is a page-based route popped through the Navigator.pop. The
|
||||||
);
|
// didPop should have been called. No further action is needed.
|
||||||
|
assert(hasPage);
|
||||||
|
assert(pendingResult == null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!route.didPop(pendingResult)) {
|
||||||
|
currentState = _RouteLifecycle.idle;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pendingResult = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleComplete() {
|
||||||
|
route.didComplete(pendingResult);
|
||||||
|
pendingResult = null;
|
||||||
|
assert(route._popCompleter.isCompleted); // implies didComplete was called
|
||||||
|
currentState = _RouteLifecycle.remove;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleRemoval({ required NavigatorState navigator, required Route<dynamic>? previousPresent }) {
|
void handleRemoval({ required NavigatorState navigator, required Route<dynamic>? previousPresent }) {
|
||||||
@ -2892,8 +2920,6 @@ class _RouteEntry extends RouteTransitionRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool doingPop = false;
|
|
||||||
|
|
||||||
void didAdd({ required NavigatorState navigator, required bool isNewFirst}) {
|
void didAdd({ required NavigatorState navigator, required bool isNewFirst}) {
|
||||||
route.didAdd();
|
route.didAdd();
|
||||||
currentState = _RouteLifecycle.idle;
|
currentState = _RouteLifecycle.idle;
|
||||||
@ -2902,14 +2928,13 @@ class _RouteEntry extends RouteTransitionRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object? pendingResult;
|
||||||
|
|
||||||
void pop<T>(T? result) {
|
void pop<T>(T? result) {
|
||||||
assert(isPresent);
|
assert(isPresent);
|
||||||
doingPop = true;
|
pendingResult = result;
|
||||||
if (route.didPop(result) && doingPop) {
|
|
||||||
currentState = _RouteLifecycle.pop;
|
currentState = _RouteLifecycle.pop;
|
||||||
}
|
}
|
||||||
doingPop = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _reportRemovalToObserver = true;
|
bool _reportRemovalToObserver = true;
|
||||||
|
|
||||||
@ -2938,9 +2963,8 @@ class _RouteEntry extends RouteTransitionRecord {
|
|||||||
return;
|
return;
|
||||||
assert(isPresent);
|
assert(isPresent);
|
||||||
_reportRemovalToObserver = !isReplaced;
|
_reportRemovalToObserver = !isReplaced;
|
||||||
route.didComplete(result);
|
pendingResult = result;
|
||||||
assert(route._popCompleter.isCompleted); // implies didComplete was called
|
currentState = _RouteLifecycle.complete;
|
||||||
currentState = _RouteLifecycle.remove;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void finalize() {
|
void finalize() {
|
||||||
@ -3763,8 +3787,11 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
assert(() { _debugLocked = false; return true; }());
|
assert(() { _debugLocked = false; return true; }());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _flushingHistory = false;
|
||||||
|
|
||||||
void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
|
void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
|
||||||
assert(_debugLocked && !_debugUpdatingPage);
|
assert(_debugLocked && !_debugUpdatingPage);
|
||||||
|
_flushingHistory = true;
|
||||||
// Clean up the list, sending updates to the routes that changed. Notably,
|
// Clean up the list, sending updates to the routes that changed. Notably,
|
||||||
// we don't send the didChangePrevious/didChangeNext updates to those that
|
// we don't send the didChangePrevious/didChangeNext updates to those that
|
||||||
// did not change at this point, because we're not yet sure exactly what the
|
// did not change at this point, because we're not yet sure exactly what the
|
||||||
@ -3828,21 +3855,35 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
canRemoveOrAdd = true;
|
canRemoveOrAdd = true;
|
||||||
break;
|
break;
|
||||||
case _RouteLifecycle.pop:
|
case _RouteLifecycle.pop:
|
||||||
|
if (!entry.handlePop(
|
||||||
|
navigator: this,
|
||||||
|
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route)){
|
||||||
|
assert(entry.currentState == _RouteLifecycle.idle);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!seenTopActiveRoute) {
|
if (!seenTopActiveRoute) {
|
||||||
if (poppedRoute != null)
|
if (poppedRoute != null)
|
||||||
entry.handleDidPopNext(poppedRoute);
|
entry.handleDidPopNext(poppedRoute);
|
||||||
poppedRoute = entry.route;
|
poppedRoute = entry.route;
|
||||||
}
|
}
|
||||||
entry.handlePop(
|
_observedRouteDeletions.add(
|
||||||
navigator: this,
|
_NavigatorPopObservation(entry.route, _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route),
|
||||||
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
|
|
||||||
);
|
);
|
||||||
|
if (entry.currentState == _RouteLifecycle.dispose) {
|
||||||
|
// The pop finished synchronously. This can happen if transition
|
||||||
|
// duration is zero.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
assert(entry.currentState == _RouteLifecycle.popping);
|
assert(entry.currentState == _RouteLifecycle.popping);
|
||||||
canRemoveOrAdd = true;
|
canRemoveOrAdd = true;
|
||||||
break;
|
break;
|
||||||
case _RouteLifecycle.popping:
|
case _RouteLifecycle.popping:
|
||||||
// Will exit this state when animation completes.
|
// Will exit this state when animation completes.
|
||||||
break;
|
break;
|
||||||
|
case _RouteLifecycle.complete:
|
||||||
|
entry.handleComplete();
|
||||||
|
assert(entry.currentState == _RouteLifecycle.remove);
|
||||||
|
continue;
|
||||||
case _RouteLifecycle.remove:
|
case _RouteLifecycle.remove:
|
||||||
if (!seenTopActiveRoute) {
|
if (!seenTopActiveRoute) {
|
||||||
if (poppedRoute != null)
|
if (poppedRoute != null)
|
||||||
@ -3877,7 +3918,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
entry = previous;
|
entry = previous;
|
||||||
previous = index > 0 ? _history[index - 1] : null;
|
previous = index > 0 ? _history[index - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Informs navigator observers about route changes.
|
// Informs navigator observers about route changes.
|
||||||
_flushObserverNotifications();
|
_flushObserverNotifications();
|
||||||
|
|
||||||
@ -3910,6 +3950,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
if (bucket != null) {
|
if (bucket != null) {
|
||||||
_serializableHistory.update(_history);
|
_serializableHistory.update(_history);
|
||||||
}
|
}
|
||||||
|
_flushingHistory = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _flushObserverNotifications() {
|
void _flushObserverNotifications() {
|
||||||
@ -4846,17 +4887,18 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
}());
|
}());
|
||||||
final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
|
final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
|
||||||
if (entry.hasPage) {
|
if (entry.hasPage) {
|
||||||
if (widget.onPopPage!(entry.route, result))
|
if (widget.onPopPage!(entry.route, result) && entry.currentState == _RouteLifecycle.idle) {
|
||||||
|
// The entry may have been disposed if the pop finishes synchronously.
|
||||||
|
assert(entry.route._popCompleter.isCompleted);
|
||||||
entry.currentState = _RouteLifecycle.pop;
|
entry.currentState = _RouteLifecycle.pop;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
entry.pop<T>(result);
|
entry.pop<T>(result);
|
||||||
|
assert (entry.currentState == _RouteLifecycle.pop);
|
||||||
}
|
}
|
||||||
if (entry.currentState == _RouteLifecycle.pop) {
|
if (entry.currentState == _RouteLifecycle.pop)
|
||||||
// Flush the history if the route actually wants to be popped (the pop
|
|
||||||
// wasn't handled internally).
|
|
||||||
_flushHistoryUpdates(rearrangeOverlay: false);
|
_flushHistoryUpdates(rearrangeOverlay: false);
|
||||||
assert(entry.route._popCompleter.isCompleted);
|
assert(entry.currentState == _RouteLifecycle.idle || entry.route._popCompleter.isCompleted);
|
||||||
}
|
|
||||||
assert(() {
|
assert(() {
|
||||||
_debugLocked = false;
|
_debugLocked = false;
|
||||||
return true;
|
return true;
|
||||||
@ -4972,15 +5014,16 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
assert(() { wasDebugLocked = _debugLocked; _debugLocked = true; return true; }());
|
assert(() { wasDebugLocked = _debugLocked; _debugLocked = true; return true; }());
|
||||||
assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);
|
assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);
|
||||||
final _RouteEntry entry = _history.firstWhere(_RouteEntry.isRoutePredicate(route));
|
final _RouteEntry entry = _history.firstWhere(_RouteEntry.isRoutePredicate(route));
|
||||||
if (entry.doingPop) {
|
// For page-based route, the didPop can be called on any life cycle above
|
||||||
// We were called synchronously from Route.didPop(), but didn't process
|
// pop.
|
||||||
// the pop yet. Let's do that now before finalizing.
|
assert(entry.currentState == _RouteLifecycle.popping ||
|
||||||
entry.currentState = _RouteLifecycle.pop;
|
(entry.hasPage && entry.currentState.index < _RouteLifecycle.pop.index));
|
||||||
_flushHistoryUpdates(rearrangeOverlay: false);
|
|
||||||
}
|
|
||||||
assert(entry.currentState != _RouteLifecycle.pop);
|
|
||||||
entry.finalize();
|
entry.finalize();
|
||||||
|
// finalizeRoute can be called during _flushHistoryUpdates if a pop
|
||||||
|
// finishes synchronously.
|
||||||
|
if (!_flushingHistory)
|
||||||
_flushHistoryUpdates(rearrangeOverlay: false);
|
_flushHistoryUpdates(rearrangeOverlay: false);
|
||||||
|
|
||||||
assert(() { _debugLocked = wasDebugLocked!; return true; }());
|
assert(() { _debugLocked = wasDebugLocked!; return true; }());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,9 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||||||
//
|
//
|
||||||
// This situation arises when dealing with the Cupertino dismiss gesture.
|
// This situation arises when dealing with the Cupertino dismiss gesture.
|
||||||
@override
|
@override
|
||||||
bool get finishedWhenPopped => _controller!.status == AnimationStatus.dismissed;
|
bool get finishedWhenPopped => _controller!.status == AnimationStatus.dismissed && !_popFinalized;
|
||||||
|
|
||||||
|
bool _popFinalized = false;
|
||||||
|
|
||||||
/// The animation that drives the route's transition and the previous route's
|
/// The animation that drives the route's transition and the previous route's
|
||||||
/// forward transition.
|
/// forward transition.
|
||||||
@ -206,6 +208,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||||||
// removing the route and disposing it.
|
// removing the route and disposing it.
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
navigator!.finalizeRoute(this);
|
navigator!.finalizeRoute(this);
|
||||||
|
_popFinalized = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -544,6 +544,44 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Page-based route pop before push finishes', (WidgetTester tester) async {
|
||||||
|
List<Page<void>> pages = <Page<void>>[const MaterialPage<void>(child: Text('Page 1'))];
|
||||||
|
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
||||||
|
Widget buildNavigator() {
|
||||||
|
return Navigator(
|
||||||
|
key: navigator,
|
||||||
|
pages: pages,
|
||||||
|
onPopPage: (Route<dynamic> route, dynamic result) {
|
||||||
|
pages.removeLast();
|
||||||
|
return route.didPop(result);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: buildNavigator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(find.text('Page 1'), findsOneWidget);
|
||||||
|
pages = pages.toList();
|
||||||
|
pages.add(const MaterialPage<void>(child: Text('Page 2')));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: buildNavigator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// This test should finish without crashing.
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
navigator.currentState!.pop();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page 1'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Pages update does update overlay correctly', (WidgetTester tester) async {
|
testWidgets('Pages update does update overlay correctly', (WidgetTester tester) async {
|
||||||
// Regression Test for https://github.com/flutter/flutter/issues/64941.
|
// Regression Test for https://github.com/flutter/flutter/issues/64941.
|
||||||
List<Page<void>> pages = const <Page<void>>[
|
List<Page<void>> pages = const <Page<void>>[
|
||||||
@ -2969,6 +3007,32 @@ void main() {
|
|||||||
expect(primaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
|
expect(primaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Pop no animation page does not crash', (WidgetTester tester) async {
|
||||||
|
// Regression Test for https://github.com/flutter/flutter/issues/86604.
|
||||||
|
Widget buildNavigator(bool secondPage) {
|
||||||
|
return Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Navigator(
|
||||||
|
pages: <Page<void>>[
|
||||||
|
const ZeroDurationPage(
|
||||||
|
child: Text('page1'),
|
||||||
|
),
|
||||||
|
if (secondPage)
|
||||||
|
const ZeroDurationPage(
|
||||||
|
child: Text('page2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onPopPage: (Route<dynamic> route, dynamic result) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await tester.pumpWidget(buildNavigator(true));
|
||||||
|
expect(find.text('page2'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildNavigator(false));
|
||||||
|
expect(find.text('page1'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('can work with pageless route', (WidgetTester tester) async {
|
testWidgets('can work with pageless route', (WidgetTester tester) async {
|
||||||
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
|
||||||
List<TestPage> myPages = <TestPage>[
|
List<TestPage> myPages = <TestPage>[
|
||||||
@ -3899,6 +3963,9 @@ class NavigatorObservation {
|
|||||||
final String? previous;
|
final String? previous;
|
||||||
final String? current;
|
final String? current;
|
||||||
final String operation;
|
final String operation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NavigatorObservation($operation, $current, $previous)';
|
||||||
}
|
}
|
||||||
|
|
||||||
class BuilderPage extends Page<void> {
|
class BuilderPage extends Page<void> {
|
||||||
@ -3914,3 +3981,43 @@ class BuilderPage extends Page<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ZeroDurationPage extends Page<void> {
|
||||||
|
const ZeroDurationPage({required this.child});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Route<void> createRoute(BuildContext context) {
|
||||||
|
return ZeroDurationPageRoute(page: this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZeroDurationPageRoute extends PageRoute<void> {
|
||||||
|
ZeroDurationPageRoute({required ZeroDurationPage page})
|
||||||
|
: super(settings: page);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => Duration.zero;
|
||||||
|
|
||||||
|
ZeroDurationPage get _page => settings as ZeroDurationPage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||||
|
return _page.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user