Flutter : How to Pop with two nested Navigators? - flutter

I have a MaterialApp(navigatorKey: rootNavigatorKey ) , which has two routes
'/datoPhotoViewer' without a drawer
'/' which has a Drawer, this drawer has listItems that pushes the widgets on his child : Navigator(navigatorKey: navigatorKey ),
The problem is when i press the back button, whole app closes, My idea was then, catch Back-Button-pressed , and calling the navigatorKey.currentState.pop() from the adequate navigatorKey
So to do it, i need WillPopScope .. when I set a WillPopScope on each route, i find that when i press Back button, the app was still closing
So i've decided to delete all the WillPopScope i had, and put only one WillPopScope(onWillPop: onPop) on main.dart where by trying, i find that it catches every 'back button pressed', and changing that static function onPop when i open a route with :
setOnPopFunction(Function func){
onPop = func;
}
But when i call this function, onPop function isn't changed ,
Another thing i saw happening : I'm getting logged those weird results when accessing the navigatorKeys
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
Function onPop ;
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
onPop = (){
log("___Navigator1 context "+ ModalRoute.of(rootNavigatorKey.currentContext).toString());
//this prints '___Navigator1 context null' , even though i can push /datoPhotoViewer to Navigator 1
log("___Navigator2 routeName "+ ModalRoute.of(navigatorKey.currentContext).settings.name.toString());
//always printing '___Navigator2 routeName /' ,even when this navigatorKey has been pushed to another route
return Future.value(false);
};
return MaterialApp(
navigatorKey: rootNavigatorKey, //Navigator 1 key
routes: {
'/': (context) { //Navigator 1 route
return WillPopScope(
onWillPop: onPop //this is the only onWillPop function i can get called when pressing back button , Which i'll change with setOnPopFunction()
child: MyMainStructure( //MyMainStructure is a Scaffold with the drawer
child: Navigator(
key: navigatorKey, //Navigator 2 key
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/': //Navigator 2 route
return MaterialPageRoute(
builder: (_) => TodosLosItemsPage());
case '/categoria': //Navigator 2 route
return MaterialPageRoute(
builder: (_) =>
CategoriaConceptosPage(settings.arguments),
);
}
},
initialRoute: '/', //Navigator 2 initial
)),
);
},
'/datoPhotoViewer': (context) => //Navigator 1 route
datoPhotoViewer(ModalRoute.of(context).settings.arguments),
},
initialRoute: '/', //Navigator 1 initial
),
);
}
}
setOnPopFunction(Function func){
onPop = func;
}
thanks in advance

To solve this i ended up using an external package
https://pub.dev/packages/back_button_interceptor
I've disabled the back button for all the app with a
WillPopScope(
onWillPop: () async => false,
child: ....
And making a BackButtonManager class to override the Back Button
List<Function> _listeners = List<Function>();
class BackButtonManager {
static void addCustomFunction(Function func){
_listeners.add(func);
BackButtonInterceptor.add(func);
}
static void resetListeners(){
_listeners.forEach((element) {
BackButtonInterceptor.remove(element);
});
_listeners.clear();
}
This is the structure of what i do when a new route is opened, on initState i call:
BackButtonManager.resetListeners();
BackButtonManager.addCustomFunction ((bool asd){
//handle it
//for example: navigatorKey.CurrentState.pop();
// then check which widget is going to be onscreen now ,
// and when this BackButton is pressed , add to BackButtonManager it's
// BackButtonFunction
BackButtonManager.resetListeners();
BackButtonManager.addCustomFunction ((bool asd){
//handle the incoming OnScreenWidget
return false;
});
return false;
});
I'd recommend to make a list that represents your stacks of routes, there might be another way , but this works

Related

Flutter persistent sidebar

In my application I want to have a sidebar that allows me to have access to specific functions everywhere in my application.
What I want :
That the sidebar remains visible when I push my pages
That I can pushNamed route or open a modal with one of the sidebar functions
That I can not display the sidebar on certain pages
What I do :
In red, the persistent sidebar and in yellow my app content.
If I click on my profil button in the HomeView, the ProfilView is displayed and my sidebar remains visible so it's ok
My AppView :
class AppView extends StatelessWidget {
const AppView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: AppConfig.kAppName,
debugShowCheckedModeBanner: false,
theme: AppTheme().data,
builder: (context, child) => SidebarTemplate(child: child), // => I create a template
onGenerateRoute: RouterClass.generate,
initialRoute: RouterName.kHome,
);
}
My SidebarTemplate : (Display the sidebar and load the page with my router)
class SidebarTemplate extends StatelessWidget {
final Widget? child;
const SidebarTemplate({Key? key, this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body : Row(
children: [
SidebarAtom(), // => My sidebar Widget
Expanded(
child: ClipRect(
child: child! // => My view
),
)
],
)
),
);
}
}
My RouterClass :
abstract class RouterClass{
static Route<dynamic> generate(RouteSettings settings){
final args = settings.arguments;
switch(settings.name){
case RouterName.kHome:
return MaterialPageRoute(
builder: (context) => HomeView()
);
case RouterName.kProfil:
return MaterialPageRoute(
builder: (context) => ProfilView(title: "Profil",)
);
default:
return MaterialPageRoute(
builder: (context) => Error404View(title: "Erreur")
);
}
}
}
How to do :
To pushNamed or open a modal with a button from my sidebar because I have an error
The following assertion was thrown while handling a gesture:
I/flutter (28519): Navigator operation requested with a context that does not include a Navigator.
I/flutter (28519): The context used to push or pop routes from the Navigator must be that of a widget that is a
I/flutter (28519): descendant of a Navigator widget.
To hide the sidebar when I want like SplashScreen for example
Any guidance on the best way to accomplish this would be appreciated.
You can use a NavigatorObserver to listen to the changes in the route.
class MyNavObserver with NavigatorObserver {
final StreamController<int> streamController;
MyNavObserver({required this.streamController});
#override
void didPop(Route route, Route? previousRoute) {
if (previousRoute != null) {
if (previousRoute.settings.name == null) {
streamController.add(3);
} else {
streamController
.add(int.parse(previousRoute.settings.name!.split('/').last));
}
}
}
#override
void didPush(Route route, Route? previousRoute) {
if (route.settings.name == null) {
streamController.add(3);
} else {
streamController.add(int.parse(route.settings.name!.split('/').last));
}
}
}
and using StreamController you can make changes to your SidebarTemplate by putting it inside StreamBuilder. This will take care of all the requirements you have mentioned in the question.
Check out the live example here.
As you can see from the Profil screenshot, the sidebar is not part of the widget subtree of the Navigator (the back button is only on the profil widget). This means that you cannot find the Navigator from the context of the sidebar. That is happening because you are using builder in your MaterialApp which inserts widgets above the navigator.
That is also the reason why you cannot hide the sidebar when you want to show a splash screen.
Do you really need to use the builder on MaterialApp? Then you can save the Navigator globally and access it from the sidebar. This is the first article when I search on DuckDuckGo, that you can follow.
To show a SplashScreen you would need to add a state to AppView and change the builder function. Not very nice if you ask me.
I suggest you to re-think your architecture and get rid of the builder in the MaterialApp.

App widget is not generating the route | firebase signup

Upon successful signup, I am trying to send users to the homepage (home) explaining how to use the app. I am doing so through this code block on my signup.dart
onPressed: () async {
try {
User user =
(await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: _emailController.text,
password: _passwordController.text,
))
.user;
if (user != null) {
user.updateProfile(displayName: _nameController.text);
Navigator.of(context).pushNamed(AppRoutes.home);
}
}
Which is pointing to the home route
class AppRoutes {
AppRoutes._();
static const String authLogin = '/auth-login';
static const String authSignUp = '/auth-signup';
static const String home = '/home';
static Map<String, WidgetBuilder> define() {
return {
authLogin: (context) => Login(),
authSignUp: (context) => SignUp(),
home: (context) => Home(),
};
}
}
However, when I sign up, the data is rendering in firebase, but the user is not being sent to the home page, and throws this error in my console
Make sure your root app widget has provided a way to generate
this route.
Generators for routes are searched for in the following order:
1. For the "/" route, the "home" property, if non-null, is used.
2. Otherwise, the "routes" table is used, if it has an entry for the route.
3. Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
4. Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.
Any thoughts on how to rectify?
Have you added onGenerateRoute in your MaterialApp? Like this:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: Router.generateRoute,
initialRoute: yourRoute,
child: YouApp(),
);
}
}
class Router {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case AppRoutes.home:
return MaterialPageRoute(builder: (_) => Home());
case AppRoutes.authLogin:
return MaterialPageRoute(builder: (_) => Login());
case AppRoutes.authSignUp:
return MaterialPageRoute(builder: (_) => SignUp());
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}')),
));
}
}
}
}
}

Flutter Provider rebuilt widget before parent's Consumer

I have got a problem with the provider package.
I want to be able to clean an attribute (_user = null) of a provider ChangeNotifier class (it is a logout feature).
The problem is when I am doing that from a Widget that use info from this Provider.
My main app is like :
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => AuthProvider(),
builder: (context, _) => App(),
),
);
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (_, auth, __) {
Widget displayedWidget;
switch (auth.loginState) {
case ApplicationLoginState.initializing:
displayedWidget = LoadingAppScreen();
break;
case ApplicationLoginState.loggedIn:
displayedWidget = HomeScreen();
break;
case ApplicationLoginState.loggedOut:
default:
displayedWidget = AuthenticationScreen(
signInWithEmailAndPassword: auth.signInWithEmailAndPassword,
registerAccount: auth.registerAccount,
);
}
return MaterialApp(
title: 'My App',
home: displayedWidget,
routes: {
ProfileScreen.routeName: (_) => ProfileScreen(),
},
);
});
}
}
My Provider class (simplified) :
class AuthProvider extends ChangeNotifier {
ApplicationLoginState _loginState;
ApplicationLoginState get loginState => _loginState;
bool get loggedIn => _loginState == ApplicationLoginState.loggedIn;
User _user;
User get user => _user;
void signOut() async {
// Cleaning the user which lead to the error later
_user = null;
_loginState = ApplicationLoginState.loggedOut;
notifyListeners();
}
}
My Profile screen which is accessible via named Route
class ProfileScreen extends StatelessWidget {
static const routeName = '/profile';
#override
Widget build(BuildContext context) {
final User user = Provider.of<AuthProvider>(context).user;
return Scaffold(
// drawer: AppDrawer(),
appBar: AppBar(
title: Text('Profile'),
),
body: Column(
children: [
Text(user.displayName),
FlatButton(
child: Text('logout'),
onPressed: () {
// Navigator.pushAndRemoveUntil(
// context,
// MaterialPageRoute(builder: (BuildContext context) => App()),
// ModalRoute.withName('/'),
// );
Provider.of<AuthProvider>(context, listen: false).signOut();
},
)
],
),
);
}
}
When I click the logout button from the profile screen, I don't understand why i get the error :
As I am using a Consumer<AuthProvider> at the top level of my app (this one includes my route (ProfileScreen), I thought it would redirect to the AuthenticationScreen due to the displayedWidget computed from the switch.
But it seems to rebuild the ProfileScreen first leading to the error. the change of displayedWidget do not seems to have any effect.
I'm pretty new to Provider. I don't understand what I am missing in the Provider pattern here ? Is my App / Consumer wrongly used ?
I hope you can help me understand what I've done wrong here ! Thank you.
Note : the commented Navigator.pushAndRemoveUntil redirect correctly to the login screen but I can see the error screen within a few milliseconds.
Your user is null, and you tried to get the name of him. You need to check it before using it. It will look like this:
user == null ?
Text("User Not Found!"),
Text(user.displayName),
From the provider API reference of Provider.of :
Obtains the nearest Provider up its widget tree and returns its
value.
If listen is true, later value changes will trigger a new State.build
to widgets, and State.didChangeDependencies for StatefulWidget.
So I think the line final User user = Provider.of<AuthProvider>(context).user; in your profile screen calls a rebuild when the _user variable is modified, and then the _user can be null in your ProfileScreen.
Have you tried to Navigator.pop the profile screen before clearing the _user variable?

how to make a validation before navigating to a route?

I have 2 pages, page1 andpage2. I want to validate that when the app is opened and there is no token or it is false, it redirects to page1 otherwise it redirects to page2, and when I have more pages I want that if there is a valid token, continues the normal flow of the navigation, I was trying this and I have this problem:
in the gif the token is not defined, the validation apparently does well, but the problem is that it continues to reload the current view, I am looking for something more optimal that avoids loading a route if some condition is not met
how can I solve that?
Map<String, WidgetBuilder> getRoutes() {
return <String, WidgetBuilder>{
'/': (BuildContext context) =>
checkNavigation("/", pag1(), context),
'page1': (BuildContext context) =>
checkNavigation("page1", page1(), context),
'page2': (BuildContext context) =>
checkNavigation("/page2", page2(), context)
};
}
dynamic checkNavigation(
String page, dynamic pageContext, BuildContext context) {
if (storage.token && page == "/") {
//Navigator.pushNamedAndRemoveUntil(context, 'page2', (_) => false);
return page2();
} else if (storage.token == false) {
//Navigator.pushNamedAndRemoveUntil(context, 'page1', (_) => false);
return page1();
} else {
return pageContext;
}
}
in my main:
.
.
.
MaterialApp(
title: 'route validation',
initialRoute: '/',
routes: getRoutes(),
It's better to control this behavior in your own abstractions and change routes only if necessary.
I would recommend to add some splash screen at root route and navigate to appropriate route, once token is initialized.
Future<void> asyncInit() {/*...*/}
void initState() {
/* ... */
asyncInit().then((_) => /* push appropriate first route */);
}
Map<String, WidgetBuilder> getRoutes() {
return <String, WidgetBuilder>{
'/': (BuildContext context) => SplashScreen(),
/* other routes */
};
}
If you need intercept other navigation events you can add your own proxy class, this may be easily implemented using Provider package
class MyNavigator {
final GlobalKey<NavigatorState> navigatorKey;
MyNavigator(this.navigatorKey);
static MyNavigator of(BuildContext context) => context.read<MyNavigator>();
Future<T> pushNamed<T extends Object>(
BuildContext context,
String routeName, {
Object arguments,
}) {
// add any additional logic and conditions here
return navigatorKey.currentState.pushNamed<T>(routeName, arguments: arguments);
}
// add any other methods you need
}
// somewhere at the top of widget tree above Widgets/Material/CupertinoApp widget.
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// add provider with navigator key above navigator
Provider(
create: (_) => MyNavigator(navigatorKey),
child: MaterialApp(navigatorKey: navigatorKey, /*...*/)
)
// use it
MyNavigator.of(context).pushNamed(...)
There is much work going right now to implement Navigator 2.0 with Router and Pages API, which gives you more control and flexibility on routing.
tracking issue:
https://github.com/flutter/flutter/issues/45938
design docs:
https://flutter.dev/go/navigator-with-router
https://flutter.dev/go/router-and-widgetsapp-integration
Pages API is already available in current stable release, but there is not enough documentation and examples at the moment.

Declarative auth routing with Firebase

Rather than pushing the user around with Navigator.push when they sign in or out, I've been using a stream to listen for sign in and sign out events.
StreamProvider<FirebaseUser>.value(
value: FirebaseAuth.instance.onAuthStateChanged,
)
It works great for the home route as it handles logging in users immediately if they're still authed.
Consumer<FirebaseUser>(
builder: (_, user, __) {
final isLoggedIn = user != null;
return MaterialApp(
home: isLoggedIn ? HomePage() : AuthPage(),
// ...
);
},
);
However, that's just for the home route. For example, if the user then navigates to a settings page where they click a button to sign out, there's no programmatic logging out and kicking to the auth screen again. I either have to say Navigator.of(context).pushNamedAndRemoveUntil('/auth', (_) => false) or get an error about user being null.
This makes sense. I'm just looking for possibly another way that when they do get logged out I don't have to do any stack management myself.
I got close by adding the builder property to the MaterialApp
builder: (_, widget) {
return isLoggedIn ? widget : AuthPage();
},
This successfully moved me to the auth page after I was unauthenticated but as it turns out, widget is actually the Navigator. And that means when I went back to AuthPage I couldn't call anything that relied on a parent Navigator.
What about this,you wrap all your screens that depend on this stream with this widget which hides from you the logic of listening to the stream and updating accordingly(you should provide the stream as you did in your question):
class AuthDependentWidget extends StatelessWidget {
final Widget childWidget;
const AuthDependentWidget({Key key, #required this.childWidget})
: super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {//you handle other cases...
if (snapshot.currentUser() != null) return childWidget();
} else {
return AuthScreen();
}
},
);
}
}
And then you can use it when pushing from other pages as follows:
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (ctx) => AuthDependentWidget(
childWidget: SettingsScreen(),//or any other screen that should listen to the stream
)));
I found a way to accomplish this (LoVe's great answer is still completely valid) in case anyone else steps on this issue:
You'll need to take advantage of nested navigators. The Root will be the inner navigator and the outer navigator is created by MaterialApp:
return MaterialApp(
home: isLoggedIn ? Root() : AuthPage(),
routes: {
Root.routeName: (_) => Root(),
AuthPage.routeName: (_) => AuthPage(),
},
);
Your Root will hold the navigation for an authed user
class Root extends StatefulWidget {
static const String routeName = '/root';
#override
_RootState createState() => _RootState();
}
class _RootState extends State<Root> {
final _appNavigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final canPop = _appNavigatorKey.currentState.canPop();
if (canPop) {
await _appNavigatorKey.currentState.maybePop();
}
return !canPop;
},
child: Navigator(
initialRoute: HomePage.routeName,
onGenerateRoute: (RouteSettings routeSettings) {
return MaterialPageRoute(builder: (_) {
switch (routeSettings.name) {
case HomePage.routeName:
return HomePage();
case AboutPage.routeName:
return AboutPage();
case TermsOfUsePage.routeName:
return TermsOfUsePage();
case SettingsPage.routeName:
return SettingsPage();
case EditorPage.routeName:
return EditorPage();
default:
throw 'Unknown route ${routeSettings.name}';
}
});
},
),
);
}
}
Now you can unauthenticate (FirebaseAuth.instance.signout()) inside of the settings page (or any other page) and immediately get kicked out to the auth page without calling a Navigator method.