mirror of
https://github.com/lucavenir/go_router_riverpod.git
synced 2025-08-26 15:31:23 +08:00
Updated and *way simpler* APIs
This commit is contained in:
@ -5,13 +5,17 @@ part 'auth.freezed.dart';
|
||||
/// Authentication class for this sample application.
|
||||
/// It should be self-explanatory.
|
||||
@freezed
|
||||
class Auth with _$Auth {
|
||||
sealed class Auth with _$Auth {
|
||||
const factory Auth.signedIn({
|
||||
required int id,
|
||||
required String displayName,
|
||||
required String email,
|
||||
required String token,
|
||||
}) = SignedIn;
|
||||
|
||||
const Auth._();
|
||||
const factory Auth.signedOut() = SignedOut;
|
||||
bool get isAuth => switch (this) {
|
||||
SignedIn() => true,
|
||||
SignedOut() => false,
|
||||
};
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'router/router_listenable.dart';
|
||||
import 'router/routes.dart';
|
||||
import 'router/router.dart';
|
||||
import 'utils/state_logger.dart';
|
||||
|
||||
void main() {
|
||||
@ -16,25 +13,12 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
class MyAwesomeApp extends HookConsumerWidget {
|
||||
class MyAwesomeApp extends ConsumerWidget {
|
||||
const MyAwesomeApp({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: SplashRoute.path,
|
||||
debugLogDiagnostics: true,
|
||||
routes: $appRoutes,
|
||||
redirect: notifier.redirect,
|
||||
),
|
||||
[notifier],
|
||||
);
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
routerConfig: router,
|
||||
|
59
example/lib/router/router.dart
Normal file
59
example/lib/router/router.dart
Normal file
@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../state/auth_controller.dart';
|
||||
import 'routes.dart';
|
||||
|
||||
part 'router.g.dart';
|
||||
|
||||
/// Exposes a [GoRouter] that uses a [Listenable] to refresh its internal state.
|
||||
///
|
||||
/// With Riverpod, we can't register a dependency via an Inherited Widget,
|
||||
/// thus making this implementation the "leanest" possible
|
||||
///
|
||||
/// To sync our app state with this our router, we simply update our listenable via `ref.listen`,
|
||||
/// and pass it to GoRouter's `refreshListenable`.
|
||||
/// In this example, this will trigger redirects on any authentication change.
|
||||
///
|
||||
/// Obviously, more logic could be implemented here, but again, this is meant to be a simple example.
|
||||
/// You can always build more listenables and even merge more than one into a more complex `ChangeNotifier`,
|
||||
/// but that's up to your case and out of this scope.
|
||||
@riverpod
|
||||
GoRouter router(RouterRef ref) {
|
||||
final routerKey = GlobalKey<NavigatorState>(debugLabel: 'routerKey');
|
||||
final isAuth = ValueNotifier<AsyncValue<bool>>(const AsyncLoading());
|
||||
ref
|
||||
..onDispose(isAuth.dispose) // don't forget to clean after yourselves (:
|
||||
// update the listenable, when some provider value changes
|
||||
// here, we are just interested in wheter the user's logged in
|
||||
..listen(
|
||||
authControllerProvider.select((value) => value.whenData((value) => value.isAuth)),
|
||||
(_, next) {
|
||||
isAuth.value = next;
|
||||
},
|
||||
);
|
||||
|
||||
final router = GoRouter(
|
||||
navigatorKey: routerKey,
|
||||
refreshListenable: isAuth,
|
||||
initialLocation: const SplashRoute().location,
|
||||
debugLogDiagnostics: true,
|
||||
routes: $appRoutes,
|
||||
redirect: (context, state) {
|
||||
final auth = isAuth.value.valueOrNull;
|
||||
if (auth == null) return const SplashRoute().location;
|
||||
|
||||
final isSplash = state.uri.path == const SplashRoute().location;
|
||||
if (isSplash) return auth ? const HomeRoute().location : const LoginRoute().location;
|
||||
|
||||
final isLoggingIn = state.uri.path == const LoginRoute().location;
|
||||
if (isLoggingIn) return auth ? const HomeRoute().location : null;
|
||||
|
||||
return auth ? null : const SplashRoute().location;
|
||||
},
|
||||
);
|
||||
ref.onDispose(router.dispose); // always clean up after yourselves (:
|
||||
|
||||
return router;
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../state/auth_controller.dart';
|
||||
import 'routes.dart';
|
||||
|
||||
part 'router_listenable.g.dart';
|
||||
|
||||
/// A [Listenable] implemented via an [AsyncNotifier].
|
||||
/// [GoRouter] accepts a [Listenable] to refresh its internal state, so this is kinda mandatory.
|
||||
///
|
||||
/// An alternative would be to register a dependency via an Inherited Widget, but that's kinda a no-go for Riverpod.
|
||||
///
|
||||
/// To sync Riverpod' state with this Listener, we simply accept and call a single callback on authentication change.
|
||||
/// Obviously, more logic could be implemented here, this is meant to be a simple example.
|
||||
///
|
||||
/// I kinda like this example, as this allows to centralize global redirecting logic in one class.
|
||||
///
|
||||
/// SIDE NOTES.
|
||||
/// This might look overcomplicated at a first glance;
|
||||
/// Instead, this method aims to follow some good some good practices:
|
||||
/// 1. It doesn't require us to pass [Ref](s) around
|
||||
/// 2. It works as a complete replacement for [ChangeNotifier], as it still implements [Listenable]
|
||||
/// 3. It allows for listening to multiple providers, or add more logic if needed
|
||||
@riverpod
|
||||
class RouterListenable extends _$RouterListenable implements Listenable {
|
||||
VoidCallback? _routerListener;
|
||||
bool _isAuth = false; // Useful for our global redirect function
|
||||
|
||||
@override
|
||||
Future<void> build() async {
|
||||
// One could watch more providers and write logic accordingly
|
||||
|
||||
_isAuth = await ref.watch(
|
||||
authControllerProvider.selectAsync(
|
||||
(data) => data.map(signedIn: (_) => true, signedOut: (_) => false),
|
||||
),
|
||||
);
|
||||
|
||||
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) {
|
||||
if (this.state.isLoading || this.state.hasError) return null;
|
||||
|
||||
final isSplash = state.uri.path == SplashRoute.path;
|
||||
|
||||
if (isSplash) {
|
||||
return _isAuth ? HomeRoute.path : LoginRoute.path;
|
||||
}
|
||||
|
||||
final isLoggingIn = state.uri.path == LoginRoute.path;
|
||||
if (isLoggingIn) return _isAuth ? HomeRoute.path : null;
|
||||
|
||||
return _isAuth ? null : SplashRoute.path;
|
||||
}
|
||||
|
||||
/// Adds [GoRouter]'s listener as specified by its [Listenable].
|
||||
/// [GoRouteInformationProvider] uses this method on creation to handle its
|
||||
/// internal [ChangeNotifier].
|
||||
/// Check out the internal implementation of [GoRouter] and
|
||||
/// [GoRouteInformationProvider] to see this in action.
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
_routerListener = listener;
|
||||
}
|
||||
|
||||
/// Removes [GoRouter]'s listener as specified by its [Listenable].
|
||||
/// [GoRouteInformationProvider] uses this method when disposing,
|
||||
/// so that it removes its callback when destroyed.
|
||||
/// Check out the internal implementation of [GoRouter] and
|
||||
/// [GoRouteInformationProvider] to see this in action.
|
||||
@override
|
||||
void removeListener(VoidCallback listener) {
|
||||
_routerListener = null;
|
||||
}
|
||||
}
|
@ -15,28 +15,16 @@ import '../state/permissions.dart';
|
||||
|
||||
part 'routes.g.dart';
|
||||
|
||||
@TypedGoRoute<SplashRoute>(path: SplashRoute.path)
|
||||
class SplashRoute extends GoRouteData {
|
||||
const SplashRoute();
|
||||
static const path = '/splash';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SplashPage();
|
||||
}
|
||||
}
|
||||
|
||||
@TypedGoRoute<HomeRoute>(
|
||||
path: HomeRoute.path,
|
||||
path: '/',
|
||||
routes: [
|
||||
TypedGoRoute<AdminRoute>(path: AdminRoute.path),
|
||||
TypedGoRoute<UserRoute>(path: UserRoute.path),
|
||||
TypedGoRoute<GuestRoute>(path: GuestRoute.path),
|
||||
TypedGoRoute<AdminRoute>(path: 'admin'),
|
||||
TypedGoRoute<UserRoute>(path: 'user'),
|
||||
TypedGoRoute<GuestRoute>(path: 'guest'),
|
||||
],
|
||||
)
|
||||
class HomeRoute extends GoRouteData {
|
||||
const HomeRoute();
|
||||
static const path = '/home';
|
||||
|
||||
/// Important note on this redirect function: this isn't reactive.
|
||||
/// No redirect will be triggered on a user role change.
|
||||
@ -62,10 +50,19 @@ class HomeRoute extends GoRouteData {
|
||||
}
|
||||
}
|
||||
|
||||
@TypedGoRoute<LoginRoute>(path: LoginRoute.path)
|
||||
@TypedGoRoute<SplashRoute>(path: '/splash')
|
||||
class SplashRoute extends GoRouteData {
|
||||
const SplashRoute();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const SplashPage();
|
||||
}
|
||||
}
|
||||
|
||||
@TypedGoRoute<LoginRoute>(path: '/login')
|
||||
class LoginRoute extends GoRouteData {
|
||||
const LoginRoute();
|
||||
static const path = '/login';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
@ -75,7 +72,6 @@ class LoginRoute extends GoRouteData {
|
||||
|
||||
class AdminRoute extends GoRouteData {
|
||||
const AdminRoute();
|
||||
static const path = 'admin';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
@ -85,7 +81,6 @@ class AdminRoute extends GoRouteData {
|
||||
|
||||
class UserRoute extends GoRouteData {
|
||||
const UserRoute();
|
||||
static const path = 'user';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
@ -95,7 +90,6 @@ class UserRoute extends GoRouteData {
|
||||
|
||||
class GuestRoute extends GoRouteData {
|
||||
const GuestRoute();
|
||||
static const path = 'guest';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
@ -104,12 +98,11 @@ class GuestRoute extends GoRouteData {
|
||||
}
|
||||
|
||||
/// This route shows how to parametrize a simple page and how to pass a simple query parameter.
|
||||
@TypedGoRoute<DetailsRoute>(path: DetailsRoute.path)
|
||||
@TypedGoRoute<DetailsRoute>(path: '/details/:id')
|
||||
class DetailsRoute extends GoRouteData {
|
||||
const DetailsRoute(this.id, {this.isNuke = false});
|
||||
final int id;
|
||||
final bool isNuke;
|
||||
static const path = '/details/:id';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
|
Reference in New Issue
Block a user