According to this breaking change, the onDidRemovePage property has replaced onPopPage.
Here’s what they do:
onPopPage: Previously, this callback was used to call [Route.didPop] and return whether the pop was successful. It was invoked only for imperative pop actions.
onDidRemovePage: This callback is triggered when the [Route] associated with a [Page] is removed from the Navigator. It handles both imperative (e.g., Navigator.pop) and declarative removals (e.g., state changes).
The breaking change has caused issues for my implementation because I need to differentiate imperative pops from declarative ones to update the internal page state that I manage.
How can I achieve this distinction with onPopPage now deprecated?
class PickerAppRouterDelegate extends RouterDelegate<void> with ChangeNotifier {
final ValueListenable<List<AppRoutePage>> pages;
final Future<bool> Function() onPagePoppedWithOperatingSystemIntent;
final void Function(String? poppingPageName) onPagePoppedImperatively;
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
PickerAppRouterDelegate({
required this.pages,
required this.onPagePoppedWithOperatingSystemIntent,
required this.onPagePoppedImperatively,
}) {
pages.addListener(notifyListeners);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: pages.value,
// Using the deprecated onPopPage callback as a temporary solution because:
// - The new onDidRemovePage is triggered for both imperative and declarative pops.
// - onPopPage, however, is invoked only for imperative actions, which aligns better with our requirements.
// If we don't track pages popped imperatively, it could cause inconsistencies in the app's internal page stack.
//
// See discussion: https://forum.example.com/t/how-to-detect-only-imperative-pops
// ignore: deprecated_member_use
onPopPage: (route, result) {
onPagePoppedImperatively(route.settings.name);
return route.didPop(result);
},
);
}
@override
Future<void> setNewRoutePath(void configuration) async {
/* No implementation needed */
}
/// Handles pops caused by system interactions (e.g., swipe gestures or back button).
/// If this method returns false, the entire app will exit.
@override
Future<bool> popRoute() {
final currentContext = navigatorKey.currentContext;
if (currentContext != null) {
if (_closeBottomAndActionSheets(currentContext)) {
return Future.value(true);
}
return onPagePoppedWithOperatingSystemIntent();
}
return Future.value(false);
}
}
The data layer class keeps track of the pages:
void onPagePoppedImperatively(String? poppingPageName) {
if (poppingPageName == null) {
return;
}
final appRoute = AppRouteName.findFromPageName(poppingPageName);
switch (appRoute) {
case AppRouteName.home:
case AppRouteName.auth:
case AppRouteName.tasks:
break;
case AppRouteName.detailSetting:
_pages.value = [const TasksPage(), const SettingsPage()];
break;
case AppRouteName.guides:
case AppRouteName.detailContent:
_pages.value = [const TasksPage(), const GuidesPage()];
break;
case AppRouteName.storeDetail:
case AppRouteName.hoursDetail:
_pages.value = [const TasksPage(), const StorePage()];
break;
default:
_pages.value = [const TasksPage()];
break;
}
}
/// Handles pops caused by system interactions (e.g., swipe gestures or back button).
/// Returning false will exit the app.
@Override
Future<bool> onPagePoppedWithOperatingSystemIntent() {
final lastPage = _pages.value.lastOrNull;
if (lastPage == null) {
return Future.value(false);
}
switch (lastPage) {
case HomePage():
case TasksPage():
case AuthPage():
return Future.value(false);
case DetailSettingPage():
_pages.value = [const TasksPage(), const SettingsPage()];
return Future.value(true);
case GuidesPage():
case DetailContentPage():
_pages.value = [const TasksPage(), const GuidesPage()];
return Future.value(true);
case StoreDetailPage():
case HoursDetailPage():
_pages.value = [const TasksPage(), const StorePage()];
return Future.value(true);
default:
_pages.value = [const TasksPage()];
return Future.value(true);
}
}
The business logic in domain layer decides the navigation stack, then the representation of the navigation stack in the domain layer is mapped to routes by the Navigator in the widget layer. This is why I need to sync the current state of the navigation stack with the actual in the widget layer.
I think it is related to the predictive back gesture on Android and the swipe back on iOS.
I seem to have the exact same issue. Have you found a good workaround? I think about using a GlobalKey on each page to identify the one that was popped from the stack, but I’m not sure.
This is how I manage it now, but that does not answer your question how you can tell the difference how the page got removed.
@override
void onDidRemovePage(Page<Object?> page) {
late final Widget screen;
if (page is MaterialPage) {
screen = page.child;
} else if (page is CupertinoPage) {
screen = page.child;
} else {
throw ArgumentError('Page must be of type MaterialPage or CupertinoPage');
}
if (availableScreens.findScreen(screenStack.last) == screen) {
screenStack.removeLast();
}
}
Flutter and the related logo are trademarks of Google LLC. We are not endorsed by or affiliated with Google LLC.
Using contents of this forum for the purposes of training proprietary AI models is forbidden. Only if your AI model is free & open source, go ahead and scrape.