Flutter: Nested navigation inside a page using go_router - flutter

In my Flutter app, I use go_router to navigate between pages.
Here are the current pages in my app:
accounts_page
add_account_page
import_accounts_page
Now, I would like to implement nested navigation inside add_account_page, so I can add a new account using multiple steps, let's say:
account_info_step
account_type_step
account_detail_step
Here is what I tried:
final _navigatorKey = GlobalKey<NavigatorState>();
final _addAccountNavigatorKey = GlobalKey<NavigatorState>();
late final router = GoRouter(
navigatorKey: _navigatorKey,
initialLocation: "/accounts_page",
routes: [
ShellRoute(
navigatorKey: _addAccountNavigatorKey,
builder: (context, state, child) => AddAccountPage(child: child),
routes: [
GoRoute(
parentNavigatorKey: _addAccountNavigatorKey,
name: "account_info_step",
path: "/account_info_step",
builder: (context, state) => const AccountInfoStep(),
),
GoRoute(
parentNavigatorKey: _addAccountNavigatorKey,
name: "account_type_step",
path: "/account_type_step",
builder: (context, state) => const AccountTypeStep(),
),
GoRoute(
parentNavigatorKey: _addAccountNavigatorKey,
name: "account_detail_step",
path: "/account_detail_step",
builder: (context, state) => const AccountDetailStep(),
),
],
),
GoRoute(
name: "accounts_page",
path: "/accounts_page",
pageBuilder: (context, state) => const AccountsPage(),
),
GoRoute(
name: "import_accounts_page",
path: "/import_accounts_page",
pageBuilder: (context, state) => const ImportAccountsPage(),
),
],
);
And then I call context.pushNamed("account_info_step"), but nothing happens.
Is it possible then to use go_router to implement nested navigation inside add_account_page and if yes, how?
Thanks.

If your intention is to get to AccountInfoStep() full path leading to this builder should be added in your case:
context.push("/accounts_page/account_info_step")
I assume you are missing name for your top routes to represent proper nesting to use the context.pushNamed()

I think the way you are trying to create nested navigation is not right. Also you don't need to use ShellRoute, you can use shell route if you want to create something like a persistent bottom navigation bar and changing only the child while keeping the bottom nav bar at the bottom.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:untitled/homepage.dart';
import 'package:untitled/test_page.dart';
final _navigatorKey = GlobalKey<NavigatorState>();
// final _addAccountNavigatorKey = GlobalKey<NavigatorState>();
class MyRouter{
static final router = GoRouter(
navigatorKey: _navigatorKey,
initialLocation: "/accounts_page",
routes: [
GoRoute(
name: "accounts_page",
path: "/accounts_page",
builder: (context, state) => const MyHomePage(),
routes: <RouteBase>[
GoRoute(
name: "account_info_step",
path: "account_info_step",
builder: (context, state) => const TestPage(),
),
GoRoute(
name: "account_type_step",
path: "account_type_step",
builder: (context, state) => const TestPage(),
),
GoRoute(
name: "account_detail_step",
path: "account_detail_step",
builder: (context, state) => const TestPage(),
),
]
),
],
);
}
I think this is what you are looking for. Also, note that you don't need to add a '/' in path for nested screens.
And to navigate to those nested screens, you can do something
like this:-
context.go('/accounts_page/account_info_step');

Related

Flutter go_router parameters within ShellRoute not working

I am using go_router and need have setup a ShellRoute. It is working fine so far. Now one of the routes inside that ShellRoute should have a route with params. And I can not make it work..
This is my setup:
final rootNavigatorKey = GlobalKey<NavigatorState>();
class AppRouter {
static final _shellNavigatorKey = GlobalKey<NavigatorState>();
static final router = GoRouter(
initialLocation: IntroView.path,
debugLogDiagnostics: true,
navigatorKey: rootNavigatorKey,
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
pageBuilder: (context, state, child) {
return NoTransitionPage(
child: ScaffoldView(
initLocation: state.location,
child: child,
),
);
},
routes: [
GoRoute(
name: ProjectsView.name,
path: ProjectsView.path,
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) {
return const FadePageTransition(
page: ProjectsView(),
);
},
routes: [
GoRoute(
parentNavigatorKey: rootNavigatorKey,
path: 'projects/:projectTitle',
pageBuilder: (context, state) {
return FadePageTransition(
page: ProjectDetailScaffold(
project: Projects.values.byName(
state.params['projectTitle']!,
),
),
);
},
),
],
),
...
And I am trying to push to ProjectDetailScaffold like this:
rootNavigatorKey.currentContext!.push(
'/projects/wishlists',
);
But I get an error that no route exists for /projects/wishlists...
What am I missing here?
Let me know if you need any more info.
I figured it out by myself:
The problem was that I specified path of the subRoute wrong. I added it's parent path as well when I only need to add :projectTitle. Here is the working solution:
GoRoute(
parentNavigatorKey: rootNavigatorKey,
path: ':projectTitle',
pageBuilder: (context, state) {
return FadePageTransition(
page: ProjectDetailScaffold(
project: Projects.values.byName(
state.params['projectTitle']!,
),
),
);
},
),

How can i get Uri params in Flutter Mobile when open with Deeplink

I configured Navigator through GoRouter and I want to know how to receive a parameter through a deep link.
Instead of configuring arguments in Widget corresponding to the destination,
For iOS simulators, on the connected terminal,
xcrun simctl openurl booted "customScheme://myHost.name.com/login?p=0"
If I enter , I want to enter the login screen and use the parameter p=0 in Login Widget at the same time. Is there any other way?
I considered using uni_links, but I do not want to use it because it interferes with Flutter's basic Deeplinking activities.
(The reason is that if you run the command from the terminal as shown above, if you have uni_links, you will only move to the root and not reach the sub-destination.)
Below is the routerConfig of GoRouter.
final _router = GoRouter(
initialLocation: '/home',
navigatorKey: _rootNavigatorKey,
observers: [
GoRouterObserver(),
routeObserver
],
routes: [
GoRoute(
path: "/",
builder: (context, state) {
return const Root();
},
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(
path: 'home',
builder: (context, state) {
return const Home();
},
),
GoRoute(
path: 'discover',
builder: (context, state) {
return const Discover();
},
),
GoRoute(
path: 'shop',
builder: (context, state) {
return const Shop();
}),
],
),
GoRoute(
path: 'login',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) {
return const Login();
},
routes: loginRouter),
],
)
],
);

How to get route history and queryParameters

My goRouter like:
GoRouter(initialLocation: '/', routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
routes: [
GoRoute(
path: 'page1',
name: 'page1',
builder: (context, state) => Page1Screen(),
),
GoRoute(
path: 'page2',
name: 'page2',
builder: (context, state) => Page2Screen(),
),
GoRoute(
path: 'page3',
name: 'page3',
builder: (context, state) => Page3Screen(),
),
],
),
]);
: HomeScreen use go to page1.
: Page1Screen use push to page2
: Page2Screen use push to page3
: page3 wants use go to return page1
In go_router 5.1.10 version i can use this code get route history to return page1.
final matches = goRouter.routerDelegate.currentConfiguration.matches;
final route = matches.lastWhereOrNull((value) => value.fullpath.contains('page1'));
context.go(goRouter.namedLocation(kRouteHotel, params: route.encodedParams, queryParams: route.queryParams));
but 5.2.0 has remove encodedParams and queryParams.
How should I do to have the same function?

Flutter: how to listen to GoRouter's route / location changes in a Riverpod provider?

I am developing a Flutter application with go_router and riverpod for navigation and state management respectively. The app has a widget which displays a live camera feed, and I'd like to "switch it off" and free the camera when other pages are stacked on top of it.
Here's a sample of the GoRouter code WITHOUT such logic.
GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => CameraWidget(),
routes: [
GoRoute(
path: 'page1',
builder: (context, state) => Page1Screen(),
),
],
),
],
)
My first attempt has been to put some logic in the GoRoute builder:
GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) {
if (state.location == "/") {
return CameraWidget();
}
return Center(child: Text("Camera not visible");
},
routes: [
GoRoute(
path: 'page1',
builder: (context, state) => Page1Screen(),
),
],
),
],
)
But this apparently does not work as the builder is not called again when going from "/" to "/page1".
I then thought of using a riverpod StateProvider to hold a camera "on/off" state, to be manipulated by GoRouter. This is what I tried:
GoRouter(
routes: [
GoRoute(
path: '/',
redirect: (context, state) {
final cameraStateNotifier = ref.read(cameraStateNotifierProvider.notifier);
if (state.location == "/") {
cameraStateNotifier.state = true;
} else {
cameraStateNotifier.state = false;
}
return null;
},
builder: (context, state) => CameraWidget(),
routes: [
GoRoute(
path: 'page1',
builder: (context, state) => Page1Screen(),
),
],
),
],
)
But this also does not work as apparently redirect gets called while rebuilding the widget tree, and it is forbidden to change a provider state while that happens.
Has anyone encountered the same issue before? How can I have a provider listen to GoRouter's location changes?
EDIT: This approach doesn't seem to work with Navigator.pop() calls and back button presses. Check out the currently accepted answer for a better solution.
I believe I found a good way to do so. I defined a provider for GoRouter first, then a second one to listen to router.routeInformationProvider. This is a ChangeNotifier which notifies everytime the route information changes. Finally we can listen to this through a third provider for the specific location.
I think this is a good workaround, even though requires importing src/information_provider.dart from the GoRouter package which is not meant to.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/information_provider.dart';
final routerProvider = Provider<GoRouter>((ref) => GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => CameraWidget(),
routes: [
GoRoute(
path: 'page1',
builder: (context, state) => Page1Screen(),
),
],
),
],
));
final routeInformationProvider = ChangeNotifierProvider<GoRouteInformationProvider>((ref) {
final router = ref.watch(routerProvider);
return router.routeInformationProvider;
});
final currentRouteProvider = Provider((ref) {
return ref.watch(routeInformationProvider).value.location;
});
After further testing of my previous answer, I found that my approach with go_router does not work on Navigator.pop() calls or back button presses. After some more digging in go_router's code, I figured it'd be easier to switch to the Routemaster package, which seems to integrate much better with Riverpod. So far I am very happy with the change.
EDIT: Improved approach now using Routemaster's observable API.
Here's the code:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:routemaster/routemaster.dart';
class RouteObserver extends RoutemasterObserver {
final ProviderRef _ref;
MyObserver(this._ref);
#override
void didChangeRoute(RouteData routeData, Page page) {
_ref.invalidate(locationProvider);
}
}
final routerProvider = Provider((ref) => RoutemasterDelegate(
routesBuilder: (context) => RouteMap(routes: {
'/': (_) => MaterialPage(child: CameraWidget()),
'/page1': (_) => MaterialPage(child: Page1Screen()),
}),
observers: [RouteObserver(ref)],
));
final locationProvider = Provider((ref) => ref.read(routerProvider).currentConfiguration?.fullPath);

Line coverage Unit Test in flutter with GoRouter

I am beginner in testing in flutter,
How to implement a test in this class
This is the code
import 'package:go_router/go_router.dart';
import 'package:moxify_app/src/features/features.dart';
final router = GoRouter(
debugLogDiagnostics: true,
routes: [
GoRoute(
name: 'home',
path: '/home',
builder: (context, state) => const HomePage(),
),
GoRoute(
name: 'login',
path: '/',
builder: (context, state) => const LoginPage(),
),
],
);
I happen to came across this when doing unit tests. To cover that edge case, iterate through your routes and check if the route pagebuilder property is not null.
test(
'check if routes are valid',
() async {
for (var route in goRoutes) {
expect(route.name, isNotNull);
expect(route.path, isNotNull);
expect(route.pageBuilder, isNotNull);
}
},
);