How to navigate to another page on load in Flutter - flutter

I want to navigate to the login page if there is no logged in user, otherwise display the homepage. I thought of calling Navigator.of(context).push() conditionally inside the build method but that triggers an exception. Is there some method I'm missing that I can override?
Update to add the Homepage widget
class HomePage extends StatelessWidget {
final AppUser user;
const HomePage({Key key, this.user}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Rera Farm'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry>[
PopupMenuItem(
child: ListTile(
title: Text('Settings'),
onTap: () {
Navigator.pop(context);
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context)
=> SettingsPage()
));
},
),
),
];
},
)
],
),
body: _buildBody(context));
}
And the container
class HomePageContainer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (BuildContext context, _ViewModel vm) {
return HomePage(
user: vm.user,
);
},
);
}
}

You need to either use a ternary in the onTap if you're using the settings button or, if you just want it to automatically send the user to the correct page when the app starts, you can put the ternary in the MyApp build method.
If you are using the settings button and just want it to pop back to the previous page if the person is not logged in then you can change NotLoggedIn() to a pop.
For some strange reason SO is refusing to post the code when it is properly formatted with four spaces, exactly as it asks, so I'm just going to make a gist.
https://gist.github.com/ScottS2017/3288c7e7e9a014430e56dd6be4c259ab

Here's how I end up doing it. I do the checks in the main method, so the user sees the splash screen set in manifest while those weird checks are made:
void main() {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences.getInstance().then((instance) {
_token = instance.getString("token");
final _loggedIn = _token != null && token != "";
runApp(MyApp(loggedIn: _loggedIn));
});
}
Then in your app add the parameters to switch:
class MyApp extends StatelessWidget {
final bool loggedIn;
MyApp({this.key, this.loggedIn});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: loggedIn ? HomePage() : LoginPage(),
);
}
}
You can also use Navigator.pushReplacement() if you need to do it below MyApp(). Just posting it here for future generations.

Related

Flutter 2.0 Navigator w/ Provider Gives Null Value Errors when Popping

I am currently having some data race bugs when using the Flutter 2.0 Navigator API. My store is implemented with MobX and passed down via Provider. After that, I pull an Observer over the global store and then re-update the Navigator (for the routes) every time my global store updates. However, everything works fine until I hit the top back arrow on the WinnerPage. It shows the following error on the screen and flashes back to being fine instantly later:
Unexpected null value.
The relevant error-causing widget was WinnerView
Therefore, to try and debug further, I learned that the value of winner is null for an instant (from onPopPage in Navigator) when pressing the back button, which pops the state. Does anyone know if there is a fix to this problem? Here is all of my code:
Store
class GlobalStore extends Store {
#observable
String? winner;
#action
void setWinner(String? newWinner) => winner = newWinner;
}
App Setup
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "MyApp",
home: Provider<GlobalStore>(
create: (_) => GlobalStore(),
child: Observer(builder: (context) {
final store = Provider.of<GlobalStore>(context);
return Navigator(
pages: [
FooPage(),
if (store.winner != null)
WinnerPage()
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
store.setWinner(null);
return true;
}
);
}),
)
);
}
}
Foo Page (default)
class FooPage extends Page {
const FooPage() : super(key: const ValueKey('Foo Page'));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this, builder: (BuildContext context) => FooView());
}
}
class FooView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final store = Provider.of<GlobalStore>(context);
return Scaffold(
body: TextButton(
child: const Text('Hello, world!'),
onPressed: () {
store.setWinner("You!");
}
)
);
}
}
Winner Page
class WinnerPage extends Page {
const WinnerPage() : super(key: const ValueKey('Winner Page'));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this, builder: (BuildContext context) => WinnerView());
}
}
class WinnerView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final store = Provider.of<GlobalStore>(context);
final winner = store.winner!; // error here!
return Scaffold(
appBar: AppBar(),
body: Column(children: [Text('${winner} is the winner!')]));
}
}
I actually solved this myself, although I cannot find a complete explanation for why the problem was happening so if anyone else has a better answer please write it. Anyways, It looks like for some reason my way of extending Page was not working correctly. Therefore, I changed each of my Navigator.pages to look like this:
[
// note: I am using the stateless widget views not pages
MaterialPage(child: const FooView(), key: const ValueKey("FooPage")),
if (store.winner != null)
MaterialPage(child: const WinnerView(), key: const ValueKey("WinnerPage"))
]
I also have a hunch that the problem was that I was only extending Page, not MaterialPage, which may not have the same behavior. However, I still don't know why it wasn't working the other way after ~1 hr of searching.

Why doesn't Navigator.pushNamed(context, "route"); work in my flutter-cupertino-app code example?

this is my first question and I hope it's readable.
I've created a sample flutter project where I've illustrated the problem specified in my question.
If the code runs on Android the AndroidAppVersion-Object creates a Material App that contains an AndroidHomeScreen-Object with a red Scaffold and a Button with the title "Android". When the user presses the button the AnotherPageView-Object appears that contains an orange Scaffold.
The AnotherPageView-Object is just a sample page.
If the Platform isn't Android an (IOS-)Cupertino-App will be created by the IOSAppVersion-Object that contains an IOSHomeScreen with a green Scaffold with a Button with the title "IOS". Again the user presses the button to create an AnotherPageView-Object.
So here is the Problem:
When I use
Navigator.pushNamed(context, AnotherPageView.anotherPage);
The code doesn't work and it gives me the error I've added down below.
However, it works if I use
Navigator.push(context,
CupertinoPageRoute(builder: (context) => AnotherPageView()));
but I'd like to know why it doesn't work with the first one.
The code:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:io';
void main() {
runApp(Platform.isAndroid ? IOSAppVersion() : AndroidAppVersion());
}
class AndroidAppVersion extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
AnotherPageView.anotherPage: (context) {
return AnotherPageView();
}
},
home: AndroidHomeScreen(),
);
}
}
class AndroidHomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.red,
body: MaterialButton(
onPressed: () {
Navigator.pushNamed(context, AnotherPageView.anotherPage);
},
child: Center(
child: Text('Android'),
),
),
);
}
}
class IOSAppVersion extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoApp(
home: IOSHomeScreen(),
);
}
}
class IOSHomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green,
body: CupertinoButton(
child: Center(child: Text('Hallo IOS')),
onPressed: () {
Navigator.pushNamed(context, AnotherPageView.anotherPage);
},
),
);
}
}
class AnotherPageView extends StatelessWidget {
static String anotherPage = "anotherPage";
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.orange,
);
}
}
Here's the error that gets shown:
The following assertion was thrown while handling a gesture:
Could not find a generator for route RouteSettings("anotherPage", null) in the _WidgetsAppState.
Generators for routes are searched for in the following order:
For the "/" route, the "home" property, if non-null, is used.
Otherwise, the "routes" table is used, if it has an entry for the route.
Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.
...
This
return CupertinoApp(
home: IOSHomeScreen(),
);
doesn't know any routes, you need to provide your routes to both materialapp and cupertinoapp, just do
return CupertinoApp(
routes: {
AnotherPageView.anotherPage: (context) {
return AnotherPageView();
}
},
home: IOSHomeScreen(),
);

Provider login logout flutter

I am trying to create a simple authentication flow using Provider. I have three pages :
LoginPage
OnboardingPage
HomePage
The flow of this app is:
if a user opens the app for the first time, he/she will be redirected to the onboarding then to login to home.
For the second time user, the app first checks the login status and redirected to either log in -> home or straight to home page.
Here is my setup in code :
main.dart
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider<StorageHelper>(create: (_) => StorageHelper()),
ChangeNotifierProvider<AuthProvider>(create: (_) => AuthProvider()),
], child: MyApp()));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (final BuildContext context,
final AuthProvider authProvider, final Widget child) {
print(authProvider.isAuthenticated); // this is false whenever I //click the logout from category(or other pushed pages) but the below ternary //operation is not executing
return MaterialApp(
title: 'My Poor App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Color(0xff29c17e),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: authProvider.isAuthenticated ? HomeScreen() : LoginScreen(),
onGenerateRoute: Router.onGenerateRoute,
);
});
}
}
LoginScreen.dart
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
return Scaffold(
body: Center(
child: MaterialButton(
onPressed: () async {
await authProvider.emailLogin('user#email.com', 'pass');
},
child: Text('Login'))),
);
}
}
HomeScreen.dart
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context, listen: false);
return Scaffold(
body: Center(
child: MaterialButton(
elevation: 2,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CategoryScreen()));
},
child: Text('Reset')),
),
);
}
}
AuthProvider.dart
class AuthProvider extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
set isAuthenticated(bool isAuth) {
_isAuthenticated = isAuth;
notifyListeners();
}
Future emailLogin(String email, String password) async {
isAuthenticated = true;
}
Future logout() async {
isAuthenticated = false;
}
}
If i logout from home page using Provider.of<AuthProvider>(context).logout() it works fine. But if I push or pushReplacement a new route and try to logout from the new route (just say I navigated from home to category page and try to logout from there), I am not redirected to LoginPage. If I print the value of isAuthenticated it prints false but the consumer is not listening or at least not reacting to the variable change.
Please don't mark this question as duplicate, I have searched many other similar questions and none of them worked for my case.
Edit:
CategoryScreen.dart
class CategoryScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
final auth = Provider.of<AuthProvider>(context, listen: false);
auth.logout();
// print(auth.isAuthenticated);
},
child: Text('Category Logout'),
),
),
);
}
}
I guess your problem is that you did not use Consumer for the logout, in your home in the MaterialApp. Just see, that if it works out for you
main.dart
// needs to listen to the changes, to make changes
home: Consumer<AuthProvider>(
builder: (context, authProvider, child){
return authProvider.isAuthenticated ? HomeScreen() : LoginScreen();
}
)
Since, Consumer was not there for your home, even if the value was being changed, it was not able to work on updating the view for you as per the Provider.

Persist Provider data across multiple pages not working

I'm using Provider in my flutter app, and when I go to a new page, the data provided to the Provider at page 1 is not accessible in page 2.
The way I understood the way Provider works, was that there is a central place where one stores all the data, and one can access that data anywhere in the application. So in my application, which is shown below, ToDoListManager is the place where all the data is stored. And if I set the data in Page 1, then I will be able to access that data in Page 2, and vice versa.
If this is not correct, then what part is wrong? And why isn't it working in my application?
Here's the code
Page 1
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => ToDoListManager(),
child: Scaffold(
appBar: AppBar(
title: Text('Cool Project'),
),
body:e ToDoList(),
),
);
}
}
class ToDoList extends StatelessWidget {
#override
Widget build(BuildContext context) {
final toDoListManager = Provider.of<ToDoListManager>(context);
return ListView.builder(
itemCount: toDoListManager.toDoList.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => Details(index)));
},
child: Text(toDoListManager.toDoList[index]),
);
},
);
}
}
Page 2
class Details extends StatelessWidget {
final int index;
Details(this.index);
#override
build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => ToDoListManager(),
child: Scaffold(
appBar: AppBar(
title: Text('Details Bro'),
),
body: AppBody(index)),
);
}
}
class AppBody extends StatelessWidget {
final int index;
AppBody(this.index);
#override
Widget build(BuildContext context) {
final toDoListManager = Provider.of<ToDoListManager>(context);
print(toDoListManager.toDoList);
return Text(toDoListManager.toDoList[1]);
}
}
ToDoListProvider
class ToDoListManager with ChangeNotifier {
List<String> _toDoList = ['yo', 'bro'];
List<String> get toDoList => _toDoList;
set toDoList(List<String> newToDoList) {
_toDoList = newToDoList;
notifyListeners();
}
}
You have 2 options:
Place your ChangeNotifierProvider above your MaterialApp so that is accesible from any of you Navigator routes.
Keep your Home widget as is but when pushing the new widget with the Navigator provide the original Manager.
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return Provider<ToDoListManager>.value(
value: toDoListManager,
child: Details(index),
);
},
),
);
},
With both approaches you don't need to create a new ChangeNotifierProvider in your details screen.

How to save last opened screen in flutter app

I am trying to reopen last opened screen after boot, Is there any simple way to do so ? sample codes are welcome !
So far I tried a code(which I got somewhere) with SharedPreferences, but it's not working.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
String lastRouteKey = 'last_route';
void main() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
String lastRoute = preferences.getString(lastRouteKey);
runApp(MyApp(lastRoute));
}
class MyApp extends StatelessWidget {
final String lastRoute;
MyApp(this.lastRoute);
#override
Widget build(BuildContext context) {
bool hasLastRoute = getWidgetByRouteName(lastRoute) != null;
return MaterialApp(
home: Foo(),
initialRoute: hasLastRoute ? lastRoute : '/',
onGenerateRoute: (RouteSettings route) {
persistLastRoute(route.name);
return MaterialPageRoute(
builder: (context) => getWidgetByRouteName(route.name),
);
},
);
}
Widget getWidgetByRouteName(String routeName) {
switch (routeName) {
case '/':
return MainWidget();
case '/':
return SecondRoute();
// Put all your routes here.
default:
return null;
}
}
void persistLastRoute(String routeName) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.setString(lastRouteKey, routeName);
}
}
class Foo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Foo'),
),
body: Column(
children: <Widget>[
RaisedButton(
child: Text('Open route second'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
RaisedButton(
child: Text('Open route main'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MainWidget()),
);
},
),
],
),
);
}
}
class SecondRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
class MainWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("MainWidget"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
should I use SQLite or JSON instead of SharedPreferences to make the code simple? thanks.
Demo
A. Navigation
when we are navigating through different screens within app, actually, the route stacks are changing.
So, firstly, we need to figure out how to listen to this changes e.g Push screen, Pop back to users screen.
1. Attaching saving method in each action button
we can actually put this on every navigation-related button.
a. on drawer items
ListTile(
title: Text("Beta"),
onTap: () {
saveLastScreen(); // saving to SharedPref here
Navigator.of(context).pushNamed('/beta'); // then push
},
),
b. on Titlebar back buttons
appBar: AppBar(
title: Text("Screen"),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
saveLastScreen(); // saving to SharedPref here
Navigator.pop(context); // then pop
},
),
),
c. and also capturing event of Phone Back button on Android devices
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: (){ // will triggered as we click back button
saveLastScreen(); // saving to SharedPref here
return Future.value(true);
},
child: Scaffold(
appBar: AppBar(
title: Text("Base Screen"),
),
Therefore, we will have more code and it will be harder to manage.
2. Listening on Route Changes using Route observer
Nonetheless, Flutter provides on MaterialApp, that we can have some "middleware" to capture those changes on route stacks.
We may have this on our MyApp widget :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Save Last Route',
navigatorObservers: <NavigatorObserver>[
MyRouteObserver(), // this will listen all changes
],
routes: {
'/': (context) {
return BaseScreen();
},
'/alpha': (context) {
return ScreenAlpha();
},
We can define MyRouteObserver class as below :
class MyRouteObserver extends RouteObserver {
void saveLastRoute(Route lastRoute) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('last_route', lastRoute.settings.name);
}
#override
void didPop(Route route, Route previousRoute) {
saveLastRoute(previousRoute); // note : take route name in stacks below
super.didPop(route, previousRoute);
}
#override
void didPush(Route route, Route previousRoute) {
saveLastRoute(route); // note : take new route name that just pushed
super.didPush(route, previousRoute);
}
#override
void didRemove(Route route, Route previousRoute) {
saveLastRoute(route);
super.didRemove(route, previousRoute);
}
#override
void didReplace({Route newRoute, Route oldRoute}) {
saveLastRoute(newRoute);
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
}
}
B. How to Start the App
As users interacting through the screens, the Shared Preferences will always store last route name. To make the app navigate correspondingly, we need to make our BaseScreen statefull and override its initState method as below :
return MaterialApp(
routes: {
'/': (context) {
return BaseScreen(); // define it as Main Route
},
class BaseScreen extends StatefulWidget {
#override
_BaseScreenState createState() => _BaseScreenState();
}
class _BaseScreenState extends State<BaseScreen> {
#override
void initState() {
super.initState();
navigateToLastPage();
}
void navigateToLastPage() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
String lastRoute = prefs.getString('last_route');
// No need to push to another screen, if the last route was root
if (lastRoute.isNotEmpty && lastRoute != '/') {
Navigator.of(context).pushNamed(lastRoute);
}
}
C. Working Repo
You may look at this repository that overrides RouteObserver as explained in second option above
Saving and Opening Screen Beta and Screen Delta in different starts
D. Shared Preferences / JSON / SQLite
I suggest to use Shared preferences for simplicity. As we only record simple String for route name, we can only write two lines of code to Save and two lines of code to Load.
If we use JSON file, we need to manually set Path for it using path_provider package.
Moreover, if we use SQLite, we need to setup DB (may consist > 8 more lines), and setup table and also inserting table method.