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

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);

Related

Flutter: Nested navigation inside a page using go_router

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');

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),
],
)
],
);

Navigation to the screen where I am now

I need to update all the widgets on the screen, for this I decided to just make navigation to the screen I'm currently on. But unfortunately nothing happens. Tell me, what's the problem?
I am using the go_router package.
routes -
final GoRouter _router = GoRouter(
routes: <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) => Splash() ,
),
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) => const Home() ,
),
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) => const Login() ,
),
GoRoute(
path: '/createPinCode',
builder: (BuildContext context, GoRouterState state) => CreatePinCode() ,
),
],
button -
onCompleted: (value) async {
pinCode = int.parse(value);
sharedPrefsSet(pinCode);
context.go('/createPinCode');
},
If you just want to clear your form textFields than rather than re-build entire page you can user clear() property of TextEditingController.
TextEditingController nameController = TextEditingController();
func clearAllData() {
nameController.clear();
emailController.clear();
}
Call this clearAllData() when user tap on submit form.

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);
}
},
);

passing Auth data with go_router

In my project, I implement the Provider method to manage state, and I'd like to share auth provider info with the go router package to keep users logged in
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (ctx) => Auth(),
),
ListenableProxyProvider<Auth, AppRouter>(
update: (_, authObj, prevOrders) =>
AppRouter(authObj)
),
}
and within my AppRouter class I have a constructer to get auth data :
class AppRouter with ChangeNotifier {
final Auth authData;
AppRouter(this.authData);
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
name: root,
path: '/',
builder: (context, state) => TabsScreen(),
// redirect: (state) => state.namedLocation(authScreen),
),
GoRoute(
name: mainScreen,
path: '/main-screen',
builder: (context, state) => HomeScreen(),
),
GoRoute(
name: authscreen,
path: '/auth-screen',
builder: (context, state) => AuthScreen(),
),
],
redirect: (state) {
final loginLoc = state.namedLocation(authScreen);
final loggingIn = state.subloc == loginLoc;
var loggedIn = authData.isLoggedIn;
if (!loggedIn && !loggingIn) return loginLoc;
if (loggedIn && (loggingIn)) return root;
return null;
},
however I can't access authData within my class and I get this error :
The instance member 'authData' can't be accessed in an initializer.
Try replacing the reference to the instance member with a different expression
this article helped me solve my problem and Also there is complete guide about implementing Navigation 2.0 using Go_Router :
Flutter Navigator 2.0: Using go_router
This is normal. Even if you're thinking:"The redirect will happen when I'll have my authData assigned" Dart don't see it like this. For dart, you're trying to use the value of authData in the moment of the creation of the router, so, it is not assigned. For example, you could do this if authData was a static field and it was already assigned.
You can probably achieve what you're trying to achieve by reading this:
https://gorouter.dev/parameters
EDIT:
Here is a code snippet of what i would do:
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
name: root,
path: '/',
builder: (context, state) => TabsScreen(),
),
GoRoute(
name: mainScreen,
path: '/main-screen',
builder: (context, state) => HomeScreen(),
),
GoRoute(
name: authscreen,
path: '/auth-screen',
builder: (context, state) => AuthScreen(),
),
],
redirect: (state) {
final Auth data=state.extra! as Auth;
final loginLoc = state.namedLocation(authScreen);
final loggingIn = state.subloc == loginLoc;
var loggedIn = data.isLoggedIn;
if (!loggedIn && !loggingIn) return loginLoc;
if (loggedIn && loggingIn) return root;
return null;
},
);
Note: Remember to always pass the state like this:
context.go('/route', extra:authData);