Flutter pushReplaceNamed is not working correctly - flutter

I am struggling in my app:
After the user logs in I navigate to my HomeView with pushReplaceNamed , so the user should not be able to pop (by dragging) on the HomeView because in my thought it is the root view. However that is not the case.. Even after calling pushReplaceNamed the user can still drag to pop:
I simply call:
Navigator.pushReplacementNamed(
context,
Views.home,
);
On the first start of the App StartView is displayed, from there I navigate to EmailView and from there to LoginView. The code above is inside my LoginView. In all the other views I simply call pushNamed.
What am I missing here? Why can the user drag pop on HomeView?
(I know I can disable it with WillPopScope, but that does not feel right...)
This is my setup in my MaterialApp:
Widget build(BuildContext context) {
return MaterialApp(
title: 'AppFlug',
navigatorKey: Get.key,
theme: ThemeData(
fontFamily: AppTextStyles.montserrat,
primarySwatch: ColorService.createMaterialColor(
AppColors.blue,
),
backgroundColor: AppColors.white,
scaffoldBackgroundColor: AppColors.white,
),
initialRoute:
AuthenticationService.isLoggedIn() ? Views.home : Views.start,
onGenerateRoute: AppRouter.generateRoute,
);
}
And here is my AppRouter:
class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case Views.start:
return MaterialPageRoute(
builder: (context) => const StartView(),
);
case Views.email:
return MaterialPageRoute(
builder: (context) => EmailView(),
);
case Views.signUp:
String email = settings.arguments as String;
return MaterialPageRoute(
builder: (context) => SignUpView(
email: email,
),
);
case Views.login:
String email = settings.arguments as String;
return MaterialPageRoute(
builder: (context) => LoginView(
email: email,
),
);
case Views.home:
return MaterialPageRoute(
builder: (context) => HomeView(),
);
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text(
'No route defined for ${settings.name}',
),
),
),
);
}
}
}

In your scenario, the user will be able to drag to pop and will return to EmailView cause you replaced login with home, this is what happens right? if u want to pop all of them (StartView, EmailView, LoginView) when you push to HomeView, then use:
Navigator.pushNamedAndRemoveUntil(context, Views.Home, (route) => route.settings.name == '/')
Also, add this to your AppRouter Cases
MaterialPageRoute(
settings: RouteSettings(name: /*name of the route*/),
builder: (context) => ...

With the help of Omar I got it working: so pushReplaceNamed is only replacing the top most route. In my case all I had to do was call:
Navigator.pushNamedAndRemoveUntil(
context,
Views.home,
(route) => false,
);
To quote the doc:
To remove all the routes below the pushed route, use a [RoutePredicate] that always returns false (e.g. (Route route) => false).

Related

pushAndRemoveUntil is not working with onGenerateRoute in flutter

pushAndRemoveUntil is not proper working with "onGenerateRoute" in MaterialApp but if I have used pushAndRemoveUntil the "routes" in MaterialApp So it's working fine.
current screen name is FiveScreen. and here is the one button and below code in for onclick.
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (BuildContext context)=>ThirdScreen()), ModalRoute.withName(MUtils.second));
if I have use the "routes" in MaterialApp So it's navigate to "SecondScreen" after onbackpress to navigate to "FirstScreen" but If I have Use the the "onGenerateRoute" in MaterialApp so it's redirect to "secondScreen" but after click on the backpress so App is closed.
Thank you in advance.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
/*routes: {
MUtils.home: (context) => HomeScreen(),
MUtils.second: (context) => SecondScreen(),
MUtils.thirdScreen: (context) => ThirdScreen(),
MUtils.fourth: (context) => FourthScreen(),
MUtils.settings: (context) => SettingsScreen(),
},*/
onGenerateRoute:MyRoutes().onGenerateRoute,
//home: const MyHomePage(title: 'Flutter Demo Home Page'),
);}
Below is onGenerate Route class
class MyRoutes{
Route? onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(
builder: (_) => MyHomePage(
title: "Home Screen",
),
);
case MUtils.home:
return MaterialPageRoute(
builder: (_) => HomeScreen(content: settings.arguments,),
);
case MUtils.second:
return MaterialPageRoute(
builder: (_) => SecondScreen(
),
);case MUtils.thirdScreen:
return MaterialPageRoute(
builder: (_) => ThirdScreen(
),
);
case MUtils.fourth:
return MaterialPageRoute(
builder: (_) => FourthScreen(
),
);
case MUtils.settings:
return MaterialPageRoute(
builder: (_) => SettingsScreen(),
);
default:
return null;
}
}
}
I have draw design to better understand.
Route:- HomeScreen=> 1_Screen=> 2_Screen=> 3_Screen=> 4_Screen=> 5_Screen (Now Navigate to) 2_Screen (onBackPress)=> 1_Screen (onBackPress)=> HomeScreen.
onGenerateRoute:- HomeScreen=> 1_Screen=> 2_Screen=> 3_Screen=> 4_Screen=> 5_Screen (Now Navigate to) 2_Screen (onBackPress)=> App Closed.
So here issue the issue in "onGenerateRoute" when backpress in 2_screen to app is closed but it's working in "Routes".
Mycode
onGenerateRoute: (setting){
Widget myroute = Container(
);
if(setting.name == '/'){
myroute = MyHomePage(
title: "Home Screen",
);
}else if(setting.name == MUtils.home){
myroute = HomeScreen();
}else if(setting.name == MUtils.second){
myroute = SecondScreen();
}else if(setting.name == MUtils.thirdScreen){
myroute = ThirdScreen();
}else if(setting.name == MUtils.fourth){
myroute = FourthScreen();
}else if(setting.name == MUtils.settings){
myroute = SettingsScreen();
}
return MaterialPageRoute(
builder: (_) => myroute,
);
},
Your Code:-
onGenerateRoute: (settings) {
final map = {'/': 1, '1': 2, '2': 3, '3': 4, '4': 5,'5':6};
return MaterialPageRoute(
builder: (ctx) => FooPage(map[settings.name]!),
settings: settings, // <<< this is the difference
);
},
Only the deference is here but I couldn't find the issue in my code.

Providing bloc to a new page using named route

I am trying to provide a local bloc to a new page, I found some way to do this by using an anonymous route but it doesn't look elegant
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return BlocProvider.value(
value: context.bloc<MyBloc>(),
child: NewPage());
}),
);
What I want is to do the same thing but using a named route and without creating a global bloc as I simply can't
Create app_router.dart file.
app_router.dart
class AppRouter {
final LocalBloc _localBloc = LocalBloc();
Route onGenerateRoute(RouteSettings routeSettings) {
switch (routeSettings.name) {
case '/':
return MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: _localBloc,
child: Home(),
),
);
case '/page1':
return MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: _localBloc,
child: Page1(),
),
);
case '/page2':
return MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: _localBloc,
child: Pag2(),
),
);
}
}
}
Then in your main.dart file
Add onGenerateRoute
final AppRouter _appRouter = AppRouter();
MaterialApp(
title: 'My App',
onGenerateRoute: _appRouter.onGenerateRoute,
),),
In your navigation, you can do this:
Navigator.of(context).pushNamed('/page1');

Lazy instantiate blocks with BlocProvider.value

I'm using Generated Routes with Flutter, and I would like to share blocs between them.
But I think that maybe the example solution might not scale the best, since the blocs are instantiated with the router.
Is there a way to instantiate them only when the user access that route?
class AppRouter {
final _counterBloc = CounterBloc();
Route onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: _counterBloc,
child: HomePage(),
),
);
case '/counter':
return MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: _counterBloc,
child: CounterPage(),
),
);
default:
return null;
}
}
}
Got it! Flutter has a new keyword called late to do this without any boilerplate.
class AppRouter {
late final CounterBloc _counterBloc = CounterBloc();
Route onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: _counterBloc,
child: HomePage(),
),
);
case '/counter':
return MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: _counterBloc,
child: CounterPage(),
),
);
default:
return null;
}
}
}
That's why we love flutter!
Reference: What is Null Safety in Dart?

Flutter Bloc State won't be yielded after change from the Page pushed on top

The AddWorkoutEvent is dispatched correctly from the PageCreateWorkout with a DbMWorkout.
This will be inserted in the correct table of the DB.
The PageCreateWorkout will be notified with the WorkoutAddedState to go to PageWorkoutDetail with the given workoutId. The routes are stacked on the PageSelectWorkout, in which the WorkoutBloc is mainly used, to show all workouts. In there the PageSelectWorkout shall be refreshed with the new WorkoutLoadedState with the newly given workoutList. (The WorkoutList contains the added Workout, which I checked in the logger; but the State won't be yielded. Note that I am extending equatable to WorkoutStates.)
else if (event is AddWorkoutEvent) {
logger.i("AddWorkoutEvent | workout: ${event.workout}");
yield WorkoutLoadingState();
try {
DbMWorkout workout = await RepositoryWorkout.repo.add(event.workout);
yield WorkoutAddedState(id: workout.id);
List<DbMWorkout> workoutList = await RepositoryWorkout.repo.getAll();
logger.i("AddWorkoutEvent | workoutListLength: ${workoutList.length}");
yield WorkoutLoadedState(workoutList: workoutList); // <-- this state
} catch (e) {
yield WorkoutErrorState(message: e.toString());
}
}
The PageSelectWorkout is the initialPage of a Navigator in an indexedStack:
IndexedStack(
sizing: StackFit.expand,
index: _currentIndex,
children: <Widget>[
Navigator(
key: _pageOverview,
initialRoute: PageOverview.routeName,
onGenerateRoute: (route) =>
RouteGenerator.generateRoute(route)),
Navigator(
key: _pageSelectWorkoutNew,
initialRoute: PageSelectWorkout.routeName,
onGenerateRoute: (route) =>
RouteGenerator.generateRoute(route)),
Navigator(
key: _pageLog,
initialRoute: PageLog.routeName,
onGenerateRoute: (route) =>
RouteGenerator.generateRoute(route)),
Navigator(
key: _pageSettings,
initialRoute: PageSettings.routeName,
onGenerateRoute: (route) =>
RouteGenerator.generateRoute(route))
]),
The named Route to the SelectWorkout is wrapped with the correct Bloc in a BlocProvider:
case PageSelectWorkout.routeName:
return CupertinoPageRoute(
settings: settings,
builder: (context) {
return BlocProvider(
create: (context) => WorkoutBloc(),
child: PageSelectWorkout());
});
Note: In other Events like DeleteWorkoutEvent , which is happening without navigating to another Page, the updated State gets yielded correctly.
I found the answer after checking out some GitHub Issues and it's rather simple:
Since I want to access the same Bloc on 2 different Pages I can not just wrap a new Instance of the Bloc to each of the Page.
Instead I should wrap the BlocProvider with the WorkoutBloc higher in the WidgetTree Hierarchy for example in the main.dart
Before:
case PageSelectWorkout.routeName:
return CupertinoPageRoute(
settings: settings,
builder: (context) => BlocProvider(
create: (context) => WorkoutBloc(), // <-- Instance of WorkoutBloc
child: PageSelectWorkout()));
case PageCreateWorkout.routeName:
return CupertinoPageRoute(
settings: settings,
builder: (context) => BlocProvider(
create: (context) => WorkoutBloc(), // <-- Instance of WorkoutBloc
child: PageCreateWorkout(
workoutIndex: arguments["workoutIndex"],
workoutPosition: arguments["workoutPosition"],
),
));
After:
case PageSelectWorkout.routeName:
return CupertinoPageRoute(
settings: settings, builder: (context) => PageSelectWorkout());
case PageCreateWorkout.routeName:
return CupertinoPageRoute(
settings: settings,
builder: (context) => PageCreateWorkout(
workoutIndex: arguments["workoutIndex"],
workoutPosition: arguments["workoutPosition"],
),
);
and higher in the Inheritance/WidgetTree Hierarchy e.g. main.dart :
return MaterialApp(
debugShowCheckedModeBanner: false,
title: Strings.appTitle,
theme: AppTheme.darkTheme,
home: MultiBlocProvider(providers: [
BlocProvider<WorkoutBloc>(
create: (context) => WorkoutBloc(), // <-- put the Instance higher
),
BlocProvider<NavigationBloc>(
create: (context) => NavigationBloc(),
),
], child: BottomNavigationController()));

Flutter: Dynamic Initial Route

Dears,
I am using provider dart package which allows listeners to get notified on changes to models per se.
I am able to detect the change inside my main app root tree, and also able to change the string value of initial route however my screen is not updating. Kindly see below the code snippet and the comments lines:
void main() => runApp(_MyAppMain());
class _MyAppMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<UserProvider>.value(
value: UserProvider(),
),
ChangeNotifierProvider<PhoneProvider>.value(
value: PhoneProvider(),
)
],
child: Consumer<UserProvider>(
builder: (BuildContext context, userProvider, _) {
return FutureBuilder(
future: userProvider.getUser(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
final User user = snapshot.data;
String initialScreen = LoginScreen.path;
// (1) I am able to get into the condition
if (user.hasActiveLogin()) {
initialScreen = HomeOneScreen.path;
}
return MaterialApp(
title: 'MyApp',
theme: ThemeData(
primarySwatch: Colors.green,
accentColor: Colors.blueGrey,
),
initialRoute: initialScreen,
// (2) here the screen is not changing...?
routes: {
'/': (context) => null,
LoginScreen.path: (context) => LoginScreen(),
RegisterScreen.path: (context) => RegisterScreen(),
HomeOneScreen.path: (context) => HomeOneScreen(),
HomeTwoScreen.path: (context) => HomeTwoScreen(),
RegisterPhoneScreen.path: (context) => RegisterPhoneScreen(),
VerifyPhoneScreen.path: (context) => VerifyPhoneScreen(),
},
);
},
);
},
),
);
}
}
Kindly Note the Below:
These are are paths static const strings
LoginScreen.path = "login"
RegisterScreen.path = "/register-screen"
HomeOneScreen.path = "home-one-screen"
HomeTwoScreen.path = "home-two-screen"
RegisterPhoneScreen.path = "/register-phone-screen"
VerifyPhoneScreen.path = "/verify-phone-screen"
What I am missing for dynamic initialRoute to work?
Many Thanks
According to this issue described on github issues it is not permissible to have initial route changes. At least this is what I understood. However what I did is that I replaced the initialRoute attribute with home attr. Thus this change mandates that initialScreen becomes a widget var.
The changes is shown below:
void main() => runApp(_MyAppMain());
class _MyAppMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<UserProvider>.value(
value: UserProvider(),
),
ChangeNotifierProvider<PhoneProvider>.value(
value: PhoneProvider(),
)
],
child: Consumer<UserProvider>(
builder: (BuildContext context, userProvider, _) {
return FutureBuilder(
future: userProvider.getUser(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
final User user = snapshot.data;
// (1) This becomes a widget
Widget initialScreen = LoginScreen();
if (user.hasActiveLogin()) {
initialScreen = HomeOneScreen();
}
return MaterialApp(
title: 'MyApp',
theme: ThemeData(
primarySwatch: Colors.green,
accentColor: Colors.blueGrey,
),
home: initialScreen,
// (2) here the initial route becomes home attr.
routes: {
'/': (context) => null,
LoginScreen.path: (context) => LoginScreen(),
RegisterScreen.path: (context) => RegisterScreen(),
HomeOneScreen.path: (context) => HomeOneScreen(),
HomeTwoScreen.path: (context) => HomeTwoScreen(),
RegisterPhoneScreen.path: (context) => RegisterPhoneScreen(),
VerifyPhoneScreen.path: (context) => VerifyPhoneScreen(),
},
);
},
);
},
),
);
}
}
Also note on my RegistrationScreen on success api response I did Navigator.of(context).pop()
Thanks