I'm trying to display a few welcome screens on the first launch of my App. After which it will go to the login screen. But for efficiency, I need it to pop all the welcome screens off the 'stack' before launching the login screen, which is the reason behind this request.
My main.dart program loads into preloader2.dart which looks like this:
class PreLoad2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
String myRoute;
return Scaffold(
body: SafeArea(
child: FlatButton(
child: Text('Press me!'),
onPressed: () {
if (loginCount == 0) { // globally defined variable
myRoute = '/welcome1';
} else {
myRoute = '/login';
}
++loginCount;
Navigator.pushNamed(context, myRoute);
},
),
),
);
}
}
It works perfectly well. But I have to click on the button.
I've tried both a stateless widget and a stateful one in preload2.
Can I write some code to replace the button so it just runs with no user input?
Just place the onPressed body into the build function before returning the Scaffold
#override
Widget build(BuildContext context) {
String myRoute;
if (loginCount == 0) // globally defined variable
myRoute = '/welcome1';
else
myRoute = '/login';
++loginCount;
Navigator.pushNamed(context, myRoute);
return Scaffold(
body: SafeArea(
child: FlatButton(
child: Text('Press me!'),
onPressed: () {
// Nothing Here
},
),
),
);
}
}
I have managed to get around, rather than answer my own question.
Inside my main.dart program I have added a new Welcome Widget. What this does is to set up a new Navigator that in turn calls each of my welcome screens. Each one in turn is pushed and then popped. So it is not quite what I was after but it definitely does the trick and appears to be quite efficient. My Welcome widget looks like this:
class Welcome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Navigator(
initialRoute: 'welcome1',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case 'welcome1':
builder = (BuildContext _) => Welcome1(
onWelcomeComplete: () {
Navigator.of(context).pop();
},
);
break;
case 'welcome2':
builder = (BuildContext _) => Welcome2(
onWelcomeComplete: () {
Navigator.of(context).pop();
},
);
break;
case 'welcome3':
builder = (BuildContext _) => Welcome3(
onWelcomeComplete: () {
Navigator.of(context).pop();
},
);
break;
case 'welcome4':
// Assume ChooseCredentialsPage collects new credentials and then
// invokes 'onSignupComplete()'.
builder = (BuildContext _) => Welcome4(
onWelcomeComplete: () {
Navigator.of(context).pop();
},
);
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return MaterialPageRoute(builder: builder, settings: settings);
},
);
}
}
Each of Welcome2(), Welcome3() and Welcome4() are held in their own files and pass back a link to the next widget, either going forward or backwards through the chain. The button code looks like this:
onPressed: () {
Navigator.of(context).pushReplacementNamed(routeName);
},
Where the route name is 'welcome2', for example, which is then linked to by the switch statement that creates a new route to the required widget.
Once on the last welcome screen (or before if required) the 'finish' button calls the onWelcomeComplete function which simply pops the complete Navigator stack leaving the code to follow the default route ('/') which I set up in the MyApp widget taking me to my Login() widget in login.dart. The code is:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/welcome',
routes: {
'/': (BuildContext context) => Login(),
'/welcome': (BuildContext context) => Welcome(),
...
I hope this answer clearly explains what I have achieved and helps someone else.
Further information about the Flutter Navigator can be found here:
Navigator Class
Related
I am building an app where I am trying to retrieve data from calling navigator.push
onTap: () async {
widget.dogData = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddDogScreen(),
),
);
widget.setDocId(widget.dogData['docId']);
setState(() {
widget.dogData;
});
},
However, I want to link to yet another screen after AddDogScreen and pass the data by popping from that screen.
SO we would have :
Initial screen -> AddDogScreen -> Another screen (then pop back to initial screen with the data)
Then the third screen should pop back to the initial screen with the data. How can I set this up, since I want to get the data from two screens?
Thanks a lot in advance.
To do what you want to do there are two useful features in Flutter navigation:
final value = await Navigator.push() returns the value that the pushed screen returned, and you can await it to wait for the returning of the next screen.
Navigator.pop(value) allows you to return a value from a screen, while going back.
All you have to do here is play with those to do what you want.
Here is an example of the type of flow that you want:
class InitialScreen extends StatelessWidget {
onPressed(BuildContext context) async {
final List<String>? addDogScreenResult = await Navigator.of(context).push<
List<String>>(
MaterialPageRoute<List<String>>(builder: (context) => AddDogScreen()));
// Do something with the result
print("We got the values: ${addDogScreenResult}");
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => onPressed(context),
child: const Text("Click to go to AddDogScreen"))));
}
}
class AddDogScreen extends StatelessWidget {
onPressed(BuildContext context) async {
final String? anotherScreenResult = await Navigator.of(context)
.push<String>(MaterialPageRoute(builder: (context) => AnotherScreen()));
if (anotherScreenResult != null) {
const addDogScreenString = "Some value AddDogScreen generates";
// We can combine a value that we got in this screen and the return value of AnotherScreen.
final List<String> addDogScreenReturnValue = [
addDogScreenString,
anotherScreenResult
];
// We simply return the list using .pop() function
// Notice that we can type the .pop() function with the return type that we give it. It is not absolutely necessary, but it adds in clarity and safety.
Navigator.of(context).pop<List<String>>(addDogScreenReturnValue);
return;
}
Navigator.of(context).pop();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => onPressed(context),
child: const Text("Click to go to AnotherScreen"))));
}
}
class AnotherScreen extends StatelessWidget {
onPressed(BuildContext context) async {
const String anotherScreenResult = "Some value";
Navigator.of(context).pop<String>(anotherScreenResult);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => onPressed(context),
child: const Text("Click to go back to initial page"))));
}
}
Also, I see in your code widget.dogData = .... Maybe it's on purpose but if not, this is not how you should manipulate state.
State variables should be defined in the class that extends your StatefulWidget, and you should be able to read it using dogData, and update it using:
setState({
dogData = 'Something';
})
I can't access a provider defined above a Scaffold from showModalBottomSheet in the FloatingActionButton.
I've defined a HomePage like so:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(), // here is the problem
);
}
)
}
}
And this is MyFAB:
class MyFAB extends StatefulWidget {
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: () => show(),
);
}
void show() {
showModalBottomSheet(
...
context: context,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: Provider.of<MyProvider>(context, listen: false).doSomething(); //Can't do this
Navigator.pop(context);
)
],
);
}
);
}
}
Error: Could not find the correct Provider<MyProvider above this BottomSheet Widget.
Fixed by placing the provider above MaterialApp, as described here.
Bottom sheets are created at the root of the material app. If a prodiver is declared below the material app, a bottom sheet cannot access it because the provider is not an ancestor of the bottom sheet in the widget tree.
The screenshot below shows a widget tree: the whole app is inside Wrapper and the bottom sheet is not created inside Wrapper. It is created as another child of MaterialApp (with a root element Container in this case).
For your case:
// main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return MaterialApp(
home: HomePage(),
);
},
);
}
}
// home_page.dart
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: MyFAB()
);
}
}
This is caused by passing it the wrong context. Wrap your FAB to a Builder widget and pass it as builder property. This will take a new context and pass it to showModalBottomSheet. Also, you can do onPressed: show, it's more concise.
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(context), // here is the problem
);
}
)
}
}
class MyFAB extends StatefulWidget {
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: (context) => show(context),
);
}
void show(ctx) {
showModalBottomSheet(
...
context: ctx,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: () {
Provider.of<MyProvider>(ctx, listen: false).doSomething(); //Can't do this
Navigator.pop(ctx)
};
)
],
);
}
);
}
}
SOLUTION
HomePage:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(),
builder: (context, _) {
return Scaffold(
body: Consumer<MyProvider>(
builder: (context, provider, _) {
return Text(provider.mytext); // this works fine
}
),
floatingActionButton: MyFAB(context), // here is the problem
);
}
)
}
}
MyFAB:
class MyFAB extends StatefulWidget {
final BuildContext ctx;
MyFAB(this.ctx)
#override
_MyFABState createState() => _MyFABState();
}
class _MyFABState extends State<MyFAB> {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
...
onPressed: () => show(),
);
}
void show() {
showModalBottomSheet(
...
context: context,
builder: (BuildContext context) {
return Wrap(
children: [
...
FlatButton(
onPressed: Provider.of<MyProvider>(widget.ctx, listen: false).doSomething(); //Can't do this
Navigator.pop(context);
)
],
);
}
);
}
}
In my opinion: showModalBottomSheet builds a bottom sheet with context which comes from Material App
1st image
so when we return any Widget to show in the Bottom sheet it uses that Material app context as we can see in the builder property in the:1st image.
2ng Image: your code
so in your code, when you are writing: Provider.of(context, listen: false).doSomething(); it is using context from the builder: (BuildContext context) which is the context of Material App. we have to change this context in order to use this Provider without having to uplift the position of our Provider above the Material App.
Now if we want to keep using that context to get the benefits of that overlay and automatic detection of suitable themes and still want to use the context of a widget that does have access to our provider:
we can pass the context of the Widget which does have Provider access to the FAB, but we will have to keep passing that context through widgets till we need to use that Provider in our FAB or till we go to a different route: in which case we can start from a new context and provider as Providers are scoped in mature.
so in your HomePage either you can wrap your scaffold inside a Builder or you can create a new widget like this:"
3rd image
so that it will have its own context which does have access to the provider we need inside our FAB as shown below in 4th image:
4th image
and then in the builder property of showModalBottomSheet change the name of the parameter in an anonymous function so that it won't be confused with the MAterial App context and context we will be passing in (Builder context or IdeaScreen context in my case image 4th)
5th image
I am creating a new widget but you do not have need to do so you can directly write your Fab code inside the anonymous function:
and can use context(not newContext which is related to Material App context) while calling the Provider as you are already doing.
But I will show in my case What I am doing in my AddTask Widget in case anyone's use case is similar to mine:
6th image
expect a context, which does have a provider access, I my case its context of IdeaScreen.
and then use it just like this:
7th image
I am new to flutter and i cannot navigate to new page from bottom navigation bar
I have main app
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
));
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
builder: (BuildContext buildContext, Widget widtget) => Scaffold(
body: RootNavigator(),
bottomNavigationBar: BottomNavigation(),
),
);
}
}
and Rootnavigator
class RootNavigator extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Navigator(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
// final args = settings.arguments;
return MaterialPageRoute(
settings: settings,
builder: (BuildContext context) {
switch (settings.name) {
case '/':
return Page1();
case '/page2':
return Page2();
case '/page3':
return Page3();
default:
return RouteErrorPage();
}
});
},
);
}
}
And bottom navigator
class BottomNavigation extends StatefulWidget {
#override
BottomNavigationState createState() {
return new BottomNavigationState();
}
}
class BottomNavigationState extends State<BottomNavigation> {
int currIndex = 0;
onTap(int index) {
setState(() => currIndex = index);
switch (index) {
case 0:
Navigator.pushNamed(context, '/');
break;
case 1:
Navigator.pushNamed(context, '/page2');
break;
case 2:
Navigator.pushNamed(context, 'page3');
break;
default:
Navigator.push(
context, MaterialPageRoute(builder: (_) => RouteErrorPage()));
}
}
....
// added app bar items
}
Tabs are switching but routes are not. It stays on home page.
I feel like there is something with context but do not know how to solve it.
Can anybody help?
Thanks
p.s. if i move bottom navigation bar to each page separatly everything work sexcept selected tab (because of state) and also i want to keep one, shared app bar
Answer is - #LoVe comment was correct.
Thats how flutter works.
if you have bottom navigation you have to swipe through pages.
Redirection means moving to completely new page and there you have to define Scaffold from sratch.
If you want to have shared AppBar - make it reusable widget
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.
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.