Search code examples
flutterriverpodflutter-riverpodgorouterroutemaster

Flutter: how to listen to GoRouter's route / location changes in a Riverpod provider?


I am developing a Flutter application with go_router and riverpod for navigation and state management respectively. The app has a widget which displays a live camera feed, and I'd like to "switch it off" and free the camera when other pages are stacked on top of it.

Here's a sample of the GoRouter code WITHOUT such logic.

GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => CameraWidget(),
      routes: [
        GoRoute(
          path: 'page1',
          builder: (context, state) => Page1Screen(),
        ),
      ],
    ),
  ],
)

My first attempt has been to put some logic in the GoRoute builder:

GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) {
        if (state.location == "/") {
          return CameraWidget();
        }
        return Center(child: Text("Camera not visible");
      },
      routes: [
        GoRoute(
          path: 'page1',
          builder: (context, state) => Page1Screen(),
        ),
      ],
    ),
  ],
)

But this apparently does not work as the builder is not called again when going from "/" to "/page1".

I then thought of using a riverpod StateProvider to hold a camera "on/off" state, to be manipulated by GoRouter. This is what I tried:

GoRouter(
  routes: [
    GoRoute(
      path: '/',
      redirect: (context, state) {
        final cameraStateNotifier = ref.read(cameraStateNotifierProvider.notifier);
        if (state.location == "/") {
          cameraStateNotifier.state = true;
        } else {
          cameraStateNotifier.state = false;
        }
        return null;
      },
      builder: (context, state) => CameraWidget(),
      routes: [
        GoRoute(
          path: 'page1',
          builder: (context, state) => Page1Screen(),
        ),
      ],
    ),
  ],
)

But this also does not work as apparently redirect gets called while rebuilding the widget tree, and it is forbidden to change a provider state while that happens.

Has anyone encountered the same issue before? How can I have a provider listen to GoRouter's location changes?


Solution

  • After further testing of my previous answer, I found that my approach with go_router does not work on Navigator.pop() calls or back button presses. After some more digging in go_router's code, I figured it'd be easier to switch to the Routemaster package, which seems to integrate much better with Riverpod. So far I am very happy with the change.

    EDIT: Improved approach now using Routemaster's observable API.

    Here's the code:

    import 'package:flutter_riverpod/flutter_riverpod.dart';
    import 'package:routemaster/routemaster.dart';
    
    class RouteObserver extends RoutemasterObserver {
      final ProviderRef _ref;
      MyObserver(this._ref);
    
      @override
      void didChangeRoute(RouteData routeData, Page page) {
        _ref.invalidate(locationProvider);
      }
    }
    
    final routerProvider = Provider((ref) => RoutemasterDelegate(
      routesBuilder: (context) => RouteMap(routes: {
        '/': (_) => MaterialPage(child: CameraWidget()),
        '/page1': (_) => MaterialPage(child: Page1Screen()),
      }),
      observers: [RouteObserver(ref)],
    ));
    
    final locationProvider = Provider((ref) => ref.read(routerProvider).currentConfiguration?.fullPath);