I just started working with Flutter and I'm not gonna lie its amazing. One "problem" I have encountered, though, is that when you hit the back key It will navigate to the previous screen. I know that thats what should happen, but how can I change that? Also, the appBar button does the same, so I'm looking for a "fix" for that one as well.
If you require any code, please, let me know! Have a great day, and sorry for any misspellings!
First, you need to set names for your routes in the MaterialApp:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => MyHomePage(),
'/second': (context) => Page2(),
'/third': (context) => Page3(),
},
);
}
}
Then you need to wrap the Scaffold of the third page (or any another page from which you want to navigate back to the HomePage) with a WillPopScope to change what happens when user presses the back button. Finally you need to use popUntil to navigate back to the HomePage.
This is the code for the build method of the third page:
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
Navigator.of(context)
.popUntil(ModalRoute.withName("/"));
return Future.value(false);
},
child: Scaffold(
body: Center(
child: Container(
child: Text('third page'),
),
),
),
);
}
You can use
Navigator.of(context).pushNamedAndRemoveUntil(
'/', (Route<dynamic> route) => false);
Create a named route '/' that corresponds to the main screen and done.
Any more queries feel free to ask
If you specify "home" property in MaterialApp, there is no need to create any route. Navigation to homescreen can then be done as below:
Navigator.of(context).popUntil(ModalRoute.withName('/'));
Related
I am using Declarative Routing from AutoRoute flutter package.
class App extends StatelessWidget {
final _appRouter = AppRouter();
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: AutoRouterDelegate.declarative(
_appRouter,
routes: (_) => [
// if the user is logged in, they may proceed to the main App
if (authService().isLoggedIn)
HomeRoute()
// if they are not logged in, bring them to the Login page
else
LoginWrapperRoute(onLogin: () => authService().logIn),
],
),
routeInformationParser:
_appRouter.defaultRouteParser(includePrefixMatches: true));
}
}
IconButton(
onPressed: () {
context.pushRoute(const AlarmRoute());
},
icon: const Icon(Icons.notifications_active),
color: const Color(0xFF666666),
)
when I try to push or navigate to a page it gives me this error:
Widget': Pages stack can be managed by either the Widget (AutoRouter.declarative) or the (StackRouter)
Problem
As per the author of auto_route on their comment:
You can't use context.pushRoute (imperative) if you use the
declarative navigation, AutoRouterDelegate.declarative. . You have
to pick one.
There are many issues open so it would be great if auto_route can implement an improvement.
My solution:
I avoid using the "declarative" approach by auto_route, but I still have similar code. Near the root of my app, I had:
return AnimatedBuilder(
animation: settingsController,
builder: (BuildContext context, Widget? child) {
return MaterialApp.router(
restorationScopeId: 'app',
routerDelegate: _appRouter.delegate(
initialRoutes: (settingsController.settings.showOnboarding)
? [const OnboardingRoute()]
: [CurrentPoseRoute()]),
routeInformationParser: _appRouter.defaultRouteParser(),
theme: ThemeData(),
darkTheme: ThemeData.dark(),
themeMode: settingsService.settings.themeMode.toMaterial(),
);
},
);
My settingsController uses the mixin ChangeNotifier, so I could use notifyListeners();.
This does build a big piece of widget tree though. For me, it doesn't matter because I use this when switching from onboarding to normal user flow.
I've being studying Flutter for about 4 days, it's becoming tedius, now I want put something together. I want to show a different AppBar content at each screen. But it seems my Scaffold's AppBar is begin ignored.
Here's my code:
class Login extends StatelessWidget{
Widget build(BuildContext context){
return Scaffold(appBar: AppBar(title:Text( 'Identification') ), body: Stack( children: [RaisedButton( child: Text("logar"), onPressed: () => {
Navigator.pushNamed(context, '/other-screen')
})]));
}
}
The app bar above it seems invisible, doesn't show
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'title placedholder',
initialRoute: '/',
routes: {
'/' : (BuildContext context) => Login(),
'/other-screen' : (BuildContext context) => Scaffold(body: Row(children: <Widget>[],))
},
I have copied your code into DartPad and the first AppBar seems to be Ok.
The problem is with your second page.
Every time you create a new Scaffold, you need to add an AppBar to it.
Try running flutter clean and then running it again.
I find this fixes most issues when the code is correct but the application doesn't reflect the intended behaviour.
I'm trying to create a BlocListener that has the ability to listen to all pages/routes throughout the app just like how you can access a Bloc or a Provider all throughout the app if they are defined at root-level like in the code below
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<IdentityTokenProvider>(
create: (_) => IdentityTokenProvider(),
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(
create: (_) => AuthBloc(),
),
],
child: MaterialApp(
debugShowCheckedModeBanner: AppConfig.DEBUGGABLE,
theme: ThemeData(
// fontFamily: CustomFontStyle.montserrat,
),
home: AuthListener(
child: Center(
child: const MainApp(),
),
),
),
),
),
);
As you can see, I have providers, blocs, and one listener. I have no problem accessing the blocs and providers in other pages. My problem is the auth listener. I lose access to the AuthListener once I move to a different page (by removing stack) , because it is inside the MaterialApp. However, in this instance, I need that specific listener (AuthListener) to be inside a MaterialApp, because it consists of code that uses page navigations (which doesn't work if the implementation is done outside/above the widget tree of a MaterialApp), and makes us of the MaterialApp context for showing dialogs.
My implementation of page routing which removes the stack, which is another cause of losing access to the AuthListener
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => route),
(Route<dynamic> route) => false);
Why do I remove the route/page stack when moving to a different page?
I specifically use this after authentication. You don't really want a user to be able to press back button after logging in, andredirect the user back to the login page right? Usually back button should hide/close the app when they are logged in.
My AuthListener implementation
class AuthListener extends StatefulWidget {
final Widget child;
const AuthListener({Key key, #required this.child}) : super(key: key);
#override
_AuthListenerState createState() => _AuthListenerState();
}
class _AuthListenerState extends State<AuthListener> {
#override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthAuthenticated) {
PageRouterController.pushAndRemoveStack(context, const EcomPage());
} else if (state is AuthUnauthenticated) {
PageRouterController.pushAndRemoveStack(context, const LoginPage());
}
},
child: widget.child,
);
}
}
Is there a different way around this?
So I ended up defining a
static final GlobalKey<NavigatorState> navigatorKey = new GlobalKey();
and used it in my MaterialApp
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: App.DEBUGGABLE,
theme: ThemeData(
// fontFamily: CustomFontStyle.montserrat,
),
navigatorKey: App.navigatorKey,
home: Center(
child: const LoginPage(),
),
);
}
So then, whenever I have to navigate in cases where the implementation is outside the MaterialApp (in my case via the AuthListener which is found at root-level, above the MaterialApp), I can navigate via
App.navigatorKey.currentState.pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => route),
(Route<dynamic> route) => false);
Which means I can finally have access to the MaterialApp navigator and context even with the listener outside the MaterialApp which allows me to do both navigation and showing of dialogs
First of, I do know how BLoC suppose to work, the idea behind it and I know the difference between BlocProvider() and BlocProvider.value() constructors.
For simplicity, my application has 3 pages with a widget tree like this:
App() => LoginPage() => HomePage() => UserTokensPage()
I want my LoginPage() to have access to UserBloc because i need to log in user etc. To do that, I wrap LoginPage() builder at App() widget like this:
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: BlocProvider<UserBloc>(
create: (context) => UserBloc(UserRepository()),
child: LoginPage(),
),
);
}
}
That obviously works just fine. Then, if User logs in successfully, he is navigated to HomePage. Now, I need to have access to two different blocs at my HomePage so I use MultiBlocProvider to pass existing UserBloc further and create a brand new one named DataBloc. I do it like this:
#override
Widget build(BuildContext context) {
return BlocListener<UserBloc, UserState>(
listener: (context, state) {
if (state is UserAuthenticated) {
Navigator.of(context).push(
MaterialPageRoute<HomePage>(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
),
BlocProvider<DataBloc>(
create: (_) => DataBloc(DataRepository()),
),
],
child: HomePage(),
),
),
);
}
},
[...]
This also works. Problem happens when from HomePage user navigates to UserTokensPage. At UserTokensPage I need my already existing UserBloc that I want to pass with BlocProvider.value() constructor. I do it like this:
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text('My App'),
actions: <Widget>[
CustomPopupButton(),
],
),
[...]
class CustomPopupButton extends StatelessWidget {
const CustomPopupButton({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return PopupMenuButton<String>(
icon: Icon(Icons.more_horiz),
onSelected: (String choice) {
switch (choice) {
case PopupState.myTokens:
{
Navigator.of(context).push(
MaterialPageRoute<UserTokensPage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: UserTokensPage(),
),
),
);
}
break;
case PopupState.signOut:
{
BlocProvider.of<UserBloc>(context).add(SignOut());
Navigator.of(context).pop();
}
}
},
[...]
When I press button to navigate to MyTokensPage i get error with message:
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building Builder(dirty):
BlocProvider.of() called with a context that does not contain a Bloc of type UserBloc.
No ancestor could be found starting from the context that was passed to BlocProvider.of<UserBloc>().
This can happen if:
1. The context you used comes from a widget above the BlocProvider.
2. You used MultiBlocProvider and didn't explicity provide the BlocProvider types.
Good: BlocProvider<UserBloc>(create: (context) => UserBloc())
Bad: BlocProvider(create: (context) => UserBloc()).
The context used was: CustomPopupButton
What am I doing wrong? Is it because i have extracted PopupMenuButton widget that somehow loses blocs? I don't understand what I can be doing wrong.
You can just wrap the Blocs you need to access through out the app by wrapping it at the entry point of the app like this
runApp(
MultiBlocProvider(
providers: [
BlocProvider<UserBloc>(
create: (context) =>
UserBloc(UserRepository()),
),
],
child: App()
)
);
}
and you can access this bloc at anywhere of your app by
BlocProvider.of<UserBloc>(context).add(event of user bloc());
EDIT 10/03/2022
Since this thread became very popular I feel I need to add some comments.
This is valid solution if your goal is to use blocs that are not provided above your MaterialApp widget, but instead being declared somewhere down the widget tree by wrapping your widget (eg. some page) with BlocProvider making it possible for that widget to access the bloc.
It is easier to avoid problems by declaring all your blocs in MultiBlocProvider somewhere up the widget tree (like I said before), but this topic was not created with that in mind. Feel free to upvote and use this aproach described in Amesh Fernando response but do that knowing the difference.
I fixed it. Inside App widget i create LoginPage with
home: BlocProvider<UserBloc>(
create: (context) => UserBloc(UserRepository()),
child: LoginPage(),
At LoginPage I simply wrap BlocBuilders one into another
Widget build(BuildContext context) {
return BlocListener<UserBloc, UserState>(
listener: (context, state) {
if (state is UserAuthenticated) {
Navigator.of(context).push(
MaterialPageRoute<HomePage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: BlocProvider<NewRelicBloc>(
create: (_) => NewRelicBloc(NewRelicRepository()),
child: HomePage(),
),
),
),
);
}
},
[...]
PopupMenuButton navigates User to TokenPage with
Navigator.of(context).push(
MaterialPageRoute<UserTokensPage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: UserTokensPage(),
),
),
);
And that solved all my problems.
Solution
Method A: Access UserBloc provider instance directly without passing it
I prefer this solution since it requires less code.
A.1 Wrap CustomPopupButton instance with provider Consumer so it rebuilds itself whenever UserBloc notifies listeners of value changes.
Change this:
actions: <Widget>[
CustomPopupButton(),
],
To:
actions: <Widget>[
Consumer<UserBloc>(builder: (BuildContext context, UserBloc userBloc, Widget child) {
return CustomPopupButton(),
});
],
A.2 Change Provider instance invocation inside the stateless widget to disable listening to value changes -- "listening" and resulting "rebuilds" are already done by Consumer.
A.2.1 Change this:
value: BlocProvider.of<UserBloc>(context),
To:
value: BlocProvider.of<UserBloc>(context, listen: false),
A.2.2 And change this:
BlocProvider.of<UserBloc>(context).add(SignOut());
To:
BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());
Method B: pass UserBloc provider instance
Same thing as Method A, but:
In A.1 you'd pass userBloc like this: return CustomPopupButton(userBloc: userBloc),.
You'd declare final UserBloc userBloc; member property inside CustomPopupButton.
In A.2 you'd do this: userBloc.add(SignOut()); instead of BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());
Explanation
flutter_bloc is using Provider, to be aware what's going on it's better understand Provider. Please refer to my answer here to understand my answer to your question, and to understand Provider and listen flag better.
Change name of context in builder whether in bottomSheet or materialPageRoute.
So that bloc can access parent context through context
unless it's going to take context from builder (bottom sheet). This can lead
to an error which you can't reach the instance of bloc .
showModalBottomSheet(
context: context,
builder: (context2) { ===> change here to context2
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: widgetA(),
),
}
You need to either decompose your widget into two widgets (which I recommend for testability reasons) or use a Builder widget to get a child context.
class MyHomePage extends StatelessWidget { #override Widget build(BuildContext context) { return BlocProvider( create: (_) => TestCubit(), child: MyHomeView(), ); } } class MyHomeView extends StatelessWidget { #override Widget build(BuildContext context) { return Scaffold( body: Center( child: RaisedButton(onPressed: () => BlocProvider.of<TestCubit>(context)...) ), ); } }
source: solved by Felix Angelov, https://github.com/felangel/bloc/issues/2064
you don't have to use BlocProvider.value() to navigate to another screen, you can just wrap MaterialApp into BlocProvider as a child of it
WillPopScope's callback isn't being fired when I expected it to be. How can I use WillPopScope inside a Navigator so a route can handle it's own back pressed behaviour.
I'm learning to use flutter, and I came across Navigator for creating multiple pages (I know there are other navigation widgets but I'm after something which only supports programatic navigation so I can handle all the UI).
The next thing I thought to look at with the Navigator was going back, I found WillPopScope which wraps a component and has a callback that gets called when the back button is pressed (if the component is rendered). This seemed ideal for me since I only want the callback to be called if the Widget is rendered.
I tried to use WillPopScope within a Navigator with the intention for only the rendered route to have it's callback (onWillPop) called when the back button is pressed, but putting WillPopScope within a Navigator does nothing (the callback isn't called).
The intention is to have a Navigator navigate to top level routes and those routes themselves potentially having Navigators, so putting WillPopScope inside means each route (or subroute) is responsible for it's own back navigation.
Many questions I've looked up seem to focus on MaterialApp, Scaffold, or other ways of handling navigation; I'm looking how to handle this without the UI that those things bring in (a use case could be a quiz app, where you need to click a next button to move forward, or something similar).
Here is the minimal main.dart file I expect route 2 to handle it's own back navigation (to keep things simple I've not put nested routes in this example).
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Navigation',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext parentContext) {
return Container(
color: Theme.of(context).colorScheme.primary,
child: Navigator(
initialRoute: "1",
onGenerateRoute: (settings) {
return PageRouteBuilder(pageBuilder: (BuildContext context,
Animation animation, Animation secondaryAnimation) {
switch (settings.name) {
case "1":
return Container(
color: Colors.red,
child: GestureDetector(onTap: () {
debugPrint("going from 1 to 2");
Navigator.of(context).pushNamed("2");
}));
case "2":
return WillPopScope(
child: Container(color: Colors.green),
onWillPop: ()async {
debugPrint("popping from route 2 disabled");
return false;
},
);
default:
throw Exception("unrecognised route \"${settings.name}\"");
}
});
})
);
}
}
When the back button is pressed in either route 1 or 2, the app exits. I expect that to only be the case in route 1, and route 2 should only log popping from route 2 disabled (with no navigation away from the page or leaving the app).
From what I understand, Navigator and WillPopScope are the Widgets to use for this sort of thing, but if not then how would I implement self contained (potentially nested) routes.
You can forward onWillPop to another navigator by using below code:
onWillPop: () async {
return !await otherNavigatorKey.currentState.maybePop();
}
When you create a Navigator you are automatically creating a new stack. Everything you .push below your Navigator using Navigator.of(context) is added to the new Navigator stack you just created. However, when you press the backbutton it doesn't know what you want to pop (if the root navigator or the new navigator).
First you need to add a WillPopScope outside your Navigator and add a NavigatorKey to your Navigator
return WillPopScope(
onWillPop: () => _backPressed(_yourKey),
child: Scaffold(
body: Navigator(
key: _yourKey
onGenerateRoute: _yourMaterialPageRouteLogic,
),
bottomNavigationBar: CustomNavigationBar(navBarOnTapCallback),
),
);
Your key can be instatiated like this
GlobalKey<NavigatorState> _yourKey = GlobalKey<NavigatorState>();
The _backPressed method will receive any backPressed you do on your device. By definition it returns true, and we don't always want to pop.
We've added a key to the navigator, and now it will be used to understand whether the new Navigator has anything in the stack in order to be popped (ie if it 'canPop').
Future<bool> _backPressed(GlobalKey<NavigatorState> _yourKey) async {
//Checks if current Navigator still has screens on the stack.
if (_yourKey.currentState.canPop()) {
// 'maybePop' method handles the decision of 'pop' to another WillPopScope if they exist.
//If no other WillPopScope exists, it returns true
_yourKey.currentState.maybePop();
return Future<bool>.value(false);
}
//if nothing remains in the stack, it simply pops
return Future<bool>.value(true);
Next you just need to add the WillPopScope anywhere inside your Navigator. Its onWillPop will be called with the logic you want. Don't forget to return true or false depending on whether you want to pop it or not :)
Here is a example of my onWillPop method (_popCamera) in a WillPopScope widget, which is placed inside my Navigator widget tree. In this example I've added a dialog when the user presses the back button in the Camera Widget:
static Future<bool> _popCamera(BuildContext context) {
debugPrint("_popCamera");
showDialog(
context: context,
builder: (_) => ExitCameraDialog(),
barrierDismissible: true);
return Future.value(false);
}
class ExitCameraDialog extends StatelessWidget {
const ExitCameraDialog({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Leaving camera forever'),
content:
Text('Are you S-U-R-E, S-I-R?'),
actions: <Widget>[
FlatButton(
child: Text('no'),
onPressed: Navigator.of(context).pop,
),
FlatButton(
//yes button
child: Text('yes'),
onPressed: () {
Navigator.of(context).pop();
_yourKey.currentState.pop();
},
),
],
);
}
Hope it's clear!