Closed petodavid closed 1 year ago
If you want to control redirections using Bloc, I would do it somehow like this: (demo from my side project; not using go_router, but it should be a good reference)
https://github.com/kingkareldev/client/blob/main/packages/presentation/lib/router/blocs/router/router_bloc.dart https://github.com/kingkareldev/client/blob/main/packages/presentation/lib/router/app_router_delegate.dart#L225
However, using go_router you can just simply call push/go methods inside your widgets. Go router will manage the state for you. You don't have to use another layer above that.
Do some constants for your paths and routeInfos and it should be ok.
@chunhtai said that "context.read() will register the dependency to the router, so that it will trigger redirect if that dependency changes. So this is exactly the right way to handle declarative redirect." And this sounds amazing, however I can not find a way to do it like that. I tried it like this but in this case if the dependency (Bloc state) changes the redirection is not triggered.
GoRoute(
name: LoginPage.name,
path: '/login',
builder: (context, state) => LoginPage(),
redirect: (context, state) {
final state = context.read<AuthBloc>().state;
if(state is Something) return '/something'
if(state is SomethingElse) return '/somethingElse'
},
),
My question is, am I completely misunderstanding something or should this work?
@chunhtai said that "context.read() will register the dependency to the router, so that it will trigger redirect if that dependency changes. So this is exactly the right way to handle declarative redirect." And this sounds amazing, however I can not find a way to do it like that. I tried it like this but in this case if the dependency (Bloc state) changes the redirection is not triggered.
GoRoute( name: LoginPage.name, path: '/login', builder: (context, state) => LoginPage(), redirect: (context, state) { final state = context.read<AuthBloc>().state; if(state is Something) return '/something' if(state is SomethingElse) return '/somethingElse' }, ),
My question is, am I completely misunderstanding something or should this work?
context.read only reads the current state and doesn’t mark the widget as a dependent. I believe you’re thinking of context.watch instead.
@felangel as you can see the redirect function is not a widget, it returns a FutureOr<String>
, are you sure that we can use context.watch here to achieve route redirection? I also receive the following assertion in case of a navigation like context.goNamed()
======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for GoRouteInformationProvider:
Tried to listen to a value exposed with provider, from outside of the widget tree.
This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing `listen: false`.
To fix, write:
Provider.of<TestCubit>(context, listen: false);
It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn't care about the value.
The context used was: Router<Object>(dependencies: [UnmanagedRestorationScope, _InheritedProviderScope<OnBoardingCubit?>, _InheritedProviderScope<TestCubit?>], state: _RouterState<Object>#8ec86)
'package:provider/src/provider.dart':
Failed assertion: line 274 pos 7: 'context.owner!.debugBuilding ||
listen == false ||
debugIsInInheritedProviderUpdate'
Here are my router:
final router = GoRouter(
navigatorKey: navigatorKey,
routes: [
GoRoute(
name: OnBoardingPage.name,
path: '/',
builder: (context, state) => OnBoardingPage(),
redirect: (context, state) {
final redirectLocation = '/login';
if (kIsWeb) return redirectLocation;
final onBoardingCubit = context.watch<OnBoardingCubit>();
final onBoardingCubitState = onBoardingCubit.state;
if (onBoardingCubitState is DoNotShowOnBoarding) return redirectLocation;
return null;
},
),
GoRoute(
name: LoginPage.name,
path: '/login',
builder: (context, state) => LoginPage(),
),
GoRoute(
name: SignupPage.name,
path: '/signup',
builder: (context, state) => SignupPage(),
),
],
redirect: (context, state) {
final testCubit = context.watch<TestCubit>();
final testCubitState = testCubit.state;
if (testCubitState is TestLogin) return '/login';
if (testCubitState is TestSignup) return '/signup';
return null;
},
);
What would be the best way to somehow implement a reactive redirection using bloc states? Currently I'm doing redirection like this:
Is flowbuilder considered here?
Sorry I was wrong, using watch
should be correct since it will add dependency.
The error looks like the watch is limited to build phase? context.owner!.debugBuilding
. The entire parsing pipeline is in a async process. which would be called in between builds. Is that a reason it is limited?
As far as I see we do not have best practices when it comes to bloc state based go_router redirection, please correct me if I am wrong
I used this in our project. Hope this may help you.
GoRouter buildRouter({
required AuthCubit authCubit,
}) {
return GoRouter(
initialLocation: initialLocation,
routes: kRoutes,
refreshListenable: _buildRefreshListener(
authCubit,
),
redirect: (context, state) {
// Handle auth redirection
if (authCubit.state.data.isSignedIn ?? false) {
if (state.location == '/auth' ||
state.location == '/intro' ||
state.location == '/register' ||
state.location.startsWith('/register?') ||
state.location == '/recovery') {
return '/home';
}
} else {
if (!state.location.startsWith('/register') &&
!state.location.startsWith('/recovery') &&
!state.location.startsWith('/intro') &&
!state.location.startsWith('/auth')) {
return '/auth';
}
}
if (state.location == '/') {
return '/home';
}
return null;
},
// debugLogDiagnostics: kDebugMode,
);
}
StreamListener _buildRefreshListener(
AuthCubit authCubit,
) {
return StreamListener(
authCubit.stream
.startWith(authCubit.state)
.map((e) => e.data.isSignedIn)
.distinct(),
);
}
Closing for now but happy to continue the conversation if you still have questions 👍
As far as I see we do not have best practices when it comes to bloc state based go_router redirection, please correct me if I am wrong
Have you got one? what @jopmiddelkamp has proposed is refreshListenable
This has been removed in GoRouter v5. Refer to the migration guide: flutter.dev/go/go-router-v5-breaking-changes
@felangel can we reopen the issue with someone proposing a better well-defined example, with go_router redirection based on state changes from Bloc, as done with auto_route here:
Yeah the StreamListener
is more a component that belongs in the Flutter framework. Just not had the time/focus yet to figure out where to add it in the Flutter framework. But as temporary solution you can just add it in your own codebase.
_streamlistener.dart:
import 'dart:async';
import 'package:flutter/foundation.dart';
// TODO: Add to package:flutter/foundation.dart?
/// Converts a [Stream] into a [Listenable]
///
/// {@tool snippet}
/// Typical usage is as follows:
///
/// ```dart
/// StreamListener(stream)
/// ```
/// {@end-tool}
class StreamListener extends ChangeNotifier {
/// Creates a [StreamListener].
///
/// Every time the [Stream] receives an event this [ChangeNotifier] will
/// notify its listeners.
StreamListener(Stream<dynamic> stream) {
notifyListeners();
_subscription = stream.asBroadcastStream().listen((_) => notifyListeners());
}
late final StreamSubscription<dynamic> _subscription;
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
Sorry I was wrong, using
watch
should be correct since it will add dependency.The error looks like the watch is limited to build phase?
context.owner!.debugBuilding
. The entire parsing pipeline is in a async process. which would be called in between builds. Is that a reason it is limited?
Is there any answer to this question? IMO it would be the most comfortable way to just use bloc.watch() inside redirect of go_router.
Would appreciate any answer.
Sorry I was wrong, using
watch
should be correct since it will add dependency. The error looks like the watch is limited to build phase?context.owner!.debugBuilding
. The entire parsing pipeline is in a async process. which would be called in between builds. Is that a reason it is limited?Is there any answer to this question? IMO it would be the most comfortable way to just use bloc.watch() inside redirect of go_router.
Would appreciate any answer.
Hi @hey-cwuerzlhuber, I recently shared my solution for redirection with go_router and Bloc in stack overflow. Hope it helps!
can i have code of above issue answer.
Hey here guys and thank you for the mental work that you have put into this thread. I am currently still facing issues in implementing a correct production-ready redirection strategy for both mobile and web using go_router and bloc.
In my scenario, I need to have an "AuthGuard" (term borrowed from Angular) that prevents users from accessing certain routes without being authenticated. I have the following use cases:
From what I could read about the go_router, it seems that this can be achieved using the redirect property of the GoRouter as such:
class WebAppRouter {
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
late final GoRouter router = GoRouter(
debugLogDiagnostics: true,
navigatorKey: _rootNavigatorKey,
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) {
return CommonAppPage(child: child);
},
routes: [
// These are the pages that are displayed under a web Scaffold
ShellRoute(
builder: (context, routerState, child) {
return BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (!state.authenticated) {
context.go(Routes.login.path);
}
},
builder: (context, authState) {
if (!authState.authenticated) {
return Center(
child: CircularProgressIndicator(),
);
}
return WebAppPage(
currentUser: authState.user!,
currentPath: routerState.fullPath,
child: child,
);
},
);
},
routes: [
GoRoute(
path: '/',
redirect: (context, state) {
return Routes.users.path;
}),
GoRoute(
name: Routes.users.name,
path: Routes.users.path,
pageBuilder: (context, state) {
return CustomTransitionPage<void>(
key: state.pageKey,
child: UsersPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
);
}),
]),
GoRoute(name: Routes.login.name, path: Routes.login.path, builder: (context, state) => LoginPage()),
],
),
],
redirect: (context, state) {
final isAuthenticated = context.read<AuthBloc>().state.authenticated;
final isLoggingIn = state.fullPath == Routes.login.path;
// User is not authenticated and not on the login page, redirect to login
if (!isAuthenticated && !isLoggingIn) {
return Routes.login.path;
// User is authenticated and on the login page, redirect to the users page
} else if (isAuthenticated && isLoggingIn) {
return Routes.users.path;
}
// No redirection needed
return null;
},
);
}
This is ok-ish in most cases. Now the thing that happens is that my go-router depends on the state from bloc, which is practically async. This means that the redirect bloc will not be called again if the user becomes authenticated or unauthenticated, unless I rebuild the entire router, each time the state changes. And this will cause redirection again to the default paths. If I don't do this, then I will suffer from erroneous redirection, as the state flag will update with delay.
My question to the community and mostly @felangel (who I respect deeply) is if you guys have a working example of a web app in flutter that handles authentication and correct redirection reactively?
Thank you so much for your time!
The best solution I've found is to wrap the Widget
returned from the build
method on the router config, with another Widget
that uses BlocConsumer
to listen for changes and invoke go_router's navigation when needed.
router.dart
final router = GoRouter(
routes: [
GoRoute(
path: '/homepage',
builder: (context, state) => RefreshOnUpdate(const HomePage()),
),
GoRoute(
path: '/auth',
builder:(context, state) => const AuthPage()
)
]
);
RefreshOnUpdate.dart
class RefreshOnUpdate extends StatelessWidget {
Widget child;
RefreshOnUpdate(this.child, {super.key});
@override
Widget build(BuildContext context) {
return BlocConsumer<UserCubit, UserState>(
listener: (context, state) {
// Redirect if the user is no longer authenticated
if(state.user == null) {
context.go("/auth");
}
},
builder: (context, state) => child,
);
}
}
You can also use redirect
on the route config, but that gets called whenever you load the page the first time, and not when the cubit updates its state.
What would be the best way to somehow implement a reactive redirection using bloc states? Currently I'm doing redirection like this:
However I do not like the fact hat I have to call GoRouter.of(context).refresh() in the bloc listener to trigger the redirection again. Is there a more professional way to work with flutter_bloc ?