4

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?

Michele
  • 2,148
  • 1
  • 9
  • 14

3 Answers3

6

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;
});
Michele
  • 2,148
  • 1
  • 9
  • 14
3

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

Michele
  • 2,148
  • 1
  • 9
  • 14
0

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.

Alex Lomia
  • 6,705
  • 12
  • 53
  • 87