Recommendations on how to deal with Navigation for multi-package apps with go_router?

I have a Flutter app project that splits the features into packages, each containing all the code for the screen widget, state management, business rules, api requests for a given feature, as well as should contain definition of routes to access the screens defined inside this package. The package might contain multiple pages, so there might be the need to perform navigation to screens inside the package, but also other packages might need to navigate to my package’s screens.

Is there a recommended way to deal with this using go_router package without the need for packages to directly depend on each other and also keeping type-safe routes (avoid named routes that could create some implicit dependency)?

Basically I’m trying to find some extension from what is described in this guide from VGV.

#go-router #navigation #multi-package

3 Likes

Curious, what is the main reason to split the app into multiple feature packages?

We have a big project (monorepo), with around 10+ teams working in it, with potential to grow even more in the upcoming months. By splitting features into packages, we enable, for instance:

  • more accurate definition of code ownership
  • running only affected code tests on pull requests
  • stronger control of code access for features (we can define which files are exported outside the package)
  • dependency between features and internal “core” libraries explicit (via pubspec.yaml)
4 Likes

GoRouter requires all of your routes to be defined in a single GoRouter configuration object, but you can split up those routes into separate libraries, like this:

You may need to make sure that your routes don’t conflict with each other. You may want to create a parent route for each feature (‘/feature1’, ‘/feature2/’) and define the child routes underneath that (‘/feature1/screenA’).

Hope this helps!

2 Likes

Thanks for the suggestion! It definitely works but navigating between LibraryA and LibraryB would be done by named routes, which creates some implicit dependency between the two. For a larger project, having higher control over that and even on avoiding conflict names is a must. I’ll probably work on some strategy in the upcoming month and I can share some results here that should solve these.

2 Likes

Thanks, yeah unfortunately GoRouter doesn’t support navigating to a relative route (you can’t call context.go(‘newlocation’) or go(‘…/newlocation’), for example).

Actually, my previous comment is wrong now, the PR for relative routes just landed and has been published in the latest version.

1 Like

The way I’ve implemented this in our app is:

  • top level app package, with material app and GoRouter
  • intermediate modules/packages
  • base package which all modules depend on

AppRouter interface/abstract class
Defined in the base package. Contains app route definitions and/or methods to do the navigation such as toCustomers(),

This interface is provided via Riverpod or any other DI solution. All packages read and reference the AppRouter interface for navigation

AppGoRouter implementation class
Defined in the top level app module. Implements the interface and responsible for creating GoRouter with the correct routes.

This approach works pretty well for us and allows the modules to navigate without needing to reference GoRouter directly.

1 Like

@joshburton Can you share more what the AppRouter interface looks like? Is this something that other developers could benefit from?

@johnpryan I don’t think it’s really something that needs to be turned into a package in order for others to benefit from, this setup is really just abstracting implementation from interface.

Something like this:

// defined in base module, usable in intermediate modules and app module
abstract class AppRouter {
  GoRouter get router;

  GlobalKey<NavigatorState> get navigatorKey;

  void goHome();

  void goBooks();

  void goBookDetail(String id);
}

// defined in app module, used by MaterialApp
class AppGoRouter implements AppRouter {
  late final GoRouter _router;

  AppGoRouter() {
    _router = GoRouter(
      // setup routes etc
    );
  }

  @override
  GoRouter get router {
    return _router;
  }

  @override
  void goHome() {
    _router.go("/");
  }

  @override
  void goBooks() {
    _router.go("/books");
  }

  @override
  void goBookDetail(String id) {
    _router.go("/books/:id");
  }
}

1 Like

Thanks for sharing some sample code - is the main benefit to give developers type-safe access to the destinations in the app? That seems like something go_router_builder aims to solve.

The main benefit here to is to define an interface of navigable routes available to all modules in a monorepo, where the actual route and definition may be defined in any of the monorepo packages.

I don’t believe go_router_builder would provide any benefit in this case as it would build routes in the package they are defined in, which may not be accessible to another package.

That’s interesting - how do you ensure that your app isn’t navigating to a broken link, for example if a code change changes the name of a URL. Is that something the developer needs to write tests for, or is there some other way to check that in the monorepo setup you have?

In our monorepo the route urls are also defined alongside the base AppRouter interface, so there’s only one place in which the urls can be changed.

But we have plenty of widget tests that run against the entire app to verify navigation behaviour.

1 Like