Redirect user from named route - flutter

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');
}
}

Related

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.

Detect if current Widget/State is visible in page route?

I want to add a Listener when the widget/state is visible and remove myself from the Listener when I leave the route.
But if the user clicks on the back button and returns to the current widget/state, I want to do the same thing again.
Currently initState, didChangeDependencies, didUpdateWidget and build are NOT called when the user clicks back from the next page, therefore I cannot detect when the user is returning and the widget was loaded from cache.
After much poking around the API, I've discovered that ModalRoute and RouteObserver is what I want.
https://api.flutter.dev/flutter/widgets/ModalRoute-class.html
https://api.flutter.dev/flutter/widgets/RouteObserver-class.html
If I just want to check if the current route is active I can call isCurrent on ModalRoute.of(context):
void onNetworkData(String data) {
if (ModalRoute.of(context).isCurrent) {
setState(() => list = data);
}
}
If I want to listen to route load/unload, I just create it and serve it up the hood with Provider like this:
class HomePage extends StatelessWidget {
final spy = RouteObserver<ModalRoute<void>>();
build(BuildContext context) {
return Provider<RouteObserver<ModalRoute<void>>>.value(
value: spy,
child: MaterialApp(
navigatorObservers: [spy],
),
);
}
}
Then somewhere in another widget:
class _AboutPageState extends State<AboutPage> with RouteAware {
RouteObserver<ModalRoute<void>>? spy;
void didChangeDependencies() {
super.didChangeDependencies();
spy = context.read<RouteObserver<ModalRoute<void>>>();
spy.subscribe(this, ModelRoute.of(context)!);
}
void dispose() {
spy.unsubscribe(this);
super.dispose();
}
void didPush() => attachListeners();
void didPopNext() => attachListeners();
void didPop() => removeListeners();
void didPushNext() => removeListeners();
attachListeners() {
}
removeListeners() {
}
}

Load a Route before pushing it via Navigator in flutter?

I'm currently trying to push a named route in flutter.
Works good so far, but the Asset Image for the background is loaded after the route was pushed via Navigator, which does not look good.
Currently I push the route like this:
#override
void initState() {
super.initState();
Timer(Duration(seconds: 5), () =>
Navigator.pushNamed(context, routeToPage2)
);
}
Is there any way to load a Page / Route without pushing it in the first place, so everything is build correctly when the route is pushed after the set time?
I guess the Asset Image gets loaded on routeToPage2? In this case the push happens everytime before the image gets load.
you can use a routegenerator for your page route and popUntil. This behavior will remove existing pages off the stack and push a single page called home page on. The MaterialPageRoute builds your route that is pushed to the navigator. Load your asset in the scaffold of HomePage. It should not require a delay to render
Navigator.of(context)
.popUntil(ModalRoute.withName(RouteGenerator.homePage));
class RouteGenerator {
static const String homePage = "/home";
static const String customPage = "/custom";
RouteGenerator._();
static Route<dynamic> handleRoute(RouteSettings routeSettings) {
Widget childWidget;
switch (routeSettings.name) {
case homePage:
{
childWidget = HomePageWidget(title: 'Home Page');
}
break;
case customPage:
{
final args = routeSettings.arguments as CustomView;
childWidget = CustomPageWidget(args);
}
break;
default:
throw FormatException("Route Not Found");
}
return MaterialPageRoute(builder: (context) => childWidget);
}
}
Sorry for being a bit too vague, maybe I should have explained my problem more detailed.
So basically I have Page1, which pushes after a Timer finished to Page2.
On Page2, I have an Image-Asset, which loads shortly after Page2 is displayed, which did not look nice. This asset is saved on the device.
I wanted to load Page2 somehow in the background, while Page1 is still being displayed to the user, so the Background-image of Page2 does not pop up after the Page is shown.
But I have found myself a suitable solution on my problem.
I use the following code on Page1:
late Image backgroundImage;
#override
void initState() {
super.initState();
backgroundImage = Image.asset("path to image");
Timer(Duration(seconds: 5),
() => Navigator.pushReplacementNamed(context, page2));
}
#override
void didChangeDependencies(){
super.didChangeDependencies();
precacheImage(backgroundImage.image, context);
}
This results in preloading the image while Page1 is shown, so the Background-Image does not pop up after Page2 is displayed to the user.

Difference between onPopPage and popRoute

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?

didChangeAppLifecycleState doesn't work as expected

I hope I understand how didChangeAppLifecycleState worked correctly.
I have page A and page B . When I click the back device button from page B ( Navigator.of(context).pop(); ), I expect didChangeAppLifecycleState in pageA will get called, but it doesn't.
PageA
class _ABCState extends State<ABCrList> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
....
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() {
print(...);
});
}else{
print(state.toString());
}
}
....
This is the initState in pageA. The function used to call backend service.
#override
void initState() {
super.initState();
_bloc.getList(context); // return list and populate to ListView
});
}
The way you're thinking it is Android's way where onResume works, but in Flutter, things don't happen this way.
Generally, this gets called when the system puts the app in the background or returns the app to the foreground.
There are mainly 4 states for it:
resumed: The application is visible and responding to user input.
inactive: The application is in an inactive state and is not receiving user input.
paused: The application is not currently visible to the user, not responding user input, and running in the background.
detached: The application is still hosted on a flutter engine but is detached from any host views.
Edit:
When you're navigating to PageB from PageA, use something like:
Navigator.pushNamed(context, "/pageB").then((flag) {
if (flag) {
// you're back from PageB, perform your function here
setState(() {}); // you may need to call this if you want to update UI
}
});
And from PageB, you'll can use
Navigator.pop(context, true);