Replies: 13 comments 29 replies
-
i'm interested in this.. |
Beta Was this translation helpful? Give feedback.
-
@lucavenir Why do you need more than one routers for MaterialApp. It seems very strange. |
Beta Was this translation helpful? Give feedback.
-
Maybe off-topic information:
|
Beta Was this translation helpful? Give feedback.
-
i'm also interesed about the best practices for riverpod and go router or any kind of router. i have a very structured flow: But i want user to be able to navigate directly (deeplinking or web) to '/server/a/library/1/medias' what i do for now is that i have a selectedServerId statenotifier and a FutureProvider that depends (whatch) this selectedSeverId to load server data. Same for library (selectedLibraryId and a Library FutureProvider) for now what i do is that for every page that is under server in the initState i set selectedServerId from the path parameter. Same for page under library where i set both serverSelectedId and LibrarySelectedId. That does not look very elegant and is error prone! |
Beta Was this translation helpful? Give feedback.
-
@lucavenir Hi, is there a way to avoid the behaviour of the So the user will always get the login screen first, if the user is logged out then that's fine, but if he's already logged in then he will get the |
Beta Was this translation helpful? Give feedback.
-
@lucavenir I looked into your examples. I personally don't like ChangeNotifier. So i take the example no. 3 and modify it a bit by implementing my own listenable. It is working. class RouteRefreshListenable extends Listenable {
VoidCallback? _listener;
@override
void addListener(VoidCallback listener) {
_listener = listener;
}
@override
void removeListener(VoidCallback listener) {
_listener = null;
}
void notify() {
_listener?.call();
}
}
// inside provider
final listenable = RouteRefreshListenable();
ref.listen<User?>(userProvider, (oldUser, next) {
listenable.notify();
}); |
Beta Was this translation helpful? Give feedback.
-
Any news on this topic? I'm trying to do a go_ruter integration in the best way and the repository with the example (go router-riverpod-firebase) is not to my liking, since the entire root widget is rebuilt. |
Beta Was this translation helpful? Give feedback.
-
Thanks for the answer, I really couldn't understand your code, I have tried to summarize mine, and it seems better to me than the example from the @lucavenir repository, if anyone has any comments or opinions they are welcome, this is an extract of my code and The best thing is that neither the tree nor the route is reconstructed.
); |
Beta Was this translation helpful? Give feedback.
-
I know this is not really likeable workaround, very straightforward and maybe subject to issues but I'll post it anyway if it can help: In our project we created an AppContainer class: import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Singleton holding riverpod [ProviderContainer] instance.
///
/// You can read the [ProviderContainer] instance with `AppContainer.instance`.
/// ```dart
/// AppContainer.instance.read(someProvider)
/// ```
///
/// This class could be rewrite to hold some other container objects instance if you need to use them outside Flutter.
/// Be careful about why you are using [AppContainer].
class AppContainer {
AppContainer._({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) : _providerContainer = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);
static AppContainer? _singleton;
static ProviderContainer get instance => _singleton!._providerContainer;
final ProviderContainer _providerContainer;
factory AppContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
_singleton = AppContainer._(
parent: parent,
overrides: overrides,
observers: observers,
);
return _singleton!;
}
} Which will hold the ProviderContainer instance. Be careful that you will need to instantiate it before running app somehow (bootstraping phase) and provide the instance to the runApp function with UncontrolledProviderScope. runApp(UncontrolledProviderScope(
container: AppContainer.instance,
child: UltrashotApp(),
)); Then we are able to do this for example : GoRoute(
path: AppRoutes.login.path,
name: AppRoutes.login.name,
builder: (context, state) {
if (state.extra is RedirectionAfterLogin) {
AppContainer.instance.read(storageProvider).write(
'redirectionAfterLogin',
jsonEncode(
(state.extra as RedirectionAfterLogin).toJson(),
),
);
}
return const LoginScreen();
},
routes: [
GoRoute(
path: AppRoutes.registerProfile.path,
name: AppRoutes.registerProfile.name,
builder: (context, state) => const RegisterProfileScreen(),
),
],
), We are not facing any issue with this way, but we try to use it with a lot of care and only if we don't find any solution in a proper way. |
Beta Was this translation helpful? Give feedback.
-
I also have doubts about the clear use of Riverpod and GoRouter. I think they are both powerful and if adjusted via recipe, by the wisest, it would help a lot to grow both. import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'routes.dart';
class App extends HookConsumerWidget {
const App({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifier = ref.watch(routerListenableProvider.notifier);
final key = useRef(GlobalKey<NavigatorState>(debugLabel: 'routerKey'));
final router = useMemoized(
() => GoRouter(
navigatorKey: key.value,
refreshListenable: notifier,
initialLocation: AppPage.splash.path,
debugLogDiagnostics: true,
routes: routes,
redirect: notifier.redirect,
),
[notifier],
);
return MaterialApp.router(
routerConfig: router,
title: 'Fluxus 4',
theme: ThemeData.dark(useMaterial3: true),
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('pt', 'BR'),
],
);
}
} part 'routes.g.dart';
@riverpod
class RouterListenable extends _$RouterListenable implements Listenable {
VoidCallback? _routerListener;
bool _isAuth = false; // Useful for our global redirect function
@override
Future<void> build() async {
final user = ref.watch(authUserProvider).asData?.value;
if (user == null) {
_isAuth = false;
} else {
_isAuth = true;
}
ref.listenSelf((_, __) {
// One could write more conditional logic for when to call redirection
if (state.isLoading) return;
_routerListener?.call();
});
}
/// Redirects the user when our authentication changes
String? redirect(BuildContext context, GoRouterState state) {
// log(' redirect state');
// log('_isAuth: $_isAuth');
// log('state.fullPath: ${state.fullPath}');
// log('state.location: ${state.location}');
// log('state.matchedLocation: ${state.matchedLocation}');
// log('state.name: ${state.name}');
// log('state.pageKey: ${state.pageKey}');
// log('state.extra.toString(): ${state.extra.toString()}');
// log('state.path: ${state.path}');
// log('state.pathParameters: ${state.pathParameters}');
// log('state.queryParameters: ${state.queryParameters}');
// log('state.queryParametersAll: ${state.queryParametersAll}');
// log('--- redirect state');
if (this.state.isLoading || this.state.hasError) return null;
// final isAuthUnderLogin =
// _isAuth && state.location.startsWith(AppPage.login.path);
// if (isAuthUnderLogin) {
// log('return if isAuthUnderLogin: AppPage.home.path');
// return AppPage.home.path;
// }
final isAuthUnderLogin = _isAuth &&
state.location != AppPage.login.path &&
state.location.startsWith(AppPage.login.path);
if (isAuthUnderLogin) {
//log('return if isAuthUnderLogin: AppPage.login.path');
return AppPage.login.path;
}
final isPathUnderLogin =
!_isAuth && state.location.startsWith(AppPage.login.path);
if (isPathUnderLogin) {
//log('return if isPathUnderLogin: null');
return null;
}
final isSplash = state.location == AppPage.splash.path;
if (isSplash) {
//log('return if isSplash: ${_isAuth ? AppPage.home.path : AppPage.login.path}');
return _isAuth ? AppPage.home.path : AppPage.login.path;
}
final isLoggingIn = state.location == AppPage.login.path;
if (isLoggingIn) {
//log('return if isLoggingIn: ${_isAuth ? AppPage.home.path : null}');
return _isAuth ? AppPage.home.path : null;
}
//log('return final: ${_isAuth ? null : AppPage.splash.path}');
return _isAuth ? null : AppPage.splash.path;
}
@override
void addListener(VoidCallback listener) {
_routerListener = listener;
}
@override
void removeListener(VoidCallback listener) {
_routerListener = null;
}
}
List<RouteBase> routes = [
GoRoute(
path: AppPage.splash.path,
name: AppPage.splash.name,
builder: (context, state) => SplashPage(
key: state.pageKey,
),
),
GoRoute(
path: AppPage.login.path,
name: AppPage.login.name,
builder: (context, state) => UserLoginPage(
key: state.pageKey,
),
routes: [
GoRoute(
path: AppPage.emailRegister.path,
name: AppPage.emailRegister.name,
builder: (context, state) {
final registerParams = state.extra as EmailParams?;
if (registerParams == null) {
throw 'Missing `VerificationPageParams` object';
}
return UserEmailRegisterPage(
registerParams: registerParams,
key: state.pageKey,
);
},
),
... enum AppPage {
...
// Basic Pages
splash('/', 'splash'),
login('/login', 'login'),
emailRegister('emailRegister', 'emailRegister'),
emailResetPassword('emailResetPassword', 'emailResetPassword'),
home('/home', 'home'),
userProfileEdit('userProfileEdit', 'userProfileEdit');
final String path;
final String name;
const AppPage(this.path, this.name);
} |
Beta Was this translation helpful? Give feedback.
-
Does anyone know a good way to use a route's params to initialize the Riverpod state? I would like to do the following: final args = SomeArguments(id: 6);
context.go(someRoute, extra: args); Then in the GoRouter I have the following: GoRoute(
path: someRoute,
builder: (context, state) {
final args = state.extra as SomeArguments;
return SomeScreen(id: args.id);
},
) The problem with this is, the state in my app is very large and complex, and there's a lot of providers used in many widgets that use this id param. Therefore, it's very cumbersome to make a bunch of family providers and pass this parameter everywhere where it's needed. You end up having to pass it as a family param to totally unrelated providers only because that provider relies on another provider that relies on another provider that needs the param. Instead, it would be nice if I could just have a provider for the id. However, if I do this then where do I initialize this provider? I could do it in Another way is to use provider overrides like so: GoRoute(
path: someRoute,
builder: (context, state) {
final args = state.extra as SomeArguments;
return ProviderScope(
overrides: [
idProvider.overrideWith((ref) => args.id),
],
child: const SomeScreen(),
);
},
); This however creates more problems since every provider that relies on I've also tried the following: ref.read(idProvider.notifier).setId(6);
context.go(someRoute); However, if the go router blocks navigation or redirects to a different page, now the One simplification I noticed in my app is that a route will never appear on the navigation stack twice. Therefore, I can treat |
Beta Was this translation helpful? Give feedback.
-
I wasn't satisfied with anything I've seen with making a router provider and all the extra stuff required to make that work well. Since the redirect method takes a A big upside to this approach is that whenever my authUser changes, the InheritedWidget causes it's children that are using I created an issue here with some example code, and I would love feedback lucavenir/go_router_riverpod#36. |
Beta Was this translation helpful? Give feedback.
-
I created an Here’s the code I’m using: class AuthRefreshListenable extends ChangeNotifier {
late final ProviderSubscription<AsyncValue<AuthUser?>> _subscription;
AuthRefreshListenable(Ref ref) {
// Listen to AuthNotifier's state changes
_subscription = ref.listen<AsyncValue<AuthUser?>>(
authNotifierProvider,
(_, __) {
notifyListeners();
},
);
}
@override
void dispose() {
_subscription.close();
super.dispose();
}
}
@Riverpod(keepAlive: true)
Raw<AuthRefreshListenable> authRefreshListenable(Ref ref) {
final listenable = AuthRefreshListenable(ref);
ref.onDispose(() => listenable.dispose());
return listenable;
}
// router class
final _rootNavigatorKey = GlobalKey<NavigatorState>();
@riverpod
GoRouter goRouter(Ref ref) {
final refreshListenable = ref.watch(authRefreshListenableProvider);
return GoRouter(
initialLocation: '/login',
navigatorKey: _rootNavigatorKey,
debugLogDiagnostics: true,
refreshListenable: refreshListenable,
redirect: (context, state) {
final path = state.uri.path;
final authState = ref.watch(authNotifierProvider);
final isAuthenticated = authState.maybeWhen(
data: (user) => validateToken(user?.accessToken),
orElse: () => false,
);
if (isAuthenticated) {
if (path.startsWith('/login')) {
return '/home';
}
} else {
if (path.startsWith('/startup') ||
_protectedRoutes.any((p) => path.startsWith(p))) {
return '/login';
}
}
return null;
},
routes: routes,
);
}
@Riverpod(keepAlive: true)
class AuthNotifier extends _$AuthNotifier {
@override
FutureOr<AuthUser?> build() async {
final storageService = ref.watch(storageServiceProvider).requireValue;
return storageService.user;
}
Future<void> login({required String email, required String password}) async {
state = const AsyncLoading();
final authRepository = ref.read(authRepositoryProvider);
final authUser = await authRepository.login(email: email, password: password);
final storageService = ref.read(storageServiceProvider).requireValue;
await storageService.setUser(authUser);
state = AsyncData(authUser);
}
} I’d love to get some feedback on:
|
Beta Was this translation helpful? Give feedback.
-
Hi there,
I think this discussion could be useful to improve these examples.
As stated in this comment of PR #1341, there could be several strategies when integrating GoRouter with Riverpod.
I'd like to start this discussion to gather some intel from the community about this matter. The objective is to leave a "best practice" to users on such topic.
Any kind of feedback or request would be useful to improve or find proper alternatives to this.
EDIT. The following content is outdated. Please take the following statements with a grain of salt, as there's better examples on the repo.
At the moment I personally like very much the approach I showed in the example above (
ref.listen
ChangeNotifier
), because:ref.listen
directive;ref.listen
's callback when needed;notifyListeners()
, we can avoid the use of a (or many)stream
and itsdispose
and we can avoid theValueNotifier
jargon. Also,notifyListeners
make the routing decisions more flexible.I'd like to know what you guys would do!
Beta Was this translation helpful? Give feedback.
All reactions