Updated and *way simpler* APIs

This commit is contained in:
venir.dev
2023-10-28 17:50:13 +02:00
parent f1e5a29e1e
commit 16c499193f
5 changed files with 84 additions and 127 deletions

View File

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

View File

@ -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,

View 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;
}

View File

@ -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;
}
}

View File

@ -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) {