Right way to handle navigation using BLoC - dart

Hello guys I'm using BLoC for app I'm currently developing but there some cases which I'm clueless like when you do login you fire API call and wait for result naturally I would send loading state and show loader but after
that finishes how to handle for example navigating to different screen.
I've currently have something like this
typedef void LoginSuccessCallback();
class LoginBloc(){
LoginBloc(Api this.api,LoginSuccessCallback loginSuccesCallback){
_login.switchMap((ev) => api.login(ev.payload.email,ev.payload.password)).listen((_) => loginSuccessCallback);
}
}
But I'm sure there is much cleaner way for handling this I've tried to search some samples which have something similar but couldn't find anything.

Edit: After a few months with this solution in place, I noticed that there are a few problems with it:
Android hardware back button does not work
The app resets when you toggle "inspect" mode.
No transitions possible
No guarantee that no forbidden route is displayed
So I no longer recommend using this approach!
For normal user-initiated navigation, you don't need the BLoC pattern at all. Just use the Navigator.
Login is a special case. Following the BLoC pattern, it would make sense to provide a isAuthenticated stream:
abstract class MyBloc {
Stream<bool> get isAuthenticated;
}
Your app will probably have 2 different named route trees: One for logged in users, and one for anonymous users:
final Map<String, WidgetBuilder> anonymousRoutes = {
'/': (context) => new LoginScreen(), // default for anon
'/register': (context) => new RegisterScreen(),
};
final Map<String, WidgetBuilder> authenticatedRoutes = {
'/': (context) => new HomeScreen(), // default for logged in
'/savings': (context) => new SavingsScreen(),
// ...
};
Usually the Navigator and its named routes are tightly coupled to the MaterialApp, but you can also define your own that is rebuilt when the isAuthenticated stream is updated:
class MyApp extends StatelessWidget {
const MyApp({Key key, this.bloc}) : super(key: key);
final MyBloc bloc;
#override
Widget build(BuildContext context) {
return MaterialApp(
builder: (BuildContext context, Widget child) {
return StreamBuilder<bool>(
stream: bloc.isAuthenticated,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (!snapshot.hasData) {
return Text('loading...');
}
bool isAuthenticated = snapshot.data;
return _buildNavigator(isAuthenticated);
},
);
},
);
}
}
Navigator _buildNavigator(bool isAuthenticated) {
// different route tree and different default route depending on auth state
final routes = isAuthenticated ? authenticatedRoutes : anonymousRoutes;
return Navigator(
key: new ValueKey(isAuthenticated),
onGenerateRoute: (RouteSettings settings) {
final name = settings.name;
return new MaterialPageRoute(
builder: routes[name],
settings: settings,
);
},
onUnknownRoute: (RouteSettings settings) {
throw Exception('unknown route');
},
);
}
Sadly right now (2018-07-14) there are a 2 conflicting asserts in the Flutter code which you have to remove to make the code above work (you can just edit it with your IDE):
Line 93 and 96 in packages\flutter\lib\src\widgets\app.dart
//assert(navigatorObservers != null),
//assert(onGenerateRoute != null || navigatorObservers == const <NavigatorObserver>[]),

Related

Changes to Stream Provider after migrating to null safety version Provider 5.0.0

I have migrated to dart null safety version. The command in migrate function fixed most issues. However, I have a Stream Provider which handles the User Session using Firebase. After the migration to Provider version 5.0.0 the app is crashing. Below is my main class.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await Firebase.initializeApp();
runApp(EasyLocalization(
child: MyApp(),
path: "assets/langs",
saveLocale: true,
supportedLocales: [
Locale('en', 'US'),
Locale('en', 'GB'),
Locale('es', 'ES'),
],
));
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AuthenticationProvider>(
create: (_) => AuthenticationProvider(FirebaseAuth.instance),
),
StreamProvider(
create: (context) =>
context.read<AuthenticationProvider>().authState,
initialData: null,
child: Authenticate())
],
child: ScreenUtilInit(
builder: () => MaterialApp(
builder: (context, child) {
return ScrollConfiguration(
//Removes the whole app's scroll glow
behavior: MyBehavior(),
child: child!,
);
},
title: 'SampleApp',
debugShowCheckedModeBanner: false,
theme: theme(),
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
home: Authenticate(),
routes: routes,
),
),
);
}
}
class Authenticate extends StatelessWidget {
#override
Widget build(BuildContext context) {
final firebaseUser = context.watch<User>();
if (firebaseUser != null) {
FirebaseFirestore.instance
.collection('user')
.doc(firebaseUser.uid)
.get()
.then((value) {
UserData.name = value.data()!['name'];
UserData.age = value.data()!['age'];
});
return View1();
}
return View2();
}
}
class MyBehavior extends ScrollBehavior {
#override
Widget buildViewportChrome(
BuildContext context, Widget child, AxisDirection axisDirection) {
return child;
}
}
The app is crashing with the following exception
The following ProviderNotFoundException was thrown building Authenticate(dirty):
Error: Could not find the correct Provider above this Authenticate Widget
This happens because you used a BuildContext that does not include the provider
of your choice. There are a few common scenarios:
You added a new provider in your main.dart and performed a hot-reload.
To fix, perform a hot-restart.
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that Authenticate is under your MultiProvider/Provider.
This usually happens when you are creating a provider and trying to read it immediately.
The answer is very simple. The variable that I was trying to listen to should be nullable.
So, basically
final firebaseUser = context.watch<User?>();

How to detect whether the route is the current route in its build method

I use FutureBuilder in my routes, which displays data after getting data from database.
I found that when I open the second route, the build method of home route was called even home route is not the current route. However, I hope the build method does not get data if home route isn't the current one.
That's the code I try to implement:
class HomeRoute extends StatefulWidget { State<StatefulWidget> createState() => HomeRouteState(); }
class HomeRouteState extends State<HomeRoute> {
#override
Widget build(BuildContext context) => Scaffold(
//...
drawer: Drawer(
// There is a ListTile that can push SecondRoute
),
body: FutureBuilder(
future: _getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {/* ... */}
),
);
_getData() async {
if(/* this route is the current one */) {
// get data
}
}
}
So basically you want to prevent the build() of your home page from getting called when it is not visible. Here is what you can try.
bool _isHomeVisible = true;
#override
Widget build(BuildContext context) {
return _isHomeVisible ? YourWidgetImplementation() : Container();
}
And before you navigate do this.
void _navigateToNewPage() {
_isHomeVisible = false; // going to new page, make it false
Navigator.push(...).then((_) {
_isHomeVisible = true; // coming back to home page, make it true
});
}

Flutter: local notification plugin, navigate to specific screen when the user tap the notification

I`m using local notification plugin and everything works fine except that when i tap on the notification.i want to navigate to specific screen when the user tap the notification
Future onSelectNotification(String payload) async {
//convert payload json to notification model object
try{
Map notificationModelMap = jsonDecode(payload);
NotificationModel model = NotificationModel.fromJson(notificationModelMap);
await Navigator.push(
context,// it`s null!
new MaterialPageRoute(
builder: (context) => CommitmentPage(model)));}
catch(e){print(e.toString());}
}
but the context always null and gives me an exception
NoSuchMethodError: The method 'ancestorStateOfType' was called on null.
Edit
i tried to use navigatorKey and pass it to material app as suggested by #Günter Zöchbauer but it gives me another exception
Navigator operation requested with a context that does not include a Navigator.
The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static final navigatorKey = new GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey:navigatorKey ,
title: 'notification',
theme: ThemeData(
primarySwatch: Colors.purple,
),
home: new RootPage(auth: new Auth(),),
);
}
}
ReminderPage
class ReminderPage extends StatefulWidget {
#override
_ReminderPageState createState() => _ReminderPageState();
}
class _ReminderPageState extends State<ReminderPage> {
final flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
....
#override
void initState() {
super.initState();
_initLocalNotification();
....
}
// this method is called when u tap on the notification
Future onSelectNotification(String payload) async {
//convert payload json to notification model object
Map notificationModelMap = jsonDecode(payload);
NotificationModel model = NotificationModel.fromJson(notificationModelMap);
try{
await Navigator.push(
MyApp.navigatorKey.currentState.context,
new MaterialPageRoute(
builder: (context) => CommitmentPage(model)));}
catch(e){print(e.toString());}
}
}
Edit 2
instead of using
await Navigator.push(
MyApp.navigatorKey.currentState.context,
new MaterialPageRoute(
builder: (context) => CommitmentPage(model)));}
catch(e){print(e.toString());}
i used
await MyApp.navigatorKey.currentState.push(MaterialPageRoute(builder: (context) => CommitmentPage(model)));
and it worked fine, but when i tap on the notification after killing the app, it takes me to the home page not to the desired one!
i think navigatorKey has not been initialized yet!
1. FMC payload
{
"notification": {
"body": "Hey, someone book your product",
"title": "production booking"
},
"priority" : "high",
"data": {
"action" : "BOOKING" //to identify the action
},
"to": "deviceFCMId"
}
2. Set the payload data in localnotification.show method
showNotification(RemoteMessage message) {
RemoteNotification notification = message.notification;
AndroidNotification android = message.notification?.android;
String action = message.data['action']; // get the value set in action key from FCM Json in Step1
// local notification to show to users using the created channel.
if (notification != null && android != null) {
flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: androidNotificationDetails, iOS: iOSNotificationDetails),
payload: action // set the value of payload
);
}
}
3. Create a navigatorKey in MyApp class
class _MyAppState extends State<MyApp> {
final GlobalKey<NavigatorState> navigatorKey =
GlobalKey(debugLabel: "Main Navigator"); //
}
4. Set navigatorkey on your MaterialApp
#override
Widget build(BuildContext context) {
//this change the status bar color to white
return MaterialApp(
navigatorKey: navigatorKey,
....
5. on initState, init localnotificationPlugin and declare the onSelectNotification method
#override
void initState() {
super.initState();
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
}
Future<dynamic> onSelectNotification(payload) async {
// implement the navigation logic
}
6. Navigation logic example
Future<dynamic> onSelectNotification(payload) async {
// navigate to booking screen if the payload equal BOOKING
if(payload == "BOOKING"){
this.navigatorKey.currentState.pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => BookingScreen()),
(Route<dynamic> route) => false,
);
}
}
Pass a navigatorKey to MaterialApp and use this key to get the context. This context contains a Navigator and you can use it to switch to whatever page you want.
https://docs.flutter.io/flutter/material/MaterialApp/navigatorKey.html
With the latest change you've done, you being taken to the home page, is more to do with the fact that you instantiate and initialise the plugin a couple of pages further in your app. considering the app got killed, the plugin would've been killed too. you should look into initialising the plugin closer to when your app starts e.g. around when the app is created or when the first page is created, depending on the behaviour you want. In this issue https://github.com/MaikuB/flutter_local_notifications/issues/99, another dev has been able to change the first page shown if the app is killed but that may different to what you want to happen.

Route Guards in Flutter

In Angular, one can use the canActivate for route guarding.
In Flutter, how would one go about it? Where is the guard placed? How do you guard routes?
I'm thinking along the lines of this:
User logged in. Their token is stored in Shared Preference (the right way to store tokens? )
User closed the app.
User opens app again. As the application starts, it determines if user is logged in (perhaps a service that checks the storage for token), and then
If logged in, load the homepage route
If not logged in, load the login page
I was stumbling over this problem too and ended up using a FutureBuilder for this. Have a look at my routes:
final routes = {
'/': (BuildContext context) => FutureBuilder<AuthState>(
// This is my async call to sharedPrefs
future: AuthProvider.of(context).authState$.skipWhile((_) => _ == null).first,
builder: (BuildContext context, AsyncSnapshot<AuthState> snapshot) {
switch(snapshot.connectionState) {
case ConnectionState.done:
// When the future is done I show either the LoginScreen
// or the requested Screen depending on AuthState
return snapshot.data == AuthState.SIGNED_IN ? JobsScreen() : LoginScreen()
default:
// I return an empty Container as long as the Future is not resolved
return Container();
}
},
),
};
If you want to reuse the code across multiple routes you could extend the FutureBuilder.
I don't think there is a route guarding mechanism per se, but you can do logic in the main function before loading the app, or use the onGenerateRoute property of a MaterialApp. One way to do that in your case is to await an asynchronous function that checks if the user is logged in before loading the initial route. Something like
main() {
fetchUser().then((user) {
if (user != null) runApp(MyApp(page: 'home'));
else runApp(MyApp(page: 'login'));
});
}
But you may also be interested in the way the Shrine app does it. They have the login page as the initial route in any case and remove it if the user is logged in. That way the user sees the login page until it has been determined whether or not they log in. I've included the relevant snippet below.
class ShrineApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shrine',
home: HomePage(),
initialRoute: '/login',
onGenerateRoute: _getRoute,
);
}
Route<dynamic> _getRoute(RouteSettings settings) {
if (settings.name != '/login') {
return null;
}
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => LoginPage(),
fullscreenDialog: true,
);
}
}
If you don't want them to see the login page at all if they are logged in, use the first approach and you can control the splash screen that shows before runApp has a UI by exploring this answer.
You can use auto_route extension. This extension is awesome for dealing with routes and provides good way of using guards. Please refer to the documentation:
https://pub.dev/packages/auto_route#route-guards
I came up with the following solution for a web project, which allows me to easily introduce guarded routes without having to worry that an unauthorized user is able to access sensitive information.
The GuardedRoute class looks like this:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:kandabis_core/core.dart' as core;
Widget _defaultTransitionsBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return child;
}
class GuardedRoute extends PageRouteBuilder {
GuardedRoute({
#required final String guardedRoute,
#required final String fallbackRoute,
#required final Stream<bool> guard,
#required final core.Router router,
final RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
final bool maintainState = true,
final Widget placeholderPage,
})
: super(
transitionsBuilder: transitionsBuilder,
maintainState: maintainState,
pageBuilder: (context, animation, secondaryAnimation) =>
StreamBuilder(
stream: guard,
builder: (context, snapshot) {
if (snapshot.hasData) {
// navigate to guarded route
if (snapshot.data == true) {
return router.routes[guardedRoute](context);
}
// navigate to fallback route
return router.routes[fallbackRoute](context);
}
// show a placeholder widget while the guard stream has no data yet
return placeholderPage ?? Container();
}
),
);
}
Using the guarded route is easy. You can define a guarded route and a fallback route (like a login page). Guard is a Stream which decides if the user can navigate to the guarded route. this is my Router class which shows how to use the GuardedRoute class:
class BackendRouter extends core.BackendRouter {
BackendRouter(
this._authenticationProvider,
this._logger
);
static const _tag = "BackendRouter";
core.Lazy<GlobalKey<NavigatorState>> _navigatorKey =
core.Lazy(() => GlobalKey<NavigatorState>());
final core.AuthenticationProvider _authenticationProvider;
final core.Logger _logger;
#override
Map<String, WidgetBuilder> get routes => {
core.BackendRoutes.main: (context) => MainPage(),
core.BackendRoutes.login: (context) => LoginPage(),
core.BackendRoutes.import: (context) => ImportPage(),
};
#override
Route onGenerateRoute(RouteSettings settings) {
if (settings.name == core.BackendRoutes.login) {
return MaterialPageRoute(
settings: settings,
builder: routes[settings.name]
);
}
return _guardedRoute(settings.name);
}
#override
GlobalKey<NavigatorState> get navigatorKey => _navigatorKey();
#override
void navigateToLogin() {
_logger.i(_tag, "navigateToLogin()");
navigatorKey
.currentState
?.pushNamed(core.BackendRoutes.login);
}
#override
void navigateToImporter() {
_logger.i(_tag, "navigateToImporter()");
navigatorKey
.currentState
?.pushReplacement(_guardedRoute(core.BackendRoutes.import));
}
GuardedRoute _guardedRoute(
String route,
{
maintainState = true,
fallbackRoute = core.BackendRoutes.login,
}) =>
GuardedRoute(
guardedRoute: route,
fallbackRoute: fallbackRoute,
guard: _authenticationProvider.isLoggedIn(),
router: this,
maintainState: maintainState,
placeholderPage: SplashPage(),
);
}
And your application class looks like this:
class BackendApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
// get router via dependency injection
final core.BackendRouter router = di.get<core.BackendRouter>();
// create app
return MaterialApp(
onGenerateRoute: (settings) => router.onGenerateRoute(settings),
navigatorKey: router.navigatorKey,
);
}
}
You can use flutter_modular package to do that. It´s a library that try to keep the same features than angular. Take a look of it documentation.
Modular Docs
My solution is to build in a route guard system, much like the other libraries out there but where we can still use the original Navigator where needed, open a modal as a named route, chain guards, and add redirects. Its really basic but can be built on quite easily.
It seems like a lot but you'll just need 3 new files to maintain along with your new guards:
- router/guarded_material_page_route.dart
- router/route_guard.dart
- router/safe_navigator.dart
// Your guards go in here
- guards/auth_guard.dart
...
First create a new class that extends MaterialPageRoute, or MaterialWithModalsPageRoute if you're like me and want to open the Modal Bottom Sheet package. I've called mine GuardedMaterialPageRoute
class GuardedMaterialPageRoute extends MaterialWithModalsPageRoute {
final List<RouteGuard> routeGuards;
GuardedMaterialPageRoute({
// ScrollController is only needed if you're using the modals, as i am in this example.
#required Widget Function(BuildContext, [ScrollController]) builder,
RouteSettings settings,
this.routeGuards = const [],
}) : super(
builder: builder,
settings: settings,
);
}
Your route guards will look like this:
class RouteGuard {
final Future<bool> Function(BuildContext, Object) guard;
RouteGuard(this.guard);
Future<bool> canActivate(BuildContext context, Object arguments) async {
return guard(context, arguments);
}
}
You can now add GuardedMaterialPageRoutes to your router file like so:
class Routes {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case homeRoute:
// These will still work with our new Navigator!
return MaterialPageRoute(
builder: (context) => HomeScreen(),
settings: RouteSettings(name: homeRoute),
);
case locationRoute:
// Following the same syntax, just with a routeGuards array now.
return GuardedMaterialPageRoute(
// Again, scrollController is only if you're opening a modal as a named route.
builder: (context, [scrollController]) {
final propertiesBloc = BlocProvider.of<PropertiesBloc>(context);
final String locationId = settings.arguments;
return BlocProvider(
create: (_) => LocationBloc(
locationId: locationId,
propertiesBloc: propertiesBloc,
),
child: LocationScreen(),
);
},
settings: RouteSettings(name: locationRoute),
routeGuards: [
// Now inject your guards, see below for what they look like.
AuthGuard(),
]
);
...
Create your async guard classes like so, as used above in our router.
class AuthGuard extends RouteGuard {
AuthGuard() : super((context, arguments) async {
final auth = Provider.of<AuthService>(context, listen: false);
const isAnonymous = await auth.isAnonymous();
return !isAnonymous;
});
}
Now you'll need a new class that handles your navigation. Here you check if you have access and simply run through each guard:
class SafeNavigator extends InheritedWidget {
static final navigatorKey = GlobalKey<NavigatorState>();
#override
bool updateShouldNotify(SafeNavigator oldWidget) {
return false;
}
static Future<bool> popAndPushNamed(
String routeName, {
Object arguments,
bool asModalBottomSheet = false,
}) async {
Navigator.of(navigatorKey.currentContext).pop();
return pushNamed(routeName, arguments: arguments, asModalBottomSheet: asModalBottomSheet);
}
static Future<bool> pushNamed(String routeName, {
Object arguments,
bool asModalBottomSheet = false,
}) async {
// Fetch the Route Page object
final settings = RouteSettings(name: routeName, arguments: arguments);
final route = Routes.generateRoute(settings);
// Check if we can activate it
final canActivate = await _canActivateRoute(route);
if (canActivate) {
// Only needed if you're using named routes as modals, under the hood the plugin still uses the Navigator and can be popped etc.
if (asModalBottomSheet) {
showCupertinoModalBottomSheet(
context: navigatorKey.currentContext,
builder: (context, scrollController) =>
(route as GuardedMaterialPageRoute)
.builder(context, scrollController));
} else {
Navigator.of(navigatorKey.currentContext).push(route);
}
}
return canActivate;
}
static Future<bool> _canActivateRoute(MaterialPageRoute route) async {
// Check if it is a Guarded route
if (route is GuardedMaterialPageRoute) {
// Check all guards on the route
for (int i = 0; i < route.routeGuards.length; i++) {
// Run the guard
final canActivate = await route.routeGuards[i]
.canActivate(navigatorKey.currentContext, route.settings.arguments);
if (!canActivate) {
return false;
}
}
}
return true;
}
}
To make it all work you will need to add the SafeNavigator key to your Material app:
MaterialApp(
navigatorKey: SafeNavigator.navigatorKey,
...
)
And now you can navigate to your routes and check if you have access to them like this:
// Opens a named route, either Guarded or not.
SafeNavigator.pushNamed(shortlistRoute);
// Opens a named route as a modal
SafeNavigator.pushNamed(shortlistRoute, asModalBottomSheet: true);
// Pops the current route and opens a named route as a modal
SafeNavigator.popAndPushNamed(shortlistRoute, asModalBottomSheet: true);

Flutter Navigator not working

I have app with two screens, and I want to make push from 1st to second screen by pressing button.
Screen 1
import 'package:flutter/material.dart';
import './view/second_page.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MainScreen();
}
}
class MainScreen extends State<MyApp> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Title")
),
body: new Center(
child: new FlatButton(child: new Text("Second page"),
onPressed: () {
Navigator.push(context,
new MaterialPageRoute(
builder: (context) => new SecondPage()))
}
)
)
)
);
}
}
Screen 2
import 'package:flutter/material.dart';
class SecondPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new SecondPageState();
}
}
class SecondPageState extends State<SecondPage> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
),
body: new Center(
child: new Text("Some text"),
),
);
}
}
Push not happening and I got this
The following assertion was thrown while handling a gesture: Navigator
operation requested with a context that does not include a Navigator.
The context used to push or pop routes from the Navigator must be that
of a widget that is a descendant of a Navigator widget.
Another exception was thrown: Navigator operation requested with a
context that does not include a Navigator.
What is wrong?
Think of the widgets in Flutter as a tree, with the context pointing to whichever node is being built with the build function. In your case, you have
MainScreen <------ context
--> MaterialApp
(--> Navigator built within MaterialApp)
--> Scaffold
--> App Bar
--> ...
--> Center
--> FlatButton
So when you're using the context to find the Navigator, you're using a context for the MainScreen which isn't under the navigator.
You can either make a new Stateless or Stateful Widget subclass to contain your Center + FlatButton, as the build function within those will point at that level instead, or you can use a Builder and define the builder callback (which has a context pointing at the Builder) to return the Center + FlatButton.
Just make the MaterialApp class in main method as this example
void main() => runApp(MaterialApp(home: FooClass(),));
it works fine for me,
I hope it will work with you
There are two main reasons why the route cannot be found.
1) The Route is defined below the context passed to Navigator.of(context) - scenario which #rmtmackenzie has explained
2) The Route is defined on the sibling branch e.g.
Root
-> Content (Routes e.g. Home/Profile/Basket/Search)
-> Navigation (we want to dispatch from here)
If we want to dispatch a route from the Navigation widget, we have to know the reference to the NavigatorState. Having a global reference is expensive, especially when you intend to move widget around the tree. https://docs.flutter.io/flutter/widgets/GlobalKey-class.html. Use it only where there is no way to get it from Navigator.of(context).
To use a GlobalKey inside the MaterialApp define navigatorKey
final navigatorKey = GlobalKey<NavigatorState>();
Widget build(BuildContext context) => MaterialApp {
navigatorKey: navigatorKey
onGenerateRoute : .....
};
Now anywhere in the app where you pass the navigatorKey you can now invoke
navigatorKey.currentState.push(....);
Just posted about it https://medium.com/#swav.kulinski/flutter-navigating-off-the-charts-e118562a36a5
There is an another very different work around about this issue, If you are using Alarm Manager (Android), and open back to your Application. If you haven't turned on the screen before navigation, the navigator will never work. Although this is a rare usage, I think It should be a worth to know.
Make sure the route table mentioned in the same context:
#override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: _isUserLoggedIn(),
builder: (ctx, loginSnapshot) =>
loginSnapshot.connectionState == ConnectionState.waiting ?
SplashScreen() : loginSnapshot.data == true ? AppLandingScreen(): SignUpScreen()
),
routes: {
AppLandingScreen.routeName: (ctx) => AppLandingScreen(),
},
);
}
I faced this issue because I defined the route table in different build method.
Am a newbie and have spent two days trying to get over the Navigtor objet linking to a black a screen.
The issue causing this was dublicated dummy data. Find Bellow the two dummny data blocks:
**Problematic data **- duplicate assets/image:
_buildFoodItem('assets/plate1.png', 'Salmon bowl', '\$24'),
_buildFoodItem('assets/plate2.png', 'Spring bowl', '\$13'),
_buildFoodItem('assets/plate1.png', 'Salmon bowl', '\$24'),
_buildFoodItem('assets/plate5.png', 'Berry bowl', '\$34'),
**Solution **- after eliminating duplicated image argument:
_buildFoodItem('assets/plate1.png', 'Salmon bowl', '\$24'),
_buildFoodItem('assets/plate2.png', 'Spring bowl', '\$13'),
_buildFoodItem('assets/plate6.png', 'Avocado bowl', '\$34'),
I hope this helps someone,,,,,,,
If the navigator is not working, it can be due to many reasons but the major one is that the navigator not finds the context.
So, to solve this issue try to wrap your widget inside Builder because the builder has its own context...

Resources