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';
})
Related
I am using FutureBuilder in one of my widgets and it requires a future. I pass the future to the widget through its constructor. The problem is that while passing the future to the widget it gets automatically executed. Since the FutureBuilder accepts only a Future and not a Future Function() i am forced to initialize a variable which in turn calls the async function. But i don't know how to pass the Future without it getting executed.
Here is the complete working example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final icecreamSource = DataService.getIcecream();
final pizzaSource = DataService.getPizza();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
children: [
MenuButton(label: 'Ice Cream', dataSource: icecreamSource),
MenuButton(label: 'Pizza', dataSource: pizzaSource),
]
),
),
),
);
}
}
class MenuButton extends StatelessWidget {
final String label;
final Future<String> dataSource;
const MenuButton({required this.label, required this.dataSource});
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
child: Text(label),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => AnotherPage(label: label, dataSource: dataSource)))
),
);
}
}
// Mock service to simulate async data sources
class DataService {
static Future<String> getIcecream() async {
print('Trying to get ice cream...');
return await Future.delayed(const Duration(seconds: 3), () => 'You got Ice Cream!');
}
static Future<String> getPizza() async {
print('Trying to get pizza...');
return await Future.delayed(const Duration(seconds: 2), () => 'Yay! You got Pizza!');
}
}
class AnotherPage extends StatefulWidget {
final String label;
final Future<String> dataSource;
const AnotherPage({required this.label, required this.dataSource});
#override
State<AnotherPage> createState() => _AnotherPageState();
}
class _AnotherPageState extends State<AnotherPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.label)),
body: Center(
child: FutureBuilder<String>(
future: widget.dataSource,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(snapshot.hasData) {
return Text('${snapshot.data}');
} else if(snapshot.hasError) {
return Text('Error occurred ${snapshot.error}');
} else {
return Text('Fetching ${widget.label}, please wait...');
}
}
),
),
);
}
}
The intended behaviour is that when i press the "Ice Cream" or "Pizza" button on the main page, the widget/screen named "Another Page" should appear and the async request should get executed during which the loading message should be displayed. However, what is happening is that on loading the homepage, even before pressing any of the buttons, both the async requests are getting executed. On pressing any of the buttons, the loading message does not appear as the request is already completed so it directly shows the result, which is totally undesirable. I am now totally confused about Futures and Future Functions. Someone please help me out.
Instead of passing the Future you could pass the function itself which returns the Future. You can try this example here on DartPad.
You have to modify MyApp like this:
final icecreamSource = DataService.getIcecream; // No () as we want to store the function
final pizzaSource = DataService.getPizza; // Here aswell
In MenuButton and in AnotherPage we need:
final Future<String> Function() dataSource; // Instead of Future<String> dataSource
No we could pass the future directly to the FutureBuilder but it's bad practice to let the FutureBuilder execute the future directly as the build method gets called multiple times. Instead we have this:
class _AnotherPageState extends State<AnotherPage> {
late final Future<String> dataSource = widget.dataSource(); // Gets executed right here
...
}
Now we can pass this future to the future builder.
instead passing Future function, why you dont try pass a string ?
Remove all final Future<String> dataSource;. You dont need it.
you can use the label only.
.....
body: Center(
child: FutureBuilder<String>(
future: widget.label == 'Pizza'
? DataService.getPizza()
: DataService.getIcecream(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
....
i just test it in https://dartpad.dev . its work fine.
you dont have to make complex, if you can achive with simple way.
So basically I am using the showModalBottomSheet widget to show a full screen container that has a GestureDetector that runs this onTap:
onTap: () {
final String testText = "Sup";
Navigator.of(context).pop(testText);
}
This obviously returns the text when I await the result when I call showModalBottomSheet however, I would also like to set enableDrag: true so that we can swipe the modal away.
My question is:
How can I pass an argument/result back when doing a swipe to dismiss? With a function, I can simple do Navigator.of(context).pop(...) but when we swipe, there is no function and so therefore I can't figure out a way to pass arguments when we swipe to dismiss.
Thank you!
When you swipe the pop() method gets called and there isn't anyway to override it but I figured out a way to handle this scenario:
You can use then() method on showModalBottomSheet() like this:
showModalBottomSheet(context: context, builder: (context) => SecondPage()).then((value) {
str = "done";
print("data: $str");
});
Keep in mind that the value that future returns the value that gets returned in pop() method otherwise it is null.
It looks like showModalBottomSheet doesn't have a way to specify the close value. So it always returns null in that case. And there is not much you can do. But the options I see:
use result wrapper to return value by reference. Like that:
class MyResult {
int myValue;
}
class MyBottomWidget ... {
MyResult result;
MyBottomWidget(this.result);
// then you can initialize the value somewhere
// result.myValue = 5;
}
final result = MyResult();
await showModalBottomSheet(context: context, builder: (_) => MyBottomWidget(result);
// and here you can use your value
print('Result value: ${result.myValue});
another way is to return a value if the result of showModalBottomSheet is null which means a modal has been closed / dissmissed.
final result = await showModalBottomSheet(...);
if (result == null) {
// initialize the value with a value you need when modal is closed.
}
You can make a func wrapper to simplify the process:
Future<T> myShowModalBottomSheet<T>(BuildContext context, WidgetBuilder builder, T dismissValue) async {
final value = await showModalBottomSheet<T>(context: context, builder: builder);
return value ?? dismissValue;
}
or like that:
Future<T> myShowModalBottomSheet<T>(BuildContext context, WidgetBuilder builder, T Function() dismissedValueBuilder) async {
final value = await showModalBottomSheet<T>(context: context, builder: builder);
return value ?? dismissedValueBuilder();
}
the other way is to make your own shoModalBottomSheet that will allow to the specified value. The source code of the function is available so it's not that difficult to implement it. It would be the cleanest solution, but still, it has some downsides as well. First is it's much more to do. The other thing is your solution will be not in sync with the native flutter function. I.e. if flutter will change the behavior of that function or widgets you will need to update your code.
search on pub.dev for a package that will have the functionality you need.
Maybe there is some other way, but I'm not aware of it).
Use this widget and add Navigator. Pop.
->Use this WillPopScope
For More Information Visit Click Here
OR
If you want to pass data in pop. Then see Example:-
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
title: 'Returning Data',
home: HomeScreen(),
),
);
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Returning Data Demo'),
),
body: const Center(
child: SelectionButton(),
),
);
}
}
class SelectionButton extends StatelessWidget {
const SelectionButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
_navigateAndDisplaySelection(context);
},
child: const Text('Pick an option, any option!'),
);
}
// A method that launches the SelectionScreen and awaits the result from
// Navigator.pop.
void _navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push returns a Future that completes after calling
// Navigator.pop on the Selection Screen.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
// After the Selection Screen returns a result, hide any previous snackbars
// and show the new result.
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('$result')));
}
}
class SelectionScreen extends StatelessWidget {
const SelectionScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pick an option'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
// Close the screen and return "Yep!" as the result.
Navigator.pop(context, 'Yep!');
},
child: const Text('Yep!'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
// Close the screen and return "Nope." as the result.
Navigator.pop(context, 'Nope.');
},
child: const Text('Nope.'),
),
)
],
),
),
);
}
}
Selection Screen:-
Navigator.pop(context, 'Yep!');
Button Code (Navigator Push Like this):-
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
For More Information
The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}
I'm trying to show a splash screen on initial app startup until I have all of the data properly retrieved. As soon as it's there, I want to navigate to the main screen of the app.
Unfortunately, I can't find a good way to trigger a method that runs that kind of Navigation.
This is the code that I'm using to test this idea. Specifically, I want to run the command Navigator.pushNamed(context, 'home'); when the variable shouldProceed becomes true. Right now, the only way I can think to do it is to display a button that I need to press to trigger the navigation code:
import 'package:flutter/material.dart';
import 'package:catalogo/src/navigationPage.dart';
class RouteSplash extends StatefulWidget {
#override
_RouteSplashState createState() => _RouteSplashState();
}
class _RouteSplashState extends State<RouteSplash> {
ValueNotifier<bool> buttonTrigger;
bool shouldProceed = false;
_fetchPrefs() async { //this simulates the asynchronous function
await Future.delayed(Duration(
seconds:
1)); // dummy code showing the wait period while getting the preferences
setState(() {
shouldProceed = true; //got the prefs; ready to navigate to next page.
});
}
#override
void initState() {
super.initState();
_fetchPrefs(); // getting prefs etc.
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: shouldProceed
? RaisedButton(
onPressed: () {
print("entered Main");
Navigator.pushNamed(context, 'home'); // <----- I want this to be triggered by shouldProceed directly
},
child: Text("Continue"),
)
: CircularProgressIndicator(), //show splash screen here instead of progress indicator
),
);
}
}
So, in short, how can I trigger a function that runs the Navigation code when shouldProceed changes?
All you have to do is after you get the preferences, just navigate to the screen and have the build method just build a progress indicator.
Try this:
_fetchPrefs() async {
await Future.delayed(Duration(seconds: 1));
Navigator.of(context).pushNamed("home"); //stateful widgets give you context
}
Here's your new build method:
#override
Widget build(BuildContext context) {
return Center(child: CircularProgressIndicator());
}
I've made a DartPad to illustrate: https://dartpad.dartlang.org/431fcd9a1ea5748a82506f13be042e85
Create a widget which can be shown or hidden similar to my ProgressBar code. Show and hide based on a timer or when the data load from the api has completed.
ProgressBar progressBar = new ProgressBar();
progressBar.show(context);
Provider.of<Api>(context, listen: false)
.loadData(context, param)
.then((data) {
progressBar.hide();
Provider.of<Api>(context, listen: false).dataCache.login = loginValue;
Navigator.pop(context, true);
Navigator.pushNamed(context, "/home");
}
});
replace the ProgressBar code with your splash screen:
class ProgressBar {
OverlayEntry _progressOverlayEntry;
void show(BuildContext context){
_progressOverlayEntry = _createdProgressEntry(context);
Overlay.of(context).insert(_progressOverlayEntry);
}
void hide(){
if(_progressOverlayEntry != null){
_progressOverlayEntry.remove();
_progressOverlayEntry = null;
}
}
OverlayEntry _createdProgressEntry(BuildContext context) =>
OverlayEntry(
builder: (BuildContext context) =>
Stack(
children: <Widget>[
Container(
color: Colors.white.withOpacity(0.6),
),
Positioned(
top: screenHeight(context) / 2,
left: screenWidth(context) / 2,
child: CircularProgressIndicator(),
)
],
)
);
double screenHeight(BuildContext context) =>
MediaQuery.of(context).size.height;
double screenWidth(BuildContext context) =>
MediaQuery.of(context).size.width;
}
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.