Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions lib/src/router/stacked_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ abstract class StackedPage<T> extends Page<T> {

Widget get child => _child;

// Track current route and listener state to prevent double-completion
// Navigator 2.0 can call createRoute() multiple times on the same Page instance
Route<T>? _currentRoute;
bool _hasAttachedListener = false;

StackedPage({
required this.routeData,
required Widget child,
Expand Down Expand Up @@ -60,10 +65,20 @@ abstract class StackedPage<T> extends Page<T> {

@override
Route<T> createRoute(BuildContext context) {
return onCreateRoute(context)
..popped.then(
_popCompleter.complete,
);
// Clear previous route reference to allow garbage collection
_currentRoute = null;

// Create the new route
_currentRoute = onCreateRoute(context);

// Only attach listener once to prevent "Future already completed" error
// Navigator 2.0 can call createRoute() multiple times during back gestures
if (!_hasAttachedListener) {
_currentRoute!.popped.then(_popCompleter.complete);
_hasAttachedListener = true;
}

return _currentRoute!;
}
}

Expand Down
24 changes: 19 additions & 5 deletions lib/src/router/widgets/route_navigator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,26 @@ class RouteNavigatorState extends State<RouteNavigator> {
restorationScopeId:
widget.navRestorationScopeId ?? widget.router.routeData.name,
pages: widget.router.stack,
onDidRemovePage: (page) {
if (page is StackedPage) {
var routeData = page.routeData;
widget.router.removeRoute(routeData);
widget.didPop?.call(routeData.route, null);
// ignore: deprecated_member_use
onPopPage: (route, result) {
// CRITICAL: Using deprecated onPopPage instead of onDidRemovePage
// because we MUST update router state BEFORE the pop completes.
// This is essential for Android predictive back gestures to work
// without animation glitches (double animations or forward replays).
// onDidRemovePage is called too late (after page removal) and causes
// page list mismatch during the animation, triggering visual glitches.
// See: https://github.com/flutter/flutter/issues/new (migration path unclear)
if (!route.didPop(result)) {
return false;
}

if (route.settings is StackedPage) {
var page = route.settings as StackedPage;
widget.router.removeRoute(page.routeData);
widget.didPop?.call(page.routeData.route, result);
}

return true;
},
)
: widget.placeholder?.call(context) ??
Expand Down