Back button not showing when using ShellRouter with GoRouter in flutter - flutter

I have a simple structure
A ShellRoute with Home as a initial page a sub route with /post/:id I navigator from the homepage to the post page using push but the backbutton is not showing on the app bar.
Also it's worth noting if on the Post widget the context.canPop() returns true but in the didUpdateWidget Methods of the Shell WidgetGoRouter.of(context).canPop() returns false so my guess is that for some reason the context of the shell is not the same as the one of the page but my NavigatorState keys are the ones of the same _shellNavigator. Yet if I hot reload on the Post Widget the canPop method start returning true but the back button does not appear
I tried setting the two pages Home and Post at the same level with no luck (see comments). I read the doc and another answer on SO and I think I follow everything done. I might be missing something obvious.
go_router: ^6.0.9
flutter: 3.3.0
final GlobalKey<NavigatorState> _rootNavigator = GlobalKey(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigator =
GlobalKey(debugLabel: 'shell');
class Shell extends StatefulWidget {
const Shell({super.key, required this.child});
final Widget child;
#override
State<Shell> createState() => _ShellState();
}
class _ShellState extends State<Shell> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: true,
title: const Text('Title'),
),
body: widget.child,
);
}
}
class App extends StatelessWidget {
App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: _router.routerDelegate,
routeInformationParser: _router.routeInformationParser,
routeInformationProvider: _router.routeInformationProvider,
);
}
final GoRouter _router =
GoRouter(navigatorKey: _rootNavigator, initialLocation: '/', routes: [
ShellRoute(
navigatorKey: _shellNavigator,
builder: (context, state, child) =>
Shell(key: state.pageKey, child: child),
routes: [
GoRoute(
parentNavigatorKey: _shellNavigator,
path: '/',
builder: (context, state) => Home(
key: state.pageKey,
),
routes: [
GoRoute(
parentNavigatorKey: _shellNavigator,
path: 'post/:id',
builder: (context, state) => Post(
key: state.pageKey,
),
)
]),
// GoRoute(
// parentNavigatorKey: _shellNavigator,
// path: '/post/:id',
// builder: (context, state) => Post(
// key: state.pageKey,
// ),
// )
])
]);
}
class Home extends StatelessWidget {
const Home({super.key});
#override
Widget build(BuildContext context) {
var data = [
{'id': 'Route a'},
{'id': 'Route b'},
];
return GridView.count(
crossAxisSpacing: 5,
mainAxisSpacing: 5,
crossAxisCount: 2,
children: data
.map((e) => Center(
child: InkWell(
onTap: () {
GoRouter.of(context).push("/post/${e['id']}");
// context.push("/post/${e['id']}");
},
child: Text(e['id']!,
style: const TextStyle(
color: Colors.black,
)),
),
))
.toList(),
);
}
}

Related

Having Flutter gorouter redirect property doesn't let navigation work

Having Flutter gorouter redirect property at top-level doesn't let navigation to go to/push any other page. It redirects to initialLocation upon pressing routing button instead of intended page(ItemOne()).
Log:
[GoRouter] going to /one
[GoRouter] redirecting to RouteMatchList(/)
Gorouter Code:
void main() => runApp(const NavApp());
const isAuth = true;
class NavApp extends StatelessWidget {
const NavApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: GoRouter(
debugLogDiagnostics: true,
initialLocation: '/',
redirect: (context, state) => isAuth ? '/' : '/one',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const NavHome(),
),
GoRoute(
path: '/one',
builder: (context, state) => const ItemOne(),
),
],
),
);
}
}
HomePage Code:
class NavHome extends StatelessWidget {
const NavHome({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Nav Home'),
),
body: Center(
child: IconButton(
onPressed: () => context.push('/one'),
icon: const Text('Push One'),
),
),
);
}
}
Page we route to using button:
class ItemOne extends StatelessWidget {
const ItemOne({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Item 1'),
),
body: const Text('This is page for Item One'),
);
}
}
Please use it like this
Center(
child: IconButton(
onPressed: () => context.go('/one'),
icon: const Text('Push One'),
),

AppBar navigation for nested pages

Normally you would have a Scaffold with its own AppBar on every page, but why is that needed with go_router? Why can't you just have a single Scaffold with an AppBar and let that handle navigation.
Going to the SecondScreen in this example won't update the AppBar and show the back button.
Why is this not possible?
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(const MyApp());
final GoRouter _router = GoRouter(
routes: <RouteBase>[
ShellRoute(
builder: (context, state, child) {
return Scaffold(
body: child,
appBar: AppBar(
title: const Text('Test'),
),
);
},
routes: [
GoRoute(
path: '/',
pageBuilder: ((context, state) => const NoTransitionPage(
child: HomeScreen(),
)),
routes: [
GoRoute(
path: 'second',
pageBuilder: ((context, state) => const NoTransitionPage(
child: SecondScreen(),
)),
),
],
),
],
),
],
);
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
#override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () => context.go('/second'),
child: const Text('Go to the Second screen'),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
#override
Widget build(BuildContext context) {
return const Center(
child: Text('Second screen'),
);
}
}
Edit: SecondScreen in this example won't update the AppBar and show the back button :
Because you are using the same appBar for all the child compnents. The main purpose of the ShellRoute is have same appBar. For it to have backButton it cannot have same appBar. The appBar should change.Which violates the usage of ShellRoute
For you to have backbutton you should navigate from ShellRoute to GoRoute or GoRoute to ShellRoute , Withtin the ShellRoute back button is not possible as it creates a shell around its children. So having backbutton inside the ShellRoute would defeat the purpose of having a same appBar throughout.
There are two corrections
change second to /second
GoRoute(
path: '/second', 👈 Change to this
pageBuilder: ((context, state) => const NoTransitionPage(
child: SecondScreen(),
)),
),
For appBar to be having backbutton use context.push('/second') instead of context.go('/second')
- context.go('/second') // Used to replace a page
- context.push('/second') // Used to push a page to navigation stack.
child: ElevatedButton(
onPressed: () => context.push('/second'), // 👈 change it to push
child: const Text('Go to the Second screen'),
),
Refer this examples:
How to change the app bar title depending on the selected GoRouter route?
How to Use Shell Route with GoRoute in same hierarchy Routes

GoRouter - Can I push 2 pages at once?

I'm using go_router and I am about to do this in a callback of one of my buttons:
EvelatedButton(
onPressed: () {
GoRouter.of(context)
..push('/page-1')
..push('/page-2');
},
)
This is to push 2 pages in the history at once. After the user click on this button, he ends up on the page page-2 and when he pops the page, there is page-1.
Is it acceptable to do that or is there any reason not to do it?
What would be those reasons and what should I do instead?
I don't think I've seen anything like that in go_router's examples.
For more context, here is a code snippet (or checkout https://github.com/ValentinVignal/flutter_app_stable/tree/go-router/push-twice-at-once):
When the button is pressed, I want to display the dialog page with the page-1 in the background.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(const MyApp());
}
final router = GoRouter(
initialLocation: '/page-0',
routes: [
GoRoute(
path: '/page-0',
builder: (_, __) => const Page0Screen(),
),
GoRoute(
path: '/page-1',
builder: (_, __) => const Page1Screen(),
),
GoRoute(
path: '/dialog',
pageBuilder: (context, state) => DialogPage(
key: state.pageKey,
child: const DialogScreen(),
),
),
],
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}
class Page0Screen extends StatelessWidget {
const Page0Screen({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page 0')),
body: Center(
child: ElevatedButton(
onPressed: () {
GoRouter.of(context)
..push('/page-1')
..push('/dialog');
},
child: const Text('Push'),
),
),
);
}
}
class Page1Screen extends StatelessWidget {
const Page1Screen({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page 1')),
body: const Center(
child: Text('Page 1'),
),
);
}
}
class DialogScreen extends StatelessWidget {
const DialogScreen({super.key});
#override
Widget build(BuildContext context) {
return const AlertDialog(
title: Text('Dialog'),
);
}
}
class DialogPage extends Page {
const DialogPage({
required this.child,
super.key,
});
final Widget child;
#override
Route createRoute(BuildContext context) {
return DialogRoute(
settings: this,
context: context,
builder: (context) {
return child;
},
);
}
}
Assuming your goal is to display a dialog you can use the showDialog function in flutter.
Below is a sample
showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Basic dialog title'),
content: const Text('A dialog is a type of modal window that\n'
'appears in front of app content to\n'
'provide critical information, or prompt\n'
'for a decision to be made.'),
actions: <Widget>[
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Disable'),
onPressed: () {
GoRouter.of(context).pop();
},
),
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Enable'),
onPressed: () {
GoRouter.of(context).pop();
},
),
],
);
},
);
go_router doesn't support pushing two routes at the same time. And it is not a good practice to push 2 pages at the same time.
What can you do instead?
You can transition from page1 to page2
Go to dialog page in the init method of the page2 using context.go('/dialog');
On exiting dialog page you can use context.pop() which will land you in page1

Flutter: How to use GoRouter with Bloc to route from Splash Screen to Login Screen

So I switched to Go router recently in my app since it is very easy to implement. But I am having trouble moving from Splash Screen to Login Screen. I have logic in my Splash Screen where I check if the user is logged in or not. Based on the user's auth, the screen either goes to Login Screen or the Home page.
This is Splash Screen.
class SplashScreen extends StatefulWidget {
static const routeName = "/SplashScreen";
const SplashScreen({Key? key}) : super(key: key);
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return BlocConsumer<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
if (kDebugMode) {
print('Listener: $state');
}
Future.delayed(const Duration(seconds: 3), () {
if (state.authStatus == AuthStatus.unAuthenticated) {
GoRouter.of(context).go('/login');
Navigator.pushNamed(context, SignUpScreen.routeName);
} else if (state.authStatus == AuthStatus.authenticated) {
//Navigator.popUntil(context, (route) => route.isFirst);
Navigator.pushReplacementNamed(context, HomePage.routeName);
}
});
},
builder: (context, Object? state) {
if (kDebugMode) {
print('object: $state');
}
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Welcome to Musajjal",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
const SizedBox(
height: 20,
),
Image.asset(
'assets/musajjalBlue.png',
width: 300,
height: 300,
),
const SizedBox(
height: 20,
),
const Text(
"Hifz ul Quran Records",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
const SizedBox(
height: 20,
),
const CircularProgressIndicator(
color: Colors.blueGrey,
),
],
),
),
);
},
);
}
}
Next, this. is my Go Router function
GoRouter _router(AuthenticationBloc bloc) {
return GoRouter(
routes: <GoRoute>[
GoRoute(
path: '/',
builder: (context, state) => const SplashScreen(),
routes: <GoRoute>[
GoRoute(path: 'login', builder: (context, state) => LoginScreen()),
GoRoute(
path: 'signUp', builder: (context, state) => SignUpScreen()),
GoRoute(path: 'homePage', builder: (context, state) => HomePage())
],
redirect: (BuildContext context, GoRouterState state) {
final isLoggedIn =
bloc.state.authStatus == AuthStatus.authenticated;
final isLoggingIn = state.location == '/login';
print(isLoggedIn);
if (!isLoggedIn && !isLoggingIn) return '/login';
if (isLoggedIn && isLoggingIn) return '/homePage';
return null;
},
),
],
);
}
The issue is the app gets stuck on Splash Screen and it does not move forward to login screen. Please help.
Try changing the logic in the redirect to
if (!isLoggedIn && !isLoggingIn) return '/login';
if (isLoggedIn) return '/homePage';
also, consider having the login logic as OR instead of AND --optional
if (!isLoggedIn || !isLoggingIn) return '/login';
Try the follwing code:
main.dart
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthenticationBloc>(
create: (context) => AuthenticationBloc), 👈 Specify the BlocProvider here
),
],
child: MaterialApp(
theme: customTheme(context),
debugShowCheckedModeBanner: false,
routerConfig: router,
}
router.dart
final GoRouter router = GoRouter(routes: [
GoRoute(
path: "/",
builder: (context, state) {
return BlocBuilder<AuthCubit, AuthState>(
buildWhen: (oldState, newState) {
return oldState is AuthInitialState;
},
builder: (context, state) {
if (state is AuthLoading) {
// return const SplashScreen(); // alternative way
context.goNamed(SplashScreen.routeName); 👈 Display your splash screen here and you can provide delay while changing state in your bloc
}
else if (state is AuthenticationUnauthenticated) {
context.goNamed(LoginPage.routeName);
} else if (state is Authenticated) {
context.goNamed(HomePage.routeName);
} else {
return const Scaffold();
}
},
);
}),

How to make go_router, Appbar, and Drawer work together in Flutter?

I'm using the go_router package in Flutter for app routing, but I'm running into issues when I use it alongside the default Flutter Appbar and Drawer widgets.
Typical "go" and "push" methods that I'm calling from clicks in the Drawer don't work as expected when pushing the back button.
The AppBar doesn't imply the leading back or menu behavior.
Is there something particular that needs to be done to get go_router to play nicely with the Flutter Navigator? Maybe I need to set some particular fields or a global key?
Here's what my setup looks like:
class MainApp extends ConsumerStatefulWidget {
const MainApp({Key? key}) : super(key: key);
#override
ConsumerState<MainApp> createState() => _MainAppState();
}
class _MainAppState extends ConsumerState<MainApp> {
late GoRouter router;
late Future<void> jwtInit;
#override
void initState() {
jwtInit = ref.read(jwtProvider.notifier).init();
router = GoRouter(
routes: [
GoRoute(
path: "/",
name: "home",
pageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: const HomeScreen(),
),
),
GoRoute(
path: "/settings",
name: "settings",
pageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: const SettingsScreen(),
),
),
GoRoute(
path: "/programs",
name: "programs",
pageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: const ProgramScreen(),
),
),
GoRoute(
path: "/programs/:programId",
name: "program",
pageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: ProgramDetailsScreen(
// programId: 39,
programId: int.parse(state.params["programId"]!),
),
),
),
GoRoute(
path: "/activity/:activityId",
name: "activity",
pageBuilder: (context, state) {
return MaterialPage<void>(
key: state.pageKey,
child: ActivityScreen(
id: int.parse(state.params["activityId"]!),
),
);
}),
GoRoute(
path: "/login",
name: "login",
pageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: const LoginScreen(),
),
),
],
errorPageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: const Scaffold(
body: Center(
child: Text("PAGE NOT FOUND!"),
),
),
),
// refreshListenable: api,
redirect: (context, state) {
final loggedIn = ref.read(jwtProvider.notifier).isLoggedIn;
final goingToLogin = state.location == '/login';
// the user is not logged in and not headed to /login, they need to login
if (!loggedIn && !goingToLogin) return '/login';
// the user is logged in and headed to /login, no need to login again
if (loggedIn && goingToLogin) return '/';
// no need to redirect - go to intended page
return null;
},
);
super.initState();
}
#override
Widget build(BuildContext context) {
//The reason for this FutureBuilder is to wait for the api key to
//load from storage before allowing the initial page to route. Otherwise
//the routing goes too fast and it looks logged out.
return FutureBuilder(
future: jwtInit,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
//Run the UI
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'MyApp',
theme: MyTheme.darkTheme(context),
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
);
} else {
return Container();
}
});
}
}
In my drawer, I'm calling the navigation like this:
onTap: () {
context.push("/settings");
}
Use ShellRouter with GoRouter to meet your requirement!
Example:
Router
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter(
initialLocation: '/',
navigatorKey: _rootNavigatorKey,
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
pageBuilder: (context, state, child) {
print(state.location);
return NoTransitionPage(
child: ScaffoldAppAndBottomBar(child: child));
},
routes: [
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/home',
pageBuilder: (context, state) {
return NoTransitionPage(
child: Scaffold(
body: const Center(
child: Text("Home"),
),
),
);
},
),
GoRoute(
path: '/',
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(child: Text("Initial")),
),
);
},
),
],
),
],
);
ScaffoldAppAndBottomBar
class ScaffoldAppAndBottomBar extends StatelessWidget {
Widget child;
ScaffoldAppAndBottomBar({super.key, required this.child});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text(
"App Bar",
),
backgroundColor: Colors.amber,
),
body: SafeArea(child: child),
bottomNavigationBar: Container(
color: Colors.blue,
height: 56,
width: double.infinity,
child: const Center(child: Text("Bottom Navigation Bar")),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.red,
onPressed: () {
context.go('/home');
},
child: const Icon(Icons.home),
));
}
}
Output:
Initially
After pressing floating button
Refer detailed code and explaination of bottom NavigationBar using ShellRoute and GoRouter here