How to remove default navigation route animation - flutter

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;
}
}
}

Related

Never is not provided to BlocBuilder<SocialCubit, SocialStates>. Context used for Bloc retrieval must be a descendant of BlocProvider

I tried to make a social media app and when setup my layout contains bottom Nav bar and screens also my cubit is ok and the code in the main is ok I tried to change bottom Nav to Text and the problem solved I had a problem with getting Bloc from child widget and I have no idea what I'm doing wrong.
how can I got this error any one can help ?
class SocialLayout extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocConsumer<SocialCubit , SocialStates>
(listener: (context,state){},
builder: (context,state)
{
var cubit= SocialCubit.get(context);
return Scaffold(
appBar: AppBar(
title: Text('News Feed',),
),
body: cubit.screens[cubit.currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: cubit.currentIndex,
onTap: (index){
cubit.changeBottomNav(index);
},
items: [
BottomNavigationBarItem(icon: Icon(IconBroken.Home,),label: "Home"),
BottomNavigationBarItem(icon: Icon(IconBroken.Chat,),label: "Chats"),
BottomNavigationBarItem(icon: Icon(IconBroken.Setting,),label: "Settings"),
BottomNavigationBarItem(icon: Icon(IconBroken.User,),label: "Users"),
],
),
);
}
);
}
}
//cubit
class SocialCubit extends Cubit<SocialStates> {
SocialCubit() : super(SocialInitialState());
static SocialCubit get(context) => BlocProvider.of(context);
SocialUserModel ? socialUserModel;
void getUserData() {
uId = CacheHelper.getData(key: 'uId');
emit(SocialGetUserLoadingState());
FirebaseFirestore.instance.collection('users').doc(uId).get().then((value) {
socialUserModel = SocialUserModel.fromJson(value.data());
print(socialUserModel.toString());
emit(SocialGetUserSuccessState());
})
.catchError((error) {
print(error.toString());
emit(SocialGetUserErrorState(error.toString()));
});
}
int currentIndex =0;
List<Widget>screens =[
Feeds(),
Chats(),
Users(),
Setting(),
];
void changeBottomNav(int index)
{
currentIndex = index;
emit(SocialChangeBottomNav());
}
}
//main
void main() async
SimpleBlocObserver();
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
DioHelper.init();
await CacheHelper.init();
bool ? isDarkMode = CacheHelper.getData(key: 'isDarkMode');
Widget widget;
uId = CacheHelper.getData(key: 'uId');
if(uId != null)
{
widget = SocialLayout();
}
else{
widget=SocialLoginScreen();
}
runApp(MyApp(isDarkMode, widget ,token ));
}
// ignore: must_be_immutable
class MyApp extends StatelessWidget {
bool ? isDarkMode;
late final Widget startWidget;
String? token;
MyApp(this.isDarkMode,this.startWidget, String? token, {Key? key}) : super(key: key) ;
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider( create: (BuildContext context) => AppCubit()..changeAppMode(fromShared: isDarkMode,),
),
BlocProvider( create: (BuildContext context) => SocialCubit()..getUserData(),
),
],
child: BlocConsumer<AppCubit, AppStates>(
listener: (context, state){},
builder: (context ,state){
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: AppCubit.get(context).isDarkMode ? ThemeMode.light: ThemeMode.dark,
home: AnimatedSplashScreen(
splash: SplashScreen(),
nextScreen: startWidget,
splashIconSize: 700,
animationDuration: const Duration(milliseconds: 2000),
splashTransition: SplashTransition.fadeTransition,
),
theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme,
builder: BotToastInit(),
navigatorObservers: [BotToastNavigatorObserver()],
);
},
),
);
}
}
*Invalid argument(s): Never is not provided to BlocBuilder<SocialCubit, SocialStates>. Context used for Bloc retrieval must be a descendant of BlocProvider.
Solved by remove unused bloc packages from pubsbec.yaml file

Flutter app: hide appBar action according to ApplicationState value

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),
),
);
}
}

Using Navigator with a bottomNavigationBar

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),
),
],
),
),
);
}
}

BLOC losing context when navigating to a new page

I'm using the BLOC pattern to authenticate a user in my app. I have a main BlocProvider that wraps my app. And a BlocBuilder to build according to the authentication state.
If the user is unauthenticated i have onboarding / intro screens that will navigate to the login screen.
The login screen is wrapped in another BlocProvider that contains a button that will do the login, and add a logged in event when the login is successful.
Problem is when i navigate from the onboarding screens i loose the main authenticationBloc context. What do i need to to to have access to the authentication bloc after i pushed a new screen.
void main() {
WidgetsFlutterBinding.ensureInitialized();
Bloc.observer = SimpleBlocObserver();
runApp(
MyApp(),
);
}
class AuthenticationWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider<AuthenticationBloc>(
create: (context) => AuthenticationBloc()..add(AppStarted()),
child: MyApp(),
),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocListener<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
if (state is Authenticated) {
_appUserProfileRepository = AppUserProfileRepository();
}
},
child: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (context, state) {
_authCredentialHelper = state.authCredentialHelper;
if (state is Uninitialized) {
return SplashScreen();
}
if (state is Unauthenticated) {
return OnboardingScreens(authCredentialHelper: _authCredentialHelper);
}
if (state is InvalidRegistration) {
return RegisterProfileScreen(authCredentialHelper: _authCredentialHelper);
}
if (state is Authenticated) {
xxx
}
return Scaffold(body: Center(child: LoadingIndicator()));
},
),
);
}
}
This is the onboarding screen where i loose the authenticationbloc context as soon as i navigate
class OnboardingScreens extends StatelessWidget {
final AuthCredentialHelper authCredentialHelper;
OnboardingScreens({this.authCredentialHelper});
_pages(BuildContext context) {
return [
xxx
];
}
_getStartedClicked(BuildContext context) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: IntroductionScreen(
pages: _pages(context),
onDone: () => _getStartedClicked(context),
showSkipButton: true,
done: xxx
),
),
);
}
}
When adding a breakpoint at 1. the context is fine with a valid value for BlocProvider.of(context)
Stepping to 2. gives me an error:
BlocProvider.of() called with a context that does not contain a Cubit of type AuthenticationBloc.
_getStartedClicked(BuildContext context) {
1----->Navigator.push(context, MaterialPageRoute(builder: (context) {
2----->return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
}
This is the LoginScreen code
class LoginScreen extends StatelessWidget {
final AuthCredentialHelper authCredentialHelper;
LoginScreen({this.authCredentialHelper});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back, color: darkBlue),
onPressed: () => Navigator.of(context).pop(),
),
backgroundColor: Colors.transparent,
elevation: 0.0,
),
body: SafeArea(
child: Center(
child: BlocProvider<LoginBloc>(
create: (context) => LoginBloc(authCredentialHelper: authCredentialHelper),
child: LoginForm(authCredentialHelper: authCredentialHelper),
),
),
),
);
}
}
Getting this error:
The following assertion was thrown building _InheritedProviderScope<LoginBloc>(value: Instance of 'LoginBloc'):
BlocProvider.of() called with a context that does not contain a Cubit of type AuthenticationBloc.
No ancestor could be found starting from the context that was passed to BlocProvider.of<AuthenticationBloc>().
This can happen if the context you used comes from a widget above the BlocProvider.
Change this :
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LoginScreen(authCredentialHelper: authCredentialHelper);
}));
to
Navigator.push(
context,
MaterialPageRoute(builder: (contextLoginScreen) {
return BlocProvider.value(
value: context.bloc<AuthenticationBloc>(),
child: LoginScreen(authCredentialHelper: authCredentialHelper));
}),
);

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.