What's the correct way of setting up navigation architecture named routes while using a bottomNavigationBar?
Here's my current setup but I feel there's a better way of doing it:
main.dart:
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case NamedRoutes.splashScreen:
return SplashScreen();
case NamedRoutes.login:
return LoginPage();
case NamedRoutes.mainApp:
return NavigatorSetup();
default:
throw Exception('Invalid route: ${settings.name}');
}
});
navigatorSetup.dart:
IndexedStack(
index: Provider.of<RoutesProvider>(context).selectedViewIndex,
children: [FirstMain(), SecondMain(), ThirdMain(), FourthMain()],
), bottomNavigationBar...
in each main files there is the following setup
class FirstMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Navigator(
key: Provider.of<RoutesProvider>(context).homeKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
case NamedRoutes.mainPage:
return MainPage();
case NamedRoutes.singleMainPage:
return SingleMainPage();
default:
throw Exception('Invalid route: ${settings.name}');
}
},
);
},
);
}
}
Then my routes provider looks like this:
class RoutesProvider extends ChangeNotifier {
int _selectedViewIndex = 0;
get selectedViewIndex => _selectedViewIndex;
set selectedViewIndex(int newIndex) {
_selectedViewIndex = newIndex;
notifyListeners();
}
GlobalKey _mainKey = GlobalKey<NavigatorState>();
GlobalKey _homeKey = GlobalKey();
GlobalKey _secondKey = GlobalKey();
GlobalKey _thirdKey = GlobalKey();
GlobalKey _fourthKey = GlobalKey();
get mainKey => _mainKey;
get homeKey => _homeKey;
get secondKey => _secondKey;
get thirdKey => _thirdKey;
get fourthKey => _fourthKey;
}
The way I'm currently changing routes when on another page of the indexedStack
final RoutesProvider routesProvider = Provider.of<RoutesProvider>(context, listen: false);
final GlobalKey thirdKey = routesProvider.thirdKey;
routesProvider.selectedViewIndex = 2;
Navigator.pushReplacementNamed(thirdKey.currentContext, NamedRoutes.third);
The better way to navigate
Creating a route_generator
import 'package:flutter/material.dart';
import 'package:routing_prep/main.dart';
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
// Getting arguments passed in while calling Navigator.pushNamed
final args = settings.arguments;
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => FirstPage());
case SecondPage.routeName:
// Validation of correct data type
if (args is String) {
return MaterialPageRoute(
builder: (_) => SecondPage(
data: args,
),
);
}
// If args is not of the correct type, return an error page.
// You can also throw an exception while in development.
return _errorRoute();
default:
// If there is no such named route in the switch statement, e.g. /third
return _errorRoute();
}
}
static Route<dynamic> _errorRoute() {
return MaterialPageRoute(builder: (_) {
return Scaffold(
appBar: AppBar(
title: Text('Error'),
),
body: Center(
child: Text('ERROR'),
),
);
});
}
}
As you can see, you've moved from having bits of routing logic everywhere around your codebase, to a single place for this logic - in the RouteGenerator. Now, the only navigation code which will remain in your widgets will be the one pushing named routes with a navigator.
Before you can run and test the app, there's still a bit of a setup to do for this RouteGenerator to function.
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
...
// Initially display FirstPage
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
);
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
...
RaisedButton(
child: Text('Go to second'),
onPressed: () {
// Pushing a named route
Navigator.of(context).pushNamed(
SecondPage.routeName,
arguments: 'Hello there from the first page!',
);
},
)
...
}
}
class SecondPage extends StatelessWidget {
static const routeName = "/second";
// This is a String for the sake of an example.
// You can use any type you want.
final String data;
SecondPage({
Key key,
#required this.data,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Routing App'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Second Page',
style: TextStyle(fontSize: 50),
),
Text(
data,
style: TextStyle(fontSize: 20),
),
],
),
),
);
}
}
Related
Although questions with such error messages exist in this site, none solves my problem.
I have a button and on clicking the button, I just need to go to a different screen. But when ever I tap on the screen, the error shows up.
I first setup a route in MaterialApp and then tried to navigate to that route on tapping the button. The full code and the error message are given below:
Code:
import 'livesession1to1.dart';
class NavigationService {
static GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp(
home: CountDownTimer(),
navigatorKey: NavigationService.navigatorKey, // set property// Added by me later from prev project
// initialRoute: "/",
routes: <String, WidgetBuilder> {
'/liveSession1to1': (context) =>LiveSession1to1(),
},
)
);
}// end of main
class CountDownTimer extends StatefulWidget {
const CountDownTimer();
final String? title='';
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer> {
#override
void initState() {
super.initState();
}// end of initstate
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Live Session'),
),
body: Text('Demo Text'),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_button(title: "Go", onPressed: () =>
Navigator.of(context ,rootNavigator: true).pushNamed('/liveSession1to1', arguments: {'room_found': 123 } )
),
],
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
Widget _button({required String title, VoidCallback? onPressed}) {
return Expanded(
child: TextButton(
child: Text(
title,
style: const TextStyle(color: Colors.white),
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: onPressed,
));
}
}
Error found:
The following assertion was thrown while handling a gesture:
Could not find a generator for route RouteSettings("/liveSession1to1", {room_found: 123}) in the _WidgetsAppState.
Make sure your root app widget has provided a way to generate
this route.
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.
So how to solve the problem ?
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
void main() {
locatorSetup();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
onGenerateRoute: generateRoute,
navigatorKey: _navService.navigatorKey,
// I don't know what your first screen is, so I'm assuming it's a Splash Screen
home: SplashScreen());
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navService.pushNamed(Routes.LiveSession1to1);
},
child: Text("Go to next page"),
),
));
}
}
class LiveSession1to1 extends StatefulWidget {
const LiveSession1to1({Key? key}) : super(key: key);
#override
State<LiveSession1to1> createState() => _LiveSession1to1State();
}
class _LiveSession1to1State extends State<LiveSession1to1> {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navService.goBack();
},
child: Text("Go to previous page"),
),
));
}
}
GetIt locator = GetIt.instance;
void locatorSetup() {
locator
.registerLazySingleton<NavigationHandler>(() => NavigationHandlerImpl());
}
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case Routes.LiveSession1to1:
return _getPageRoute(view: LiveSession1to1(), routeName: settings.name);
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}'),
),
),
);
}
}
PageRoute _getPageRoute({String? routeName, Widget? view}) {
return MaterialPageRoute(
settings: RouteSettings(
name: routeName,
),
builder: (_) => view!,
);
}
class Routes {
static const String LiveSession1to1 = "liveSession1to1";
}
abstract class NavigationHandler {
///Pushes `destinationRoute` route onto the stack
Future<dynamic>? pushNamed(String destinationRoute, {dynamic arg});
///Pushes `destinationRoute` onto stack and removes stack items until
///`lastRoute` is hit
Future<dynamic>? pushNamedAndRemoveUntil(
String destinationRoute, String lastRoute,
{dynamic arg});
///Pushes `destinationRoute` onto stack with replacement
Future<dynamic>? pushReplacementNamed(String destinationRoute, {dynamic arg});
///Pushes `destinationRoute` after popping current route off stack
Future<dynamic>? popAndPushNamed(String destinationRoute, {dynamic arg});
///Pops current route off stack
void goBack();
///Pops routes on stack until `destinationRoute` is hit
void popUntil(String destinationRoute);
///Exits app
void exitApp();
late GlobalKey<NavigatorState> navigatorKey;
}
/// Handles navigation
class NavigationHandlerImpl implements NavigationHandler {
#override
late GlobalKey<NavigatorState> navigatorKey;
/// Constructs a NavigationHandler instance
NavigationHandlerImpl({GlobalKey<NavigatorState>? navigatorKey}) {
this.navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>();
}
NavigatorState? get state => navigatorKey.currentState;
#override
void exitApp() {
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
}
#override
void goBack() {
if (state != null) {
return state!.pop();
}
}
#override
Future? popAndPushNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.popAndPushNamed(destinationRoute, arguments: arg);
}
}
#override
void popUntil(String destinationRoute) {
if (state != null) {
return state!.popUntil(ModalRoute.withName(destinationRoute));
}
}
#override
Future? pushNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.pushNamed(destinationRoute, arguments: arg);
}
}
#override
Future? pushNamedAndRemoveUntil(String destinationRoute, String lastRoute,
{arg}) {
if (state != null) {
return state!.pushNamedAndRemoveUntil(
destinationRoute,
ModalRoute.withName(lastRoute),
arguments: arg,
);
}
}
#override
Future? pushReplacementNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.pushReplacementNamed(destinationRoute, arguments: arg);
}
}
}
I'm quite new to Flutter and I think I haven't understand all the logic behind the state management with Providers.
I've the following widget:
class App extends StatelessWidget {
List<IconButton> navigationActions(BuildContext context) {
return
Consumer<ApplicationState>(builder: (context, appState, _) {
if (appState.loginState == 'loggedIn') {
return [IconButton(
icon: const Icon(Icons.logout),
tooltip: 'Logout',
onPressed: () {
context.read<ApplicationState>().signOut();
},
)];
}
})
;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FOO'),
actions: navigationActions(context)
),
body: ListView(
.........
)
)
}
And I want to show/hide the AppBar action according to the flag loginState set inside ApplicationState
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: (context, _) => App(),
),
);
}
class ApplicationState extends ChangeNotifier {
ApplicationState() {
init();
}
String _loginState = 'loggedOut';
String get loginState => _loginState;
}
I'm not sure about how to implement the function navigationActions.
Which should be the return type? Since I'm not returning a data in the else branch I'm not sure about how to manage that type.
Maybe there are smarter solution, I don't know yet.. Someone has ever implemented a similar logic with Providers?
navigationActions has to return List<IconButton>, but you are returning the result of Consumer which is a Widget. You can use other methods to get the ApplicationState. Here is example code which does what you want:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ApplicationState extends ChangeNotifier {
String _loginState = 'loggedIn';
set loginState(String state) {
_loginState = state;
}
get loginState => _loginState;
void toggleState() {
if (loginState == 'loggedIn')
loginState = 'loggedOut';
else
loginState = 'loggedIn';
notifyListeners();
}
void signOut() {}
}
class ActionTest extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => ApplicationState(),
child: ActionApp(),
);
}
}
class ActionApp extends StatelessWidget {
List<IconButton> navigationActions(BuildContext context) {
final appState = Provider.of<ApplicationState>(context);
if (appState.loginState == 'loggedIn') {
return [
IconButton(
icon: const Icon(Icons.logout),
tooltip: 'Logout',
onPressed: () {
appState.signOut();
},
)
];
} else {
return [];
}
}
#override
Widget build(BuildContext context) {
final appState = Provider.of<ApplicationState>(context);
return Scaffold(
appBar: AppBar(title: Text('FOO'), actions: navigationActions(context)),
body: Container(),
floatingActionButton: FloatingActionButton(
onPressed: () {
appState.toggleState();
},
child: Icon(appState.loginState == 'loggedIn'
? Icons.toggle_off
: Icons.toggle_on),
),
);
}
}
I have a widget which is getting data from one of my other classes(booruHandler), I am using the search function for the future of a future builder but I need it to be called multiple times but only once the future builder has completed, I am using a similar future builder in another widget and getting it to call again using setState to increment widget.pageNum when an action happens. Is there a way to make it so the setState gets called when the future builder has finished building? I tried putting setState inside the futurebuilder and making it call if the connectionState is done but that doesn't work
class SnatcherProgressPage extends StatefulWidget {
String tags,amount,timeout;
int pageNum=0;
int count=0;
SnatcherProgressPage(this.tags,this.amount,this.timeout);
#override
_SnatcherProgressPageState createState() => _SnatcherProgressPageState();
}
class _SnatcherProgressPageState extends State<SnatcherProgressPage> {
static int limit, count;
#override
void initState() {
// TODO: implement initState
super.initState();
if (int.parse(widget.amount) <= 100){limit = int.parse(widget.amount);} else {limit = 100;}
}
BooruHandler booruHandler = new GelbooruHandler("https://gelbooru.com", limit);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Snatching"),
),
body: FutureBuilder(
future: booruHandler.Search(widget.tags,widget.pageNum),
builder: (context, AsyncSnapshot snapshot){
switch(snapshot.connectionState){
case ConnectionState.active:
return Text("Snatching");
break;
case ConnectionState.done:
if (snapshot.data.length < int.parse(widget.amount)){
// Inc pagenum to get more data
// Call the writer function on all of the data
}
return Text(snapshot.data.length.toString());
break;
case ConnectionState.waiting:
return CircularProgressIndicator();
break;
case ConnectionState.none:
return Text("hmmmmmm");
break;
}
return Text("hmmmmmm");
},
),
);
}
}
very nice example could be like this:
final navigatorKey = GlobalKey<NavigatorState>();
void main() => runApp(
MaterialApp(
home: HomePage(),
navigatorKey: navigatorKey, // Setting a global key for navigator
),
);
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Center(
child: Text('test')
)
),
floatingActionButton: FloatingActionButton(
onPressed: showMyDialog, // Calling the function without providing it BuildContext
),
);
}
}
void showMyDialog() {
showDialog(
context: navigatorKey.currentContext,
builder: (context) => Center(
child: Material(
color: Colors.transparent,
child: Text('Hello'),
),
)
);
}
showMyDialog() could be used everywhere including static Utility classes, db connection classes etc...
Call back pageNum when updating
class SnatcherProgressPage extends StatefulWidget {
final NumberCallBack onPageNumUpdated;
}
// ...
setState((){
onPageNumUpdated(/* the number */);
});
// ...
typedef NumberCallBack = void Function(int number);
Or use state management
I am below code which given in flutter documentation for page routing
// Within the `FirstRoute` widget
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
}
But it provides some animation while pushing and poping route.
For Android, the entrance transition for the page slides the page
upwards and fades it in. The exit transition is the same, but in
reverse.
The transition is adaptive to the platform and on iOS, the page slides
in from the right and exits in reverse. The page also shifts to the
left in parallax when another page enters to cover it. (These
directions are flipped in environments with a right-to-left reading
direction.)
Is there any way to route to next page without any animation?
Edit:
Please check the entire code:
class MyApp extends StatelessWidget {
final routes = <String, WidgetBuilder>{
SecondRoute.tag: (context) => SecondRoute(),
};
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter Routes",
home: new FirstRoute(),
routes: routes,
onGenerateRoute: (routeSettings) {
if (routeSettings.name == SecondRoute.tag)
return PageRouteBuilder(pageBuilder: (_, a1, a2) => SecondRoute());
return null;
},
);
}
}
class FirstRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: Center(
child: RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.of(context).pushNamed(SecondRoute.tag);
},
),
),
);
}
}
class SecondRoute extends StatelessWidget {
static String tag = 'second-route';
#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!'),
),
),
);
}
}
For Navigator.push(...)
Navigator.push(
context,
PageRouteBuilder(pageBuilder: (_, __, ___) => SecondRoute()),
)
For Navigator.pushNamed(...)
First, add this to your MaterialApp
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == '/second')
return PageRouteBuilder(pageBuilder: (_, __, ___) => SecondRoute());
return null;
},
)
And now, you can use:
Navigator.pushNamed(context, '/second');
The animation is performed by MaterialPageRoute. If you don't want it, simple use something else:
Navigator.push(
context,
PageRouteBuilder(pageBuilder: (_, __, ___) => MyRoute()),
)
Replace your MyApp with this.
class MyApp extends StatelessWidget {
final routes = <String, WidgetBuilder>{SecondRoute.tag: (context) => SecondRoute()};
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter Routes",
home: new FirstRoute(),
onGenerateRoute: (routeSettings) {
if (routeSettings.name == SecondRoute.tag)
return PageRouteBuilder(
pageBuilder: (_, a1, a2) => FadeTransition(opacity: a1 ,child: SecondRoute()),
transitionDuration: Duration(seconds: 5),
);
return null;
},
);
}
}
As Flutter is now migrating to Navigator 2.0 for increased support, I would recommend checking out their migration guide on adding a TransitionDelegate to the Navigator. Add an instance of this class to your navigator to achieve the intended result:
import 'package:flutter/widgets.dart';
class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
#override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
}) {
final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
// Renames isEntering to isWaitingForEnteringDecision.
if (pageRoute.isWaitingForEnteringDecision) {
pageRoute.markForAdd();
}
results.add(pageRoute);
}
for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) {
// Checks the isWaitingForExitingDecision before calling the markFor methods.
if (exitingPageRoute.isWaitingForExitingDecision) {
exitingPageRoute.markForRemove();
final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
if (pagelessRoutes != null) {
for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
pagelessRoute.markForRemove();
}
}
}
results.add(exitingPageRoute);
}
return results;
}
}
aidan marshal's solution is simple and works fine but there some adjustments in his code
import 'package:flutter/widgets.dart';
class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
#override
Iterable<RouteTransitionRecord> resolve({
required List<RouteTransitionRecord> newPageRouteHistory,
required Map<RouteTransitionRecord?, RouteTransitionRecord>
locationToExitingPageRoute, required Map<RouteTransitionRecord?,
List<RouteTransitionRecord>> pageRouteToPagelessRoutes}) {
{
final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
// Renames isEntering to isWaitingForEnteringDecision.
if (pageRoute.isWaitingForEnteringDecision) {
pageRoute.markForAdd();
}
results.add(pageRoute);
}
for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) {
// Checks the isWaitingForExitingDecision before calling the markFor methods.
if (exitingPageRoute.isWaitingForExitingDecision) {
exitingPageRoute.markForRemove();
final List<RouteTransitionRecord>? pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
if (pagelessRoutes != null) {
for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
pagelessRoute.markForRemove();
}
}
}
results.add(exitingPageRoute);
}
return results;
}
}
}
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.