Difference between onPopPage and popRoute - flutter

I´m using Navigation 2.0 for flutter navigation development, however, there´s only one topic for me to understand it properly. I´m using a custom RootBackButtonDispatcher:
class AppBackButtonDispatcher extends RootBackButtonDispatcher {
final AppRouteDelegate routerDelegate;
AppBackButtonDispatcher({
#required this.routerDelegate,
}) : super();
#override
Future<bool> didPopRoute() {
return routerDelegate.popRoute();
}
}
When pressing the back button, the function that gets called is (a function that is inside my AppRouteDelegate):
#override
Future<bool> popRoute() {
if (_canPop()) {
_removePage(viewModel.pages.last);
viewModel.rebuild;
return SynchronousFuture(true);
}
return SynchronousFuture(false); // This will exit the application
}
The previous code handles properly the back button functionality, but the Navigator´s onPopPage never gets called:
#override
Widget build(BuildContext context) => Navigator(
key: navigatorKey,
onPopPage: _onPopPage, => WHY DON´T YOU GET CALLED!
pages: _buildPages(),
);
Am I handling this code in a wrong way, or by adding the custon back button dispatcher, the forementioned function never gets called?

Related

Flutter - Cubit & Navigation 2.0: emitting new page from page

I am trying to create a website with Flutter using Navigation 2.0 and BLoC pattern. To do so, I read the following guides:
https://medium.com/#JalalOkbi/flutter-navigator-2-0-with-bloc-the-ultimate-guide-6672b115adf
https://lucasdelsol01.medium.com/flutter-navigator-2-0-for-mobile-dev-bloc-state-management-integration-3a180b4d25b3
and this repo: https://lucasdelsol01.medium.com/flutter-navigator-2-0-for-mobile-dev-bloc-state-management-integration-3a180b4d25b3 (which implements the first guide).
However I am facing an issue where I am trying to push a new page from one of my website displayed page: the new page is never displayed!
To understand:
Each pages are pushed via a MainNavigationCubit. This cubit's state (meaning pages) is maintained within the NavigationStack.
My MainNavigationCubit is responsible for building the Navigator in my custom RouterDelegate (see code below). So upon a state change it rebuilds the Navigator with the proper list of pages.
The problem context:
I have a "Book" page which displays the details about a specific book.
In order to get the details, it expects a book id.
If the book id is invalid or not found, then the "404 not found page" is pushed via MainNavigationCubit.
This can happen, eg, if the user is manually inputting a correct URL to the book page but with an invalid ID.
However the "404 not found page" is never displayed although the MainNavigationCubit properly emits a new NavigationStack with relevant pages.
This is the code from my custom RouterDelegate:
#override
GlobalKey<NavigatorState> get navigatorKey => GlobalKey<NavigatorState>(debugLabel: 'main_navigation_key');
#override
Future<void> setNewRoutePath(PageConfig configuration) {
if (configuration.route != homeRoute) {
mainNavigationCubit.push(configuration.route, configuration.args);
} else {
mainNavigationCubit.clearToHome();
}
return SynchronousFuture(null);
}
#override
Widget build(BuildContext context) {
return BlocBuilder<MainNavigationCubit, NavigationStack>(
builder: (context, stack) {
return Navigator(
pages: stack.pages,
key: navigatorKey,
onPopPage: (route, result) => _onPopPage.call(route, result),
);
},
);
#override
PageConfig get currentConfiguration => mainNavigationCubit.state.last;
bool _onPopPage(Route<dynamic> route, dynamic result) {
final didPop = route.didPop(result);
if (!didPop) {
return false;
}
if (mainNavigationCubit.canPop()) {
mainNavigationCubit.pop();
return true;
} else {
return false;
}
}
And this is the code from my "Book" StatelessWidget page:
#override
Widget build(BuildContext context) {
if (bookId == -1) {
context.read<MainNavigationCubit>().showNotFound(); // let's assume this will be properly handled when I'll be creating this page's BLoC.
}
return // full book details UI;
}
And just in case the code of MainNavigationCubit.showNotFound():
void showNotFound() {
clearAndPush(notFound);
}
void clearAndPush(String path, [Map<String, dynamic>? args]) {
final PageConfig pageConfig = PageConfig(location: path, args: args);
emit(state.clearAndPush(pageConfig));
}
OK, so after a lot of investigation I have found the reason for my issue.
As the documentation says: a Cubit won't notify listeners upon emitting a new state that is equal to the current state.
In my case, my MainNavigationCubit's state is a NavigationStack which I took from this guide: https://medium.com/#JalalOkbi/flutter-navigator-2-0-with-bloc-the-ultimate-guide-6672b115adf
Looking at the code, the NavigationStack exposes methods that mutates an internal list of pages.
The problem is this list belongs to the current state, therefore modifying it means to also modify the current state.
As both current and new state rely on the same exact list, the Cubit won't emit the new state.

Navigator 2.0 - WillPopScope vs BackButtonListener

I have an app with a BottomNavigationBar and an IndexedStack which shows the tab content. Each tab has its own Router with its own RouterDelegate to mimic iOS-style tab behavior (where each tab has its own navigation controller).
Before, this app was only published on iOS. I'm now working on the Android version and need to correctly support the Android hardware back button. I did this by implementing a ChildBackButtonDispatchers per tab, which are a child of the parent RootBackButtonDispatcher. This works.
The issue I'm having now is that I use WillPopScope widgets to save a user's input when they leave a screen. This works correctly if the user taps the back button in the AppBar, but the callback isn't triggered when the user taps the hardware back button. I implemented BackButtonListeners on these screens as well, but this means I have to wrap the screens in both WillPopScopes and BackButtonListeners, both calling the same callback.
It this how it's supposed to be, or am I doing something wrong?
Relevant widget hierarchy:
MaterialApp
Navigator
tab interface with IndexedStack
the selected tab Widget the tab's Router
Navigator
multiple pages, with on the last page in the stack...
BackButtonListener
WillPopScope
Scaffold
My (simplified) router delegate looks like this:
class AppRouterDelegate extends RouterDelegate<AppRoute>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoute> {
AppRouterDelegate({
List<MaterialPage> initialPages = const [],
}) : _pages = initialPages;
final navigatorKey = GlobalKey<NavigatorState>();
final List<MaterialPage> _pages;
List<MaterialPage> get pages => List.unmodifiable(_pages);
void push(AppRoute route) {
final shouldAddPage = _pages.isEmpty || (_pages.last.arguments as AppRoute != route);
if (!shouldAddPage) {
return;
}
_pages.add(route.page);
notifyListeners();
}
#override
Future<void> setNewRoutePath(AppRoute route) async {
_pages.clear();
_pages.add(route.page);
notifyListeners();
return SynchronousFuture(null);
}
#override
Future<bool> popRoute() {
if (canPop) {
pop();
return SynchronousFuture(true);
}
return SynchronousFuture(false);
}
bool get canPop => _pages.length > 1;
void pop() {
if (canPop) {
_pages.remove(_pages.last);
notifyListeners();
}
}
void popTillRoot() {
while (canPop) {
_pages.remove(_pages.last);
}
notifyListeners();
}
bool _onPopPage(Route<dynamic> route, result) {
final didPop = route.didPop(result);
if (!didPop) {
return false;
}
if (canPop) {
pop();
return true;
} else {
return false;
}
}
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _onPopPage,
pages: pages,
);
}
}
I found this Flutter issue which makes me think I shouldn't have the WillPopScope at all, but without it the taps in the AppBar are not caught...
I know this question is old, but here's an answer for others who arrive here.
From the AppBar leading documentation (emphasis mine):
If this is null and automaticallyImplyLeading is set to true, the AppBar will imply an appropriate widget. For example, if the AppBar is in a Scaffold that also has a Drawer, the Scaffold will fill this widget with an IconButton that opens the drawer (using Icons.menu). If there's no Drawer and the parent Navigator can go back, the AppBar will use a BackButton that calls Navigator.maybePop.
So in order to make the Android back button work the same way as the App Bar's back button, you need to use the Navigator.maybePop method, which will respect WillPopScope.
Conveniently, Flutter provides PopNavigatorRouterDelegateMixin to make this easy; it provides an implementation of popRoute that uses maybePop and therefore will work identically to the App Bar's automatically-generated back/dismiss button. The nice thing about Flutter being open source is that you can jump into the Flutter code to verify what the mixin is doing:
mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
/// The key used for retrieving the current navigator.
///
/// When using this mixin, be sure to use this key to create the navigator.
GlobalKey<NavigatorState>? get navigatorKey;
#override
Future<bool> popRoute() {
final NavigatorState? navigator = navigatorKey?.currentState;
if (navigator == null)
return SynchronousFuture<bool>(false);
return navigator.maybePop();
}
}
So I think the only mistake in your code is that, even though you've mixed-in PopNavigatorRouterDelegateMixin on your router delegate, you are also providing your own override of popRoute. When the user taps the Android back button, your popRoute implementation is called, and it just pops the last page. If you delete your popRoute override and let the mixin do its thing, then the Android back button will function identically to the App Bar back/dismiss button.

How can I execute a function when a user hits the back button or swipes to close a screen in flutter?

I want to always execute a function when a screen is closed, either when a user presses the back button or does a swipe to close the page.
What I have tried is to use WillPopScope
return WillPopScope(
onWillPop: _onWillPop,
child: CupertinoScaffold( ... the rest of my tree
Future<bool> _onWillPop() async {
myFunction();
return true;
}
While this does successfully execute my function whenever the page is closed via the back button, it seems to have disabled being able to swipe to go back on iOS. Swipe to go back on android still seems to function.
Is there another way of doing this so I can retain the swipe to go back functionality on iOS?
edit:
Just incase it matters, myFunction() involves a provider and context and throws an error when I try to call it from dispose.
In actuality myFunction() is:
context.read(myVisibilityProvider).changeVisibility(false);
I successfully managed to run a function calling it from dispose retaining the iOS swipe to back gesture.
class ScrondScreen extends StatefulWidget {
const ScrondScreen({Key? key}) : super(key: key);
#override
_ScrondScreenState createState() => _ScrondScreenState();
}
class _ScrondScreenState extends State<ScrondScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
);
}
void myFunction() {
print('I run');
}
#override
void dispose() {
myFunction();
super.dispose();
}
}
console:
flutter: I run

Redirect user from named route

So I have a similar issue as the person who asked this older question, except with different requirements that none of the answers there help with.
When a user opens the app, I want them to be greeted with the login page if they haven't logged in or the home page (a bottom nav bar view) if they did. I can define this in the MaterialApp as follows:
MaterialApp(
initialRoute: authProvider.isAuthenticated
? '/home'
: '/login',
routes: {
'/home': (_) =>
ChangeNotifierProvider<BottomNavigationBarProvider>(
child: AppBottomNavigationBar(),
create: (_) => BottomNavigationBarProvider()),
'/login': (_) => LoginView()
},
)
So far so good. Except I want this to work on the web, and now even though the default screen when a user first opens myapp.com is myapp.com/#/login, any user can bypass the login screen by simply accessing myapp.com/#/home.
Now I tried to redirect the user to the login page in the initState() of the bottom navigation bar (and setting the initialRoute to be /home), but on mobile this has undesirable behaviour.
If I try this:
void initState() {
super.initState();
if (!Provider.of<AuthProvider>(context, listen: false).isAuthenticated) {
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushNamed('/login');
});
}
}
then simply pressing back will return the user to the home page, again bypassing the login. If I try to use popAndPushNamed instead of just pushing, pressing back will open a blank screen (instead of closing the app).
Is there any way to do this correctly so it works on both web and mobile?
If you use the RouteAware mixin on your widget classes, they will be notified when they are navigated to (or away from). You can use this to check if the user is supposed to be there and to navigate them away if they are not:
To use it, first you need some global instance of RouteObserver that all your widget classes can access:
final routeObserver = RouteObserver<PageRoute>();
Then you need to register it with your MaterialApp:
MaterialApp(
routeObservers: [routeObserver],
)
Then register your widget to the route observer:
class HomeViewState extends State<HomeView> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context));
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
void didPop() {
// This gets called when this widget gets popped
}
void didPopNext() {
// This gets called when another route gets popped making this widget visible
}
void didPush() {
// This gets called when this widget gets pushed
}
void didPushNext() {
// This gets called with another widget gets pushed making this widget hidden
}
...
}
In your case, you can use the didPush route to navigate the user to the login page if they get to that page in error:
void didPush() {
if (checkLoginStateSomehow() == notLoggedIn) {
Navigator.of(context).pushReplacementNamed('login');
}
}

How to go back and refresh the previous page in Flutter?

I have a home page which when clicked takes me to another page through navigates, do some operations in then press the back button which takes me back to the home page. but the problem is the home page doesn't get refreshed.
Is there a way to reload the page when i press the back button and refreshes the home page?
You can trigger the API call when you navigate back to the first page like this pseudo-code
class PageOne extends StatefulWidget {
#override
_PageOneState createState() => new _PageOneState();
}
class _PageOneState extends State<PageOne> {
_getRequests()async{
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new RaisedButton(onPressed: ()=>
Navigator.of(context).push(new MaterialPageRoute(builder: (_)=>new PageTwo()),)
.then((val)=>val?_getRequests():null),
),
));
}
}
class PageTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
//somewhere
Navigator.pop(context,true);
}
}
Or you can just use a stream if the API is frequently updated, the new data will be automatically updated inside your ListView
For example with firebase we can do this
stream: FirebaseDatabase.instance.reference().child(
"profiles").onValue
And anytime you change something in the database (from edit profile page for example), it will reflect on your profile page. In this case, this is only possible because I am using onValue which will keep listening for any changes and do the update on your behalf.
(In your 1st page): Use this code to navigate to the 2nd page.
Navigator.pushNamed(context, '/page2').then((_) {
// This block runs when you have returned back to the 1st Page from 2nd.
setState(() {
// Call setState to refresh the page.
});
});
(In your 2nd page): Use this code to return back to the 1st page.
Navigator.pop(context);
use result when you navigate back from nextScreen as follow :
Navigator.of(context).pop('result');
or if you are using Getx
Get.back(result: 'hello');
and to reload previous page use this function :
void _navigateAndRefresh(BuildContext context) async {
final result = await Get.to(()=>NextScreen());//or use default navigation
if(result != null){
model.getEMR(''); // call your own function here to refresh screen
}
}
call this function instead of direct navigation to nextScreen
The solution which I found is simply navigating to the previous page:
In getx:
return WillPopScope(
onWillPop: () {
Get.off(() => const PreviousPage());
return Future.value(true);
},
child: YourChildWidget(),
or if you want to use simple navigation then:
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>PreviousPage() ,));
Simply i use this:
onPressed: () {
Navigator.pop(context,
MaterialPageRoute(builder: (context) => SecondPage()));
},
this to close current page:
Navigator.pop
to navigate previous page:
MaterialPageRoute(builder: (context) => SecondPage())
In FirtsPage, me adding this for refresh on startUpPage:
#override
void initState() {
//refresh the page here
super.initState();
}
For a more fine-grained, page-agnostic solution I came up with this Android Single LiveEvent mimicked behaviour.
I create such field inside Provider class, like:
SingleLiveEvent<int> currentYearConsumable = SingleLiveEvent<int>();
It has a public setter to set value. Public consume lets you read value only once if present (request UI refresh). Call consume where you need (like in build method).
You don't need Provider for it, you can use another solution to pass it.
Implementation:
/// Useful for page to page communication
/// Mimics Android SingleLiveEvent behaviour
/// https://stackoverflow.com/questions/51781176/is-singleliveevent-actually-part-of-the-android-architecture-components-library
class SingleLiveEvent<T> {
late T _value;
bool _consumed = true;
set(T val) {
_value = val;
_consumed = false;
}
T? consume() {
if (_consumed) {
return null;
} else {
_consumed = true;
return _value;
}
}
}
await the navigation and then call the api function.
await Navigator.of(context).pop();
await api call
You can do this with a simple callBack that is invoked when you pop the route. In the below code sample, it is called when you pop the route.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
_someFunction()async{
Navigator.of(context).push(MaterialPageRoute(builder: (_)=> PageTwo(
onClose():(){
// Call setState here to rebuild this widget
// or some function to refresh data on this page.
}
)));
}
#override
Widget build(BuildContext context) {
return SomeWidget();
}
...
} // end of widget
class PageTwo extends StatelessWidget {
final VoidCallback? onClose;
PageTwo({Key? key, this.onClose}) : super(key: key);
#override
Widget build(BuildContext context) {
return SomeWidget(
onEvent():{
Navigate.of(context).pop();
onClose(); // call this wherever you are popping the route
);
}
}