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()
]
);
}
}
Related
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.
.
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.
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),
)),
);
}
}
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
I have implemented an app which navigate through few screens and the main point is a BottomNavigationBar with four tabs. Inside the second tab. I have a ModalBottomSheet that has a button option to logout. The idea is to navigate back to Login page after logout button clicked.
well it does navigate back to login screen but it takes the BottomNavigationBar with it.
I will provide my navigation stack view and the bottom navigation bar main page.
Login to the BottomNavigationBar Page
Navigator.of(context).push(PageTransition(
child: ProHomeScreen(),
type: PageTransitionType.rightToLeft,
));
The BottomNavigationBar main page
class ProHomeScreen extends StatefulWidget {
const ProHomeScreen({Key key}) : super(key: key);
static String tag = 'pro-home-page';
#override
_ProHomeScreenState createState() => _ProHomeScreenState();
}
class _ProHomeScreenState extends State<ProHomeScreen>with SingleTickerProviderStateMixin {
int _selectedIndex = 0;
List<int> _history = [0];
GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
TabController _tabController;
List<Widget> mainTabs;
List<BuildContext> navStack = [null, null, null, null];
//
#override
void initState() {
_tabController = TabController(vsync: this, length: 4);
mainTabs = <Widget>[
Navigator(
onGenerateRoute: (RouteSettings settings){
return PageRouteBuilder(pageBuilder: (context, animiX, animiY) { // use page PageRouteBuilder instead of 'PageRouteBuilder' to avoid material route animation
navStack[0] = context;
return DailyGuideScreen();
});
}),
Navigator(
onGenerateRoute: (RouteSettings settings){
return PageRouteBuilder(pageBuilder: (context, animiX, animiY) { // use page PageRouteBuilder instead of 'PageRouteBuilder' to avoid material route animation
navStack[1] = context;
return UserProfileViewScreen();
});
}),
Navigator(
onGenerateRoute: (RouteSettings settings){
return PageRouteBuilder(pageBuilder: (context, animiX, animiY) { // use page PageRouteBuilder instead of 'PageRouteBuilder' to avoid material route animation
navStack[2] = context;
return PetProfileViewScreen();
});
}),
Navigator(
onGenerateRoute: (RouteSettings settings){
return PageRouteBuilder(pageBuilder: (context, animiX, animiY) { // use page PageRouteBuilder instead of 'PageRouteBuilder' to avoid material route animation
navStack[3] = context;
return AllMealPlanPage();
});
}),
];
super.initState();
}
//
final List<BottomNavigationBarRootItem> bottomNavigationBarRootItems = [
BottomNavigationBarRootItem(
bottomNavigationBarItem: BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text(''),
),
),
BottomNavigationBarRootItem(
bottomNavigationBarItem: BottomNavigationBarItem(
icon: Icon(Icons.person),
title: Text(''),
),
),
BottomNavigationBarRootItem(
bottomNavigationBarItem: BottomNavigationBarItem(
icon: Icon(Icons.pets),
title: Text(''),
),
),
BottomNavigationBarRootItem(
bottomNavigationBarItem: BottomNavigationBarItem(
icon: Icon(Icons.fastfood),
title: Text(''),
),
),
];
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
body: TabBarView(
controller: _tabController,
physics: NeverScrollableScrollPhysics(),
children: mainTabs,
),
bottomNavigationBar: BottomNavigationBar(
items: bottomNavigationBarRootItems.map((e) => e.bottomNavigationBarItem).toList(),
currentIndex: _selectedIndex,
selectedItemColor: Color(0xFF03B898),
unselectedItemColor: Color(0xFF01816B),
onTap: _onItemTapped,
// indicator: UnderlineTabIndicator(
// borderSide: BorderSide(color: lightGreen, width: 5.0),
// insets: EdgeInsets.only(bottom: 44.0),
// ),
),
),
onWillPop: () async{
if (Navigator.of(navStack[_tabController.index]).canPop()) {
Navigator.of(navStack[_tabController.index]).pop();
setState((){ _selectedIndex = _tabController.index; });
return false;
}else{
if(_tabController.index == 0){
setState((){ _selectedIndex = _tabController.index; });
SystemChannels.platform.invokeMethod('SystemNavigator.pop'); // close the app
return true;
}else{
_tabController.index = 0; // back to first tap if current tab history stack is empty
setState((){ _selectedIndex = _tabController.index; });
return false;
}
}
},
);
}
void _onItemTapped(int index) {
_tabController.index = index;
setState(() => _selectedIndex = index);
}
}
////
class BottomNavigationBarRootItem {
final String routeName;
final NestedNavigator nestedNavigator;
final BottomNavigationBarItem bottomNavigationBarItem;
BottomNavigationBarRootItem({
#required this.routeName,
#required this.nestedNavigator,
#required this.bottomNavigationBarItem,
});
}
abstract class NestedNavigator extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey;
NestedNavigator({Key key, #required this.navigatorKey}) : super(key: key);
}
class HomeNavigator extends NestedNavigator {
HomeNavigator({Key key, #required GlobalKey<NavigatorState> navigatorKey})
: super(
key: key,
navigatorKey: navigatorKey,
);
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case '/':
builder = (BuildContext context) => AllMealPlanPage();
break;
case '/home/1':
builder = (BuildContext context) => MealPlanDetailsScreen();
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
);
}
}
////
Second Tab The Logout option Navigation(which is inside a ModalBottomSheet)
_userProfileBtmSheet() {
showModalBottomSheet(
...
).whenComplete(() {
if(goLogOut){
_isLoading ? null : _handleLogout(context);
}
});
}
////
Within _handleLogout()
Navigator.pop(
context, new MaterialPageRoute(builder: (context) => LoginScreen()));
I need to navigate out from the BottomNavigationBar page and replace the screen with the login screen.
https://github.com/jasonwaku/jason_pawfect/tree/master
Instead of using
Navigator.pop(
context, new MaterialPageRoute(builder: (context) => LoginScreen()));
Try using
Navigator.of(context).pushReplacment(
context, new MaterialPageRoute(builder: (context) => LoginScreen()));
I faced the same issue today where I have to go to login page from one of the tab through logout button, and bottom navigation bar also passed along, what i have done is used bloc state management and navigate from navigation bar page according to the state generated by authentication bloc.
logout button:
await BlocProvider.of<AuthenticationCubit>(context).logout();
authentication cubit:
Future<void> logout() async {
emit(AuthenticationLoadingState());
await userRepository.logout();
emit(UnAutheticatedState());
}
And listen to the Unauthenticated state in bloc listner widget in the home page of navigation bar
Scaffold(
body: BlocListener<AuthenticationCubit, AuthenticationState>(
listener: (context, state) {
if (state is UnautheticatedState) {
Navigator.pushAndRemoveUntil<dynamic>(
context,
MaterialPageRoute<dynamic>(
builder: (BuildContext context) => Login(),
),
(route) =>
false, //if you want to disable back feature set to false
);
}
},