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

4

5

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?

Piteous answered 29/10, 2022 at 18:13 Comment(0)
P
2

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);

Piteous answered 2/11, 2022 at 11:45 Comment(0)
P
9

EDIT: This approach doesn't seem to work with Navigator.pop() calls and back button presses. Check out the currently accepted answer for a better solution.


I believe I found a good way to do so. I defined a provider for GoRouter first, then a second one to listen to router.routeInformationProvider. This is a ChangeNotifier which notifies everytime the route information changes. Finally we can listen to this through a third provider for the specific location.

I think this is a good workaround, even though requires importing src/information_provider.dart from the GoRouter package which is not meant to.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/information_provider.dart';

final routerProvider = Provider<GoRouter>((ref) => GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => CameraWidget(),
      routes: [
        GoRoute(
          path: 'page1',
          builder: (context, state) => Page1Screen(),
        ),
      ],
    ),
  ],
));

final routeInformationProvider = ChangeNotifierProvider<GoRouteInformationProvider>((ref) {
  final router = ref.watch(routerProvider);
  return router.routeInformationProvider;
});

final currentRouteProvider = Provider((ref) {
  return ref.watch(routeInformationProvider).value.location;
});
Piteous answered 29/10, 2022 at 19:35 Comment(0)
P
2

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);

Piteous answered 2/11, 2022 at 11:45 Comment(0)
M
2

One thing you could do with GoRouter to handle the route changes is using the RouteObserver:

// router.dart

final routeObserverProvider = RouteObserver<ModalRoute<void>>(); // <--

final routerProvider = Provider<GoRouter>((ref) {
  final routeObserver = ref.read(routeObserverProvider);
  
  return GoRouter(
    observers: [routeObserver], // <--
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => CameraWidget(),
        routes: [
          GoRoute(
            path: 'page1',
            builder: (context, state) => Page1Screen(),
          ),
        ],
      ),
    ],
  );
});
// camera_widget.dart

class CameraWidget extends ConsumerStatefulWidget {
  const CameraWidget({Key? key}) : super(key: key);

  @override
  ConsumerState createState() => _CameraWidgetState();
}

class _CameraWidgetState extends ConsumerState<CameraWidget> with RouteAware { // <-- NOTE: `with RouteAware`
  late RouteObserver _routeObserver;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    // saving the route observer reference for later use in 
    // `dispose()`. Otherwise, calling `ref.read` there causes the
    // "Looking up a deactivated widget's ancestor is unsafe" error
    _routeObserver = ref.read(routeObserverProvider);

    _routeObserver.subscribe(this, ModalRoute.of(context)!);
  }

  @override
  void dispose() {
    _routeObserver.unsubscribe(this);

    super.dispose();
  }

    @override
  void didPush() { // <--
    // do something
  }

  @override
  void didPushNext() { // <--
    // do something
  }

  @override
  void didPop() { // <--
    // do something
  }

  @override
  void didPopNext() { // <--
    // do something
  }

  @override
  Widget build(BuildContext context) { /* blah blah */ }
}

As you see, now you can access didPush(), didPushNext(), didPop() and didPopNext() methods on the widget.

Mordancy answered 9/3, 2023 at 1:20 Comment(1)
Interestingly, GoRouter seems to only ever call didPush in my case. I then use GoRouterState.of(context).fullPath to check if I actually arrived at the current route.Nefertiti
U
0

After web login, we would be redirected to our app via deeplinks and we were getting the route as /timeline?id=abc-dfjn-23. We needed to check the id with the userId of the particular user and show the correct screen or throw an error screen. To fetch the id parameter from the GoRoute we used

Uri.parse(path).queryParameters['id']

path is the String variable in which we were saving the deeplink.

Umlaut answered 3/6 at 6:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.