Skip to content

fix(1181): Navigator2 - Navigation animation glitches on back gesture (Android only) - #1182

Open
n0mad-d3v wants to merge 6 commits into
Stacked-Org:masterfrom
n0mad-d3v:fix/navigator-2-future-completion-and-animation
Open

n0mad-d3v wants to merge 6 commits into
Stacked-Org:masterfrom
n0mad-d3v:fix/navigator-2-future-completion-and-animation

Conversation

@n0mad-d3v

Copy link
Copy Markdown

Description

This PR fixes two issues with Navigator 2.0 integration in Stacked's router:

  1. "Future already completed" error during back gestures
  2. Animation glitches on Android (forward flash before backward animation)

Issues

Issue 1: Future Already Completed

Reproduction:

  1. Enable Navigator 2.0 in Stacked (navigator2: true in build.yaml)
  2. Navigate to any route
  3. Perform a back gesture (swipe from edge on iOS/Android)
  4. Error: Bad state: Future already completed

Root Cause:

  • Navigator 2.0's declarative API rebuilds page stacks during gestures
  • StackedPage.canUpdate() returns true for matching pages
  • The same StackedPage instance is reused
  • createRoute() is called multiple times on the same instance
  • Each call attempts to complete the same _popCompleter

Deprecated warnings

It uses the deprecated member onPopPage: (route, result) (lib/src/router/widgets/route_navigator.dart) as I didn't find any way to fix that using onDidRemovePage: (page).
Any feedback welcome on that!

Breaking Changes

None. The changes are internal to StackedPage and RouteNavigator and maintain backward compatibility.

Checklist

  • Fixes are limited to the specific issues
  • Code follows existing style/conventions
  • No breaking changes to public API
  • Comments explain the "why" behind changes
  • All existing tests pass
  • Added tests for the fixes (if applicable)
  • Updated documentation (if needed)

Additional Context

These issues appear when using Navigator 2.0 with the declarative API. Navigator 1.0 is unaffected as it uses imperative navigation.

The fixes follow Flutter's own Navigator 2.0 patterns and align with the official documentation for implementing custom page-based navigation.

Related Issues

#1181

@n0mad-d3v n0mad-d3v changed the title fix(1181): Navigator2 - Navigation animation glitches on back gesture (Android only) fix(#1181): Navigator2 - Navigation animation glitches on back gesture (Android only) Dec 24, 2025
@n0mad-d3v n0mad-d3v changed the title fix(#1181): Navigator2 - Navigation animation glitches on back gesture (Android only) fix(1181): Navigator2 - Navigation animation glitches on back gesture (Android only) Dec 24, 2025
@busmleon

Copy link
Copy Markdown

I assume this is the same reason why I get "future already completed" when popping a route quickly after another. Are there any plans when this will be fixed/merged?

Comment thread lib/src/router/stacked_page.dart Outdated
widget.router.removeRoute(routeData);
widget.didPop?.call(routeData.route, null);
// ignore: deprecated_member_use
onPopPage: (route, result) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression: imperative page removals no longer sync the router stack. With onPopPage set instead of onDidRemovePage, Flutter only calls back for Navigator.pop / maybePop / popUntil. Every other imperative removal of a page-backed route (Navigator.removeRoute, removeRouteBelow, pushReplacement, pushAndRemoveUntil, replace/replaceRouteBelow) goes through _RouteEntry.complete(..., imperativeRemoval: true), which only calls widget.onDidRemovePage (see navigator.dart handlePop/handlePush). The old code handled those through onDidRemovePage.

Scenario: app code calls Navigator.of(context).pushAndRemoveUntil(...) or removeRoute(route) on a StackedPage route. The route gets disposed, but its StackedPage stays in router._pages. removeRoute/notifyAll never run, so the URL, guards and didPop go stale. On the next notifyAll rebuild the Navigator finds a page with no entry and calls createRoute on the same StackedPage again, so the removed screen comes back. That is also the "createRoute called twice on the same page" case the completer guard in stacked_page.dart now hides silently.

_popCompleter.complete,
);
..popped.then((value) {
if (!_popCompleter.isCompleted) _popCompleter.complete(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard hides the symptom rather than fixing it. When createRoute runs a second time on the same StackedPage (for example the page resurrected after an imperative removal, as described on route_navigator.dart), the page completer has already completed with the first route's result, usually null for a removal. Whatever the live second route pops with is then dropped silently, so a caller awaiting router.push<T>() got null early and never sees the real result. Before this change the case showed up as a StateError. Now it loses data without any signal. Consider at least an assert or debug log when the completer is already completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants