lucavenir / go_router_riverpod

An example on how to use Riverpod + GoRouter
460 stars 68 forks source link

Suggestion: link auth state with GoRouter using an InheritedWidget. #36

Open joshuadutton opened 4 months ago

joshuadutton commented 4 months ago

I want feedback on this approach. 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 BuildContext, I wanted to see what it would take to implement Auth.of(context) while also working with Riverpod. Here's what I did:

Auth Widget and InheritedWidget example:

// Note: I'm using widget state with hooks just so I can pass my `AuthRepository` to my Riverpod provider 
// to swap out different auth implementations. This is not needed if you aren't following this pattern, 
// but it's really helpful for me to switch out between Firebase and a mock for testing.

class Auth extends HookConsumerWidget {
  const Auth({super.key, required this.child, required this.repository});

  final Widget child;
  final AuthRepository repository;

  static AuthUser? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<_InheritedAuth>()?.authUser;
  }

  static AuthUser of(BuildContext context) {
    final auth = maybeOf(context);
    assert(auth != null, "No Auth found in context");
    return auth!;
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authUser = ref.watch(authNotifierProvider);

    useEffect(() {
      final authNotifier = ref.read(authNotifierProvider.notifier);
      authNotifier.setRepository(repository);
      return;
    }, []);

    return _InheritedAuth(authUser: authUser, child: child);
  }
}

class _InheritedAuth extends InheritedWidget {
  const _InheritedAuth({
    required this.authUser,
    required super.child,
  });

  final AuthUser authUser;

  @override
  bool updateShouldNotify(_InheritedAuth oldWidget) => oldWidget.authUser != authUser;
}

Then I just wrap my app with Auth like this:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'firebase_options.dart';
import 'routes/router.dart';
import 'theme/app_theme.dart';
import 'auth/auth.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Auth(
        repository: FirebaseAuthRepository(defaultRole: "user"),
        child: MaterialApp.router(
          routerConfig: router,
          title: "WhenPro",
          theme: lightTheme,
          darkTheme: darkTheme,
        ));
  }
}

A big upside to this approach is that whenever my authUser changes, the InheritedWidget causes it's children that are using Auth.of(context) to rebuild. This includes the router! That means that redirects are automatically triggered by changing auth state. I've done some testing, and this only happens when I expect it to.

Any downsides?

I could provide a complete example of this approach. I've also setup my app and boilerplate to use StatefulShellRoutes and swappable Firebase auth, which are two other things this example intends to provide (looking at the other issues).

XuanTung95 commented 4 months ago

Could you explain what are you trying to do and why it's not working? It's hard to understand.

joshuadutton commented 4 months ago

Could you explain what are you trying to do and why it's not working? It's hard to understand.

I'm not looking for help to get something working. Everything is working fine. This is a suggestion for a different approach that I want to discuss. If it's confusing, I could provide a complete example as a PR.

XuanTung95 commented 4 months ago

It's confusing. Can you describe it by words, it's better than some code.

joshuadutton commented 4 months ago

@XuanTung95 are you familiar with Inherited Widgets? It's built into the Flutter framework as a way to propagate information to widgets in the widget tree. (See https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). In fact, it's what GoRouter uses to allow you to get the router with the build context when you write final router = GoRouter.of(context);. I find the name, Inherited Widget, confusing because it's not referring to object inheritance, like subclassing a Widget.

Using an Inherited Widget, I can pass my auth state directly to the redirect method of my router, bypassing the need to wrap my router in a Riverpod provider. Inside the redirect method, I just need to call final authUser = Auth.of(context);, I feel like this is much cleaner since it leverages the Flutter APIs instead of working around them.

To do this, I just have the Inherited Widget watch my Riverpod auth provider and then rebuild it's child widgets whenever the auth state changes. From what I understand, the Flutter framework is smart in doing so, only rebuilding the widgets that actually reference the .of(context) method.

XuanTung95 commented 4 months ago

.of(context) = service locator + dependence.

It rebuild because of the "dependence" part. You can use only the service locator without "dependence".

There are many ways to do it, for example: context.getInheritedWidgetOfExactType(), 'ref.read', 'ProviderScope.containerOf(context, listen: false).read()'

XuanTung95 commented 4 months ago

I don't think using InheritedWidget is user friendly. Why don't you use riverpod instead, it do the same thing. If that is the suggestion you are looking for.