Flutter GoRouter not navigating to selected rouye - flutter

Router config returns the correct path upon selecting a category from the homescreen but not navigating to the desired CategoriesFeedScreen. Here are the 3 code files guigind this navigation and the issues. Please assist as I have tried to resolve this issue for 3 weeks now on my own and no luck. Thanks
Router config (goRouterProvider.dart)
final GlobalKey<NavigatorState> _rootNavigator = GlobalKey(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigator =
GlobalKey(debugLabel: 'shell');
final goRouterProvider = Provider<GoRouter>(
(ref) {
bool isDuplicate = false;
final notifier = ref.read(goRouterNotifierProvider);
return GoRouter(
navigatorKey: _rootNavigator,
debugLogDiagnostics: true,
initialLocation: '/',
refreshListenable: notifier,
redirect: (context, state) {
final isLoggedIn = notifier.value;
final isGoingToLogin = state.subloc == '/auth-screen';
if (!isLoggedIn && !isGoingToLogin && !isDuplicate) {
isDuplicate = true;
return '/auth-screen';
}
if (isLoggedIn && isGoingToLogin && !isDuplicate) {
isDuplicate = true;
return '/';
}
if (isDuplicate) {
isDuplicate = false;
}
return null;
},
routes: [
GoRoute(
path: '/landing',
name: 'landing',
builder: (context, state) => LandingScreen(key: state.pageKey),
),
GoRoute(
path: '/auth-screen',
name: 'auth-screen',
builder: (context, state) => AuthScreen(key: state.pageKey),
),
ShellRoute(
navigatorKey: _shellNavigator,
builder: (context, state, child) =>
BottomNavScreen(key: state.pageKey, child: child),
routes: [
GoRoute(
path: '/',
name: RouteConstants.homeRouteName,
pageBuilder: (context, state) {
return NoTransitionPage(
child: HomeScreen(
key: state.pageKey,
),
);
},
routes: [
GoRoute(
parentNavigatorKey: _shellNavigator,
path: 'categories/:name',
name: RouteConstants.categoriesRouteName,
pageBuilder: (context, state) {
return NoTransitionPage(
child: CategoriesFeedScreen(
name: state.params['name']!,
key: state.pageKey,
),
);
},
),
GoRoute(
parentNavigatorKey: _shellNavigator,
path: 'brands/:id',
name: RouteConstants.brandsRoutename,
pageBuilder: (context, state) {
return NoTransitionPage(
child: BrandsNavRail(
id: int.parse(state.params['id']!),
key: state.pageKey,
),
);
},
),
GoRoute(
parentNavigatorKey: _shellNavigator,
path: 'popular-products/:id',
name: RouteConstants.popularProductsRouteName,
pageBuilder: (context, state) {
return NoTransitionPage(
child: PopularProducts(
id: state.params['id']!,
key: state.pageKey,
),
);
},
)
],
),
GoRoute(
path: '/feeds',
name: RouteConstants.feedsRouteName,
pageBuilder: (context, state) {
return NoTransitionPage(
child: FeedsScreen(
key: state.pageKey,
),
);
},
routes: [
GoRoute(
parentNavigatorKey: _shellNavigator,
path: 'details/:id',
name: RouteConstants.detailsRouteName,
pageBuilder: (context, state) {
// final id = state.params['id'].toString();
return NoTransitionPage(
child: ProductDetailsScreen(
id: state.params['id']!,
key: state.pageKey,
),
);
},
)
],
),
GoRoute(
path: '/cart',
name: RouteConstants.cartRouteName,
pageBuilder: (context, state) {
return NoTransitionPage(
child: CartScreen(
key: state.pageKey,
),
);
},
),
GoRoute(
path: '/account',
name: RouteConstants.accountRouteName,
pageBuilder: (context, state) {
return NoTransitionPage(
child: AccountScreen(
key: state.pageKey,
),
);
},
)
],
)
],
errorBuilder: (context, state) => RouteErrorScreen(
errorMsg: state.error.toString(),
key: state.pageKey,
),
);
},
);
CategoriesFeedScreen:
class CategoriesFeedScreen extends HookConsumerWidget {
final String name;
const CategoriesFeedScreen({Key? key, required this.name}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final categoryName = ref.watch(categoryProvider);
final productList = ref.watch(productProvider.notifier);
final catProductList = productList.findByCatName(
categoryName.toString(),
);
return Scaffold(
appBar: AppBar(
title: Text(name),
),
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
childAspectRatio: 2 / 3,
mainAxisSpacing: 10,
),
itemCount: catProductList.length,
itemBuilder: (context, i) {
return FeedsProduct(
id: catProductList[i].id,
);
},
),
);
}
}
CategoryWidget:
class CategoryWidget extends HookConsumerWidget {
CategoryWidget({Key? key, required this.i}) : super(key: key);
final int i;
#override
Widget build(BuildContext context, WidgetRef ref) {
final categoryLabel = ref.watch(categoryProvider);
return InkWell(
onTap: () {
return context.goNamed(
RouteConstants.categoriesRouteName,
params: {'name': categoryLabel[i].name.toLowerCase()},
);
},
child: Container(
main.dart:
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(goRouterProvider);
final categoryLabels = ref.watch(categoryProvider.notifier);
final productList = ref.watch(productProvider.notifier);
// final themeStatus = ref.watch(
// themeProvider.notifier); // used to switch between dark and light modes
useEffect(() {
resetNewLaunch();
productList.fetchProducts();
categoryLabels.getCategoryLabels();
return;
}, const []);
return ref.watch(authStateChangeProvider).when(
data: (data) => ResponsiveSizer(
builder: (context, orientation, screenType) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
builder: BotToastInit(), // call BotToastInit
// navigatorObservers: [BotToastNavigatorObserver()],
// theme: AppTheme.myTheme(themeStatus.getTheme(), context),
darkTheme: ThemeData.dark(),
routeInformationParser: router.routeInformationParser,
routeInformationProvider: router.routeInformationProvider,
routerDelegate: router.routerDelegate,
);
},
),
error: (error, stackTrace) => ErrorText(error: error.toString()),
loading: () => const Loader(),
);
}
}
I am running the latest flutter version and goRouter package. Thanks a bunch for your help

Fixed the non-navigation issue. Changed the parentNavigatorKey in the sub-routes of the routerConfig file (goRouterProvider) from _shellNavigator to _rootNavigator which allows the new screens to display outside of the bottomwnavigator shell. The sub-routes simply refused to open inside of the bottomNav tabs even though the correct params and paths were being registered.
.

Related

Use StreamBuilder as root of navigation stack with GoRouter

Currently I have a StreamBuilder switching between a HomePage and LandingPage depending on the current auth state. The issue I have encountered is that I cannot pop the stack to the original /landing directory on a state change. This means that when a user logs in, the AuthPage remains on the top of the stack and the StreamBuilder builds the HomePage underneath.
AuthGate
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
#override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
return snapshot.hasData ? const HomePage() : const LandingPage();
},
);
}
}
LandingPage
This pushes the AuthPage to the stack.
class LandingPage extends StatelessWidget {
const LandingPage({super.key});
...
Row(
children: <Widget>[
FloatingActionButton.extended(
heroTag: UniqueKey(),
onPressed: () {
context.push('/auth');
},
label: const Text('Get started'),
),
FloatingActionButton.extended(
heroTag: UniqueKey(),
onPressed: () {
context.push('/auth');
},
label: const Text('Log in'),
),
],
)
...
}
}
Stack before auth change
Stack after auth change
Note how the AuthPage remains on the top of the stack but the Widget under StreamBuilder changes to the HomePage
(This is my first Stack question so please feel free to ask me to amend any information etc.)
If you are using GoRouter, then what you want to achieve should be done through GoRouter similarly to this:
GoRouter(
refreshListenable:
GoRouterRefreshListenable(FirebaseAuth.instance.authStateChanges()),
initialLocation: '/auth',
routes: <GoRoute>[
GoRoute(
path: '/landing',
name: 'landing',
builder: (context, state) {
return LandingPage()
},
routes: [
GoRoute(
path: 'auth',
name: 'auth',
builder: (context, state) => const AuthPage(),
),
],
),
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomePage(),
),
],
errorBuilder: (context, state) {
return const Scaffold(
body: Text('404'),
);
},
redirect: (context, state) async {
final userRepo = injector.get<UserRepository>();
final user = FirebaseAuth.instance;
const authPaths = [
'/landing',
'/landing/auth',
];
bool isAuthPage = authPaths.contains(state.subloc);
if(user != null) {
if (isAuthPage) {
return '/home';
}
}
if(!isAuthPage) {
return '/auth';
}
return null;
},
);
class GoRouterRefreshListenable extends ChangeNotifier {
GoRouterRefreshListenable(Stream stream) {
notifyListeners();
_subscription = stream.asBroadcastStream().listen(
(_) {
notifyListeners();
},
);
}
late final StreamSubscription _subscription;
#override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
Please also read documentation on of GoRouter.

Page always visible in Flutter

I have a app that was developed with FlutterFlow and I`m implementing Google AdMob in it. I need a page that will always be visible in my app life cycle, this page is holding the AdMob banner.
I have already created the page, my problem is: how can I make this page always visible?
This is where I handle de navigation in my app, I`m using GoRouter:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_transition/page_transition.dart';
import '../flutter_flow_theme.dart';
import '../../index.dart';
import '../../main.dart';
import '../lat_lng.dart';
import '../place.dart';
import 'serialization_util.dart';
import 'package:consulta_beneficios/home_tab.dart';
export 'package:go_router/go_router.dart';
export 'serialization_util.dart';
const kTransitionInfoKey = '__transition_info__';
class AppStateNotifier extends ChangeNotifier {
bool showSplashImage = true;
void stopShowingSplashImage() {
showSplashImage = false;
notifyListeners();
}
}
GoRouter createRouter(AppStateNotifier appStateNotifier) => GoRouter(
initialLocation: '/',
debugLogDiagnostics: true,
refreshListenable: appStateNotifier,
errorBuilder: (context, _) => appStateNotifier.showSplashImage
? Builder(
builder: (context) => Container(
color: Color(0xFF12378F),
child: Center(
child: Image.asset(
'assets/images/logo-white.png',
width: 150,
height: 150,
fit: BoxFit.contain,
),
),
),
)
: HomeWidget(),
routes: [
FFRoute(
name: '_initialize',
path: '/',
builder: (context, _) => appStateNotifier.showSplashImage
? Builder(
builder: (context) => Container(
color: Color(0xFF12378F),
child: Center(
child: Image.asset(
'assets/images/logo-white.png',
width: 150,
height: 150,
fit: BoxFit.contain,
),
),
),
)
: HomeWidget(),
routes: [
FFRoute(
name: 'home',
path: 'home',
builder: (context, params) => HomeWidget(),
),
FFRoute(
name: 'search',
path: 'search',
builder: (context, params) => SearchWidget(),
),
FFRoute(
name: 'article',
path: 'article',
builder: (context, params) => ArticleWidget(
data: params.getParam('data', ParamType.JSON),
),
)
].map((r) => r.toRoute(appStateNotifier)).toList(),
).toRoute(appStateNotifier),
],
urlPathStrategy: UrlPathStrategy.path,
);
extension NavParamExtensions on Map<String, String?> {
Map<String, String> get withoutNulls => Map.fromEntries(
entries
.where((e) => e.value != null)
.map((e) => MapEntry(e.key, e.value!)),
);
}
extension _GoRouterStateExtensions on GoRouterState {
Map<String, dynamic> get extraMap =>
extra != null ? extra as Map<String, dynamic> : {};
Map<String, dynamic> get allParams => <String, dynamic>{}
..addAll(params)
..addAll(queryParams)
..addAll(extraMap);
TransitionInfo get transitionInfo => extraMap.containsKey(kTransitionInfoKey)
? extraMap[kTransitionInfoKey] as TransitionInfo
: TransitionInfo.appDefault();
}
class FFParameters {
FFParameters(this.state, [this.asyncParams = const {}]);
final GoRouterState state;
final Map<String, Future<dynamic> Function(String)> asyncParams;
Map<String, dynamic> futureParamValues = {};
// Parameters are empty if the params map is empty or if the only parameter
// present is the special extra parameter reserved for the transition info.
bool get isEmpty =>
state.allParams.isEmpty ||
(state.extraMap.length == 1 &&
state.extraMap.containsKey(kTransitionInfoKey));
bool isAsyncParam(MapEntry<String, dynamic> param) =>
asyncParams.containsKey(param.key) && param.value is String;
bool get hasFutures => state.allParams.entries.any(isAsyncParam);
Future<bool> completeFutures() => Future.wait(
state.allParams.entries.where(isAsyncParam).map(
(param) async {
final doc = await asyncParams[param.key]!(param.value)
.onError((_, __) => null);
if (doc != null) {
futureParamValues[param.key] = doc;
return true;
}
return false;
},
),
).onError((_, __) => [false]).then((v) => v.every((e) => e));
dynamic getParam<T>(
String paramName,
ParamType type, [
bool isList = false,
]) {
if (futureParamValues.containsKey(paramName)) {
return futureParamValues[paramName];
}
if (!state.allParams.containsKey(paramName)) {
return null;
}
final param = state.allParams[paramName];
// Got parameter from `extras`, so just directly return it.
if (param is! String) {
return param;
}
// Return serialized value.
return deserializeParam<T>(
param,
type,
isList,
);
}
}
class FFRoute {
const FFRoute({
required this.name,
required this.path,
required this.builder,
this.requireAuth = false,
this.asyncParams = const {},
this.routes = const [],
});
final String name;
final String path;
final bool requireAuth;
final Map<String, Future<dynamic> Function(String)> asyncParams;
final Widget Function(BuildContext, FFParameters) builder;
final List<GoRoute> routes;
GoRoute toRoute(AppStateNotifier appStateNotifier) => GoRoute(
name: name,
path: path,
pageBuilder: (context, state) {
final ffParams = FFParameters(state, asyncParams);
final page = ffParams.hasFutures
? FutureBuilder(
future: ffParams.completeFutures(),
builder: (context, _) => builder(context, ffParams),
)
: builder(context, ffParams);
final child = page;
final transitionInfo = state.transitionInfo;
return transitionInfo.hasTransition
? CustomTransitionPage(
key: state.pageKey,
child: child,
transitionDuration: transitionInfo.duration,
transitionsBuilder: PageTransition(
type: transitionInfo.transitionType,
duration: transitionInfo.duration,
reverseDuration: transitionInfo.duration,
alignment: transitionInfo.alignment,
child: child,
).transitionsBuilder,
)
: MaterialPage(key: state.pageKey, child: child);
},
routes: routes,
);
}
class TransitionInfo {
const TransitionInfo({
required this.hasTransition,
this.transitionType = PageTransitionType.fade,
this.duration = const Duration(milliseconds: 300),
this.alignment,
});
final bool hasTransition;
final PageTransitionType transitionType;
final Duration duration;
final Alignment? alignment;
static TransitionInfo appDefault() => TransitionInfo(hasTransition: false);
}
This is my MateriaApp widget:
#override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'consulta-beneficios',
localizationsDelegates: [
FFLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
locale: _locale,
supportedLocales: const [Locale('en', '')],
theme: ThemeData(brightness: Brightness.light),
darkTheme: ThemeData(brightness: Brightness.dark),
themeMode: _themeMode,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
);
}
If I use MaterialApp I can solve this issue with the home property. But I dont know how to do it with MaterialApp.router`.

Go_Router Pass Object to new route

I want to pass object from the listview to the sub route. It seems no way to pass the object.
Is there any how to do it?
GoRouter routes(AuthBloc bloc) {
return GoRouter(
navigatorKey: _rootNavigatorKey,
routes: <RouteBase>[
GoRoute(
routes: <RouteBase>[
GoRoute(
path: loginURLPagePath,
builder: (BuildContext context, GoRouterState state) {
return const LoginPage();
},
),
GoRoute(
path: homeURLPagePath,
builder: (BuildContext context, GoRouterState state) =>
const HomePage(),
routes: <RouteBase>[
GoRoute(
path: feeURLPagePath,
name: 'a',
builder: (context, state) => FeePage(),
routes: [
/// Fee Details page
GoRoute(
name: 'b',
path: feeDetailsURLPagePath,
builder: (BuildContext context, GoRouterState state) =>
const FeeDetails(),
),
]),
],
),
],
path: welcomeURLPagePath,
builder: (BuildContext context, GoRouterState state) =>
const SplashPage(),
),
],
refreshListenable: GoRouterRefreshStream(bloc.stream),
debugLogDiagnostics: true,
initialLocation: welcomeURLPagePath,
},
);
}
The error says no initial match found for feeDetails!
Use extra parameter in context.goNamed()
Example:
Object:
class Sample {
String attributeA;
String attributeB;
bool boolValue;
Sample(
{required this.attributeA,
required this.attributeB,
required this.boolValue});}
Define GoRoute as
GoRoute(
path: '/sample',
name: 'sample',
builder: (context, state) {
Sample sample = state.extra as Sample; // -> casting is important
return GoToScreen(object: sample);
},
),
Call it as:
Sample sample = Sample(attributeA: "True",attributeB: "False",boolValue: false)
context.goNamed("sample",extra:sample );
Receive it as:
class GoToScreen extends StatelessWidget {
Sample? object;
GoToScreen({super.key, this.object});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
object.toString(),
style: const TextStyle(fontSize: 24),
)),
);
}
}

Can not navigate with Go Router when implement Navigation Widget in navigatorBuilder function

Description
I am trying to Navigate between screens with a Navigation Menu using go_router plugin. When I click the item in the menu, nothing happens but if I change the URL the screen does change.
Video shows the problem
Expect
Every time I navigate back and forth, both URL and screen change
My Code
app_router.dart
class AppRouter {
AppRouter(this._appBloc);
final AppBloc _appBloc;
GoRouter get router => GoRouter(
routes: pages.values.toList(growable: false),
errorBuilder: (context, state) => ErrorPage(
key: state.pageKey,
),
refreshListenable: GoRouterRefreshStream(_appBloc.stream),
navigatorBuilder: _buildRouterView,
redirect: _redirect,
);
String? _redirect(GoRouterState state) {
final loggedIn = _appBloc.state.status == AppStatus.authenticated;
final name = state.subloc;
final loggingIn = name == '/login' || name == '/';
if (!loggedIn) return loggingIn ? null : '/login';
if (loggingIn) return '/app';
return null;
}
static Map<String, GoRoute> pages = {
route_names.onboard: GoRoute(
name: route_names.onboard,
path: routes[route_names.onboard]!,
pageBuilder: (context, state) => OnboardPage.page(key: state.pageKey),
routes: [
GoRoute(
path: route_names.login.subRoutePath,
name: route_names.login,
pageBuilder: (context, state) => LoginPage.page(key: state.pageKey),
),
GoRoute(
path: route_names.signUp.subRoutePath,
name: route_names.signUp,
pageBuilder: (context, state) => LoginPage.page(key: state.pageKey),
),
],
),
'app': GoRoute(
path: '/app',
// All /app pages get the main scaffold
builder: (context, state) {
return Text("App Main");
},
routes: [
ExplorePage.route,
PlanPage.route,
AccountPage.route,
]),
};
Widget _buildRouterView(BuildContext context, GoRouterState state, Widget child) {
return Builder(
builder: (context) => BlocBuilder<AppBloc, AppState>(builder: (context, appState) {
if (appState.status == AppStatus.unauthenticated) {
return child;
}
return HomePageSkeleton(
child: child,
);
}),
);
}
}
app.dart
class AppView extends StatelessWidget {
// ignore: prefer_const_constructors_in_immutables
AppView({super.key, required AppBloc appBloc}) {
_appBloc = appBloc;
_appRouter = AppRouter(_appBloc);
}
late final AppBloc _appBloc;
late final AppRouter _appRouter;
#override
Widget build(BuildContext context) {
return BlocListener<AppBloc, AppState>(
listener: (context, state) {
if (state == const AppState.unauthenticated()) {
_appRouter.router.goNamed(route_names.login);
}
},
child: MaterialApp.router(
supportedLocales: AppLocalizations.supportedLocales,
routeInformationParser: _appRouter.router.routeInformationParser,
routeInformationProvider: _appRouter.router.routeInformationProvider,
routerDelegate: _appRouter.router.routerDelegate,
),
);
}
}
HomePageSkeleton.class
// inside build method
class HomePageSkeleton extends StatelessWidget with NavigationMixin {
const HomePageSkeleton({Key? key,required this.child}) : super(key: key);
final Widget child;
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Responsive.isMobile(context) ? AppBottomNavigation(index: 0) : const SizedBox(),
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (Responsive.isTablet(context) || Responsive.isLaptop(context))
// It takes 1/6 part of the screen
Expanded(
child: SideMenu(index: 0, onSelected: (index) => onTabSelected(index, context)),
),
Expanded(
// It takes 5/6 part of the screen
flex: 5,
child: child),
],
),
),
);
}
}
//onTapSelected method
void onTabSelected(int index, BuildContext context) {
switch (index) {
case 0:
// context.goNamed(route_names.explore);
context.go('/app/explore');
break;
case 1:
// context.goNamed(route_names.plan);
context.go('/app/plan');
break;
case 2:
// context.goNamed(route_names.account);
context.go('/app/account');
break;
default:
throw Exception('Unknown view');
}
}
I change my class AppRouter into:
class AppRouter {
AppRouter(AppBloc appBloc)
: _router = GoRouter(
routes: getPages().values.toList(growable: false),
errorBuilder: (context, state) => ErrorPage(
key: state.pageKey,
),
refreshListenable: GoRouterRefreshStream(appBloc.stream),
navigatorBuilder: _buildRouterView,
redirect: (GoRouterState state) {
_redirect(state, appBloc);
},
),
_appBloc = appBloc;
final AppBloc _appBloc;
final GoRouter _router;
GoRouter get router {
return _router;
}
... and it worked

How to work with NavigationBar in go_router? | Flutter

I am currently struggling refactoring my routing code with go_router.
I already got some simple routes like /signin & /signup, but the problem comes in when I try to make the routing work with a BottomNavigationBar that has multiple screens. I would like to have a separate route for each of them like /home, /events & /profile.
I figured out that I somehow have to return the same widget with a different parameter to prevent the whole screen to change whenever a BottomNavigationBarItem is pressed and instead only update the part above the BottomNavigationBar which would be the screen itself.
I came up with a pretty tricky solution:
GoRoute(
path: '/:path',
builder: (BuildContext context, GoRouterState state) {
final String path = state.params['path']!;
if (path == 'signin') {
return const SignInScreen();
}
if (path == 'signup') {
return const SignUpScreen();
}
if (path == 'forgot-password') {
return const ForgotPasswordScreen();
}
// Otherwise it has to be the ScreenWithBottomBar
final int index = getIndexFromPath(path);
if (index != -1) {
return MainScreen(selectedIndex: index);
}
return const ErrorScreen();
}
)
This does not look very good and it makes it impossible to add subroutes like /profile/settings/appearance or /events/:id.
I would like to have something easy understandable like this:
GoRoute(
path: '/signin',
builder: (BuildContext context, GoRouterState state) {
return const SignInScreen();
}
),
GoRoute(
path: '/signup',
builder: (BuildContext context, GoRouterState state) {
return const SignUpScreen();
}
),
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) {
return const ScreenWithNavBar(selectedScreen: 1);
}
),
GoRoute(
path: '/events',
builder: (BuildContext context, GoRouterState state) {
return const ScreenWithNavBar(selectedScreen: 2);
},
routes: <GoRoute>[
GoRoute(
path: ':id',
builder: (BuildContext context, GoRouterState state) {
return const EventScreen();
}
)
]
)
Is there any way to achieve the behavior?
This is an outstanding feature request for go_router that I hope to resolve in the coming weeks. Stay tuned.
Now you can use ShellRouter with GoRouter to create Navigation Bar
Explaination:
Things to keep in mind while using context.go() from ShellRoute to GoRoute
Specify parentNavigatorKey prop in each GoRoute
Use context.go() to replace page , context.push() to push page to stack
Code Structure to follow:
final _parentKey = GlobalKey<NavigatorState>();
final _shellKey = GlobalKey<NavigatorState>();
|_ GoRoute
|_ parentNavigatorKey = _parentKey 👈 Specify key here
|_ ShellRoute
|_ GoRoute // Needs Bottom Navigation
|_ parentNavigatorKey = _shellKey
|_ GoRoute // Needs Bottom Navigation
|_ parentNavigatorKey = _shellKey
|_ GoRoute // Full Screen which doesn't need Bottom Navigation
|_parentNavigatorKey = _parentKey
Code:
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: ScaffoldWithNavBar(
location: state.location,
child: child,
));
},
routes: [
GoRoute(
path: '/',
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(child: Text("Home")),
),
);
},
),
GoRoute(
path: '/discover',
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(child: Text("Discover")),
),
);
},
),
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/shop',
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(child: Text("Shop")),
),
);
}),
],
),
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/login',
pageBuilder: (context, state) {
return NoTransitionPage(
key: UniqueKey(),
child: Scaffold(
appBar: AppBar(),
body: const Center(
child: Text("Login"),
),
),
);
},
),
],
);
BottomNavigationBar
class ScaffoldWithNavBar extends StatefulWidget {
String location;
ScaffoldWithNavBar({super.key, required this.child, required this.location});
final Widget child;
#override
State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
}
class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
int _currentIndex = 0;
static const List<MyCustomBottomNavBarItem> tabs = [
MyCustomBottomNavBarItem(
icon: Icon(Icons.home),
activeIcon: Icon(Icons.home),
label: 'HOME',
initialLocation: '/',
),
MyCustomBottomNavBarItem(
icon: Icon(Icons.explore_outlined),
activeIcon: Icon(Icons.explore),
label: 'DISCOVER',
initialLocation: '/discover',
),
MyCustomBottomNavBarItem(
icon: Icon(Icons.storefront_outlined),
activeIcon: Icon(Icons.storefront),
label: 'SHOP',
initialLocation: '/shop',
),
MyCustomBottomNavBarItem(
icon: Icon(Icons.account_circle_outlined),
activeIcon: Icon(Icons.account_circle),
label: 'MY',
initialLocation: '/login',
),
];
#override
Widget build(BuildContext context) {
const labelStyle = TextStyle(fontFamily: 'Roboto');
return Scaffold(
body: SafeArea(child: widget.child),
bottomNavigationBar: BottomNavigationBar(
selectedLabelStyle: labelStyle,
unselectedLabelStyle: labelStyle,
selectedItemColor: const Color(0xFF434343),
selectedFontSize: 12,
unselectedItemColor: const Color(0xFF838383),
showUnselectedLabels: true,
type: BottomNavigationBarType.fixed,
onTap: (int index) {
_goOtherTab(context, index);
},
currentIndex: widget.location == '/'
? 0
: widget.location == '/discover'
? 1
: widget.location == '/shop'
? 2
: 3,
items: tabs,
),
);
}
void _goOtherTab(BuildContext context, int index) {
if (index == _currentIndex) return;
GoRouter router = GoRouter.of(context);
String location = tabs[index].initialLocation;
setState(() {
_currentIndex = index;
});
if (index == 3) {
context.push('/login');
} else {
router.go(location);
}
}
}
class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
final String initialLocation;
const MyCustomBottomNavBarItem(
{required this.initialLocation,
required Widget icon,
String? label,
Widget? activeIcon})
: super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
}
Output:
The solution that worked for me in the end was the following:
I awas able to create a route that specifies which values are allowed:
GoRoute(
path: '/:screen(home|discover|notifications|profile)',
builder: (BuildContext context, GoRouterState state) {
final String screen = state.params['screen']!;
return TabScreen(screen: screen);
}
)
With that done, I pass whatever value the route contains (e.g. '/home' or '/discover') to the TabScreen which then displays the exact same Widget, but without reloading also the TabBar over and over again:
class TabScreen extends StatelessWidget {
const TabScreen({
super.key,
required this.screen
});
final String screen;
#override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(child: screen == 'home' ? const HomeScreen() : screen == 'discover' ? const DiscoverScreen() : screen == 'notifications' ? const NotificationsScreen() : ProfileScreen(),
CustomBottomNavigationBar()
]
);
}
}