The purpose of the go_router is to use declarative routes to reduce complexity, regardless of the platform you're targeting (mobile, web, desktop), handling deep linking from Android, iOS and the web while still allowing an easy-to-use developer experience.

Last update: Jun 24, 2022

Pub Version Test codecov License

go_router

The goal of the go_router package is to simplify use of the Router in Flutter as specified by the MaterialApp.router constructor. By default, it requires an implementation of the RouterDelegate and RouteInformationParser classes. These two implementations themselves imply the definition of a third type to hold the app state that drives the creation of the Navigator. You can read an excellent blog post on these requirements on Medium. This separation of responsibilities allows the Flutter developer to implement a number of routing and navigation policies at the cost of complexity.

The purpose of the go_router is to use declarative routes to reduce complexity, regardless of the platform you're targeting (mobile, web, desktop), handling deep linking from Android, iOS and the web while still allowing an easy-to-use developer experience.

Table of Contents

Contributors

It's amazing to me how many folks have already contributed to this project. Huge shout out to the go_router contributors!

  • Salakar for the CI action on GitHub that is always helping me track down stuff I forget
  • rydmike for a bunch of README and dartdoc fixes as well as a great example for keeping state during nested navigation
  • Abhishek01039 for helping me change a life-long habit of sorting constructors after fields, which goes against Dart best practices
  • SunlightBro for the Android system Back button fix
  • craiglabenz for a bunch of README fixes; also, Craig has been talking about adding build_runner support to produce typesafe go and push code for named routes, so thumbs up on this issues if that's a feature you'd like to see in go_router
  • kevmoo for helping me track down spelling issues in my README and unused imports; keep it coming, Kev!

Changelog

If you'd like to see what's changed in detail over time, you can read the go_router Changelog.

Migrating to 2.0

There is a breaking change in the go_router 2.0 release: by popular demand, the params property of the GoRouterState object has been split into two properties:

  • params for parameters that are part of the path and, e.g. /family/:fid
  • queryParams for parameters that added optionally at the end of the location, e.g. /login?from=/family/f2

In the 1.x releases, the params property was a single object that contained both the path and query parameters in a single map. For example, if you had been using the params property to access query parameters like this in 1.x:

GoRoute(
  path: '/login',
  pageBuilder: (context, state) => MaterialPage<void>(
    key: state.pageKey,
    // 1.x: accessing query parameters
    child: LoginPage(from: state.params['from']),
  ),
),

in 2.0, you would access the query parameters like this:

GoRoute(
  path: '/login',
  pageBuilder: (context, state) => MaterialPage<void>(
    key: state.pageKey,
    // 2.0: accessing query parameters
    child: LoginPage(from: state.queryParams['from']),
  ),
),

Likewise, if you were using named routes in 1.x, you may have been passing both path and query parameters like so:

ListTile(
  title: Text(p.name),
  // 1.x: passing both path and query parameters
  onTap: () => context.goNamed(
    'person',
    // "extra" path params were query params
    {'fid': family.id, 'pid': p.id, 'qid': 'quid'},
  ),
),

Now you'll need to change your code to do the following in 2.0:

ListTile(
  title: Text(p.name),
  // 2.0: passing both path and query parameters
  onTap: () => context.goNamed(
    'person',
    params: {'fid': family.id, 'pid': p.id},
    queryParams: {'qid': 'quid'},
  ),
),

I got a little clever merging the two kinds of parameters into a single scope and hopefully this change makes things a little more clear.

Getting Started

To use the go_router package, follow these instructions.

Declarative Routing

The go_router is governed by a set of routes which you specify as part of the GoRouter constructor:

class App extends StatelessWidget {
  ...
  final _router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        pageBuilder: (context, state) => MaterialPage<void>(
          key: state.pageKey,
          child: const Page1Page(),
        ),
      ),
      GoRoute(
        path: '/page2',
        pageBuilder: (context, state) => MaterialPage<void>(
          key: state.pageKey,
          child: const Page2Page(),
        ),
      ),
    ],
  ...
  );
}

In this case, we've defined two routes. Each route path will be matched against the location to which the user is navigating. Only a single path will be matched, specifically the one that matches the entire location (and so it doesn't matter in which order you list your routes). The path will be matched in a case-insensitive way, although the case for parameters will be preserved.

Router state

A GoRoute also contains a pageBuilder function which is called to create the page when a path is matched. The builder function is passed a state object, which is an instance of the GoRouterState class that contains some useful information:

GoRouterState property description example 1 example 2
location location of the full route, including query params /login?from=/family/f2 /family/f2/person/p1
subloc location of this sub-route w/o query params /login /family/f2
path the GoRoute path /login family/:fid
fullpath full path to this sub-route /login /family/:fid
params params extracted from the location {} {'fid': 'f2'}
queryParams optional params from the end of the location {'from': '/family/f1'} {}
error Exception associated with this sub-route, if any Exception('404') ...
pageKey unique key for this sub-route ValueKey('/login') ValueKey('/family/:fid')

You can read more about sub-locations/sub-routes and parameterized routes below but the example code above uses the pageKey property as most of the example code does. The pageKey is used to create a unique key for the MaterialPage or CupertinoPage based on the current path for that page in the stack of pages, so it will uniquely identify the page w/o having to hardcode a key or come up with one yourself.

Error handling

In addition to the list of routes, the go_router needs an errorPageBuilder function in case no page is found, more than one page is found or if any of the page builder functions throws an exception, e.g.

class App extends StatelessWidget {
  ...
  final _router = GoRouter(
    ...
    errorPageBuilder: (context, state) => MaterialPage<void>(
      key: state.pageKey,
      child: ErrorPage(state.error),
    ),
  );
}

The GoRouterState object contains the location that caused the exception and the Exception that was thrown attempting to navigate to that route.

Initialization

With just a list of routes and an error page builder function, you can create an instance of a GoRouter, which itself provides the objects you need to call the MaterialApp.router constructor:

class App extends StatelessWidget {
  App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => MaterialApp.router(
        routeInformationParser: _router.routeInformationParser,
        routerDelegate: _router.routerDelegate,
      );

  final _router = GoRouter(routes: ..., errorPageBuilder: ...);
}

With the router in place, your app can now navigate between pages.

Navigation

To navigate between pages, use the GoRouter.go method:

// navigate using the GoRouter
onTap: () => GoRouter.of(context).go('/page2')

The go_router also provides a simplified means of navigation using Dart extension methods:

// more easily navigate using the GoRouter
onTap: () => context.go('/page2')

The simplified version maps directly to the more fully-specified version, so you can use either. If you're curious, the ability to just call context.go(...) and have magic happen is where the name of the go_router came from.

If you'd like to navigate via the Link widget, that works, too:

Link(
  uri: Uri.parse('/page2'),
  builder: (context, followLink) => TextButton(
    onPressed: followLink,
    child: const Text('Go to page 2'),
  ),
),

If the Link widget is given a URL with a scheme, e.g. https://flutter.dev, then it will launch the link in a browser. Otherwise, it'll navigate to the link inside the app using the built-in navigation system.

You can also navigate to a named route, discussed below.

Current location

If you want to know the current location, use the GoRouter.location property. If you'd like to know when the current location changes, either because of manual navigation or a deep link or a pop due to the user pushing the Back button, the GoRouter is a ChangeNotifier, which means that you can call addListener to be notified when the location changes, either manually or via Flutter's builder widget for ChangeNotifier objects, the non-intuitively named AnimatedBuilder:

class RouterLocationView extends StatelessWidget {
  const RouterLocationView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final router = GoRouter.of(context);
    return AnimatedBuilder(
      animation: router,
      builder: (context, child) => Text(router.location),
    );
  }
}

Or, if you're using the provider package, it comes with built-in support for re-building a Widget when a ChangeNotifier changes with a type that is much more clearly suited for the purpose.

Initial Location

If you'd like to set an initial location for routing, you can set the initialLocation argument of the GoRouter constructor:

final _router = GoRouter(
  routes: ...,
  errorPageBuilder: ...,
  initialLocation: '/page2',
);

The value you provide to initialLocation will be ignored if your app is started using deep linking.

Parameters

The route paths are defined and implemented in the path_to_regexp package, which gives you the ability to include parameters in your route's path:

final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/family/:fid',
      pageBuilder: (context, state) {
        // use state.params to get router parameter values
        final family = Families.family(state.params['fid']!);

        return MaterialPage<void>(
          key: state.pageKey,
          child: FamilyPage(family: family),
        );
      },
    ),
  ],
  errorPageBuilder: ...,
]);

You can access the matched parameters in the state object using the params property.

Dynamic linking

The idea of "dynamic linking" is that as the user adds objects to your app, each of them gets a link of their own, e.g. a new family gets a new link. This is exactly what route parameters enables, e.g. a new family has its own identifier when can be a variable in your family route, e.g. path: /family/:fid.

Sub-routes

Every top-level route will create a navigation stack of one page. To produce an entire stack of pages, you can use sub-routes. In the case that a top-level route only matches part of the location, the rest of the location can be matched against sub-routes. The rules are still the same, i.e. that only a single route at any level will be matched and the entire location much be matched.

For example, the location /family/f1/person/p2, can be made to match multiple sub-routes to create a stack of pages:

/             => HomePage()
  family/f1   => FamilyPage('f1')
    person/p2 => PersonPage('f1', 'p2') ← showing this page, Back pops the stack ↑

To specify a set of pages like this, you can use sub-page routing via the routes parameter to the GoRoute constructor:

final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      pageBuilder: (context, state) => MaterialPage<void>(
        key: state.pageKey,
        child: HomePage(families: Families.data),
      ),
      routes: [
        GoRoute(
          path: 'family/:fid',
          pageBuilder: (context, state) {
            final family = Families.family(state.params['fid']!);

            return MaterialPage<void>(
              key: state.pageKey,
              child: FamilyPage(family: family),
            );
          },
          routes: [
            GoRoute(
              path: 'person/:pid',
              pageBuilder: (context, state) {
                final family = Families.family(state.params['fid']!);
                final person = family.person(state.params['pid']!);

                return MaterialPage<void>(
                  key: state.pageKey,
                  child: PersonPage(family: family, person: person),
                );
              },
            ),
          ],
        ),
      ],
    ),
  ],
  errorPageBuilder: ...
);

The go_router will match the routes all the way down the tree of sub-routes to build up a stack of pages. If go_router doesn't find a match, then the error handler will be called.

Also, the go_router will pass parameters from higher level sub-routes so that they can be used in lower level routes, e.g. fid is matched as part of the family/:fid route, but it's passed along to the person/:pid route because it's a sub-route of the family/:fid route.

Pushing pages

In addition to the go method, the go_router also provides a push method. Both go and push can be used to build up a stack of pages, but in different ways. The go method does this by turning a single location into any number of pages in a stack using Sub-routes.

The push method is used to push a single page onto the stack of existing pages, which means that you can build up the stack programmatically instead of declaratively. When the push method matches an entire stack via sub-routes, it will take the top-most page from the stack and push that page onto the stack.

You can also push a named route, discussed below.

Redirection

Sometimes you want your app to redirect to a different location. The go_router allows you to do this at a top level for each new navigation event or at the route level for a specific route.

Top-level redirection

Sometimes you want to guard pages from being accessed when they shouldn't be, e.g. when the user is not yet logged in. For example, assume you have a class that tracks the user's login info:

class LoginInfo extends ChangeNotifier {
  var _userName = '';
  String get userName => _userName;
  bool get loggedIn => _userName.isNotEmpty;

  void login(String userName) {
    _userName = userName;
    notifyListeners();
  }

  void logout() {
    _userName = '';
    notifyListeners();
  }
}

You can use this info in the implementation of a redirect function that you pass as to the GoRouter constructor:

class App extends StatelessWidget {
  final loginInfo = LoginInfo();
  ...
  late final _router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        pageBuilder: (context, state) => MaterialPage<void>(
          key: state.pageKey,
          child: HomePage(families: Families.data),
        ),
      ),
      ...,
      GoRoute(
        path: '/login',
        pageBuilder: (context, state) => MaterialPage<void>(
          key: state.pageKey,
          child: const LoginPage(),
        ),
      ),
    ],

    errorPageBuilder: ...,

    // redirect to the login page if the user is not logged in
    redirect: (state) {
      final loggedIn = loginInfo.loggedIn;
      final goingToLogin = state.location == '/login';

      // the user is not logged in and not headed to /login, they need to login
      if (!loggedIn && !goingToLogin) return '/login';

      // the user is logged in and headed to /login, no need to login again
      if (loggedIn && goingToLogin) return '/';

      // no need to redirect at all
      return null;
    },
  );
}

In this code, if the user is not logged in and not going to the /login path, we redirect to /login. Likewise, if the user is logged in but going to /login, we redirect to /. If there is no redirect, then we just return null. The redirect function will be called again until null is returned to enable multiple redirects.

To make it easy to access this info wherever it's need in the app, consider using a state management option like provider to put the login info into the widget tree:

class App extends StatelessWidget {
  final loginInfo = LoginInfo();

  // add the login info into the tree as app state that can change over time
  @override
  Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
        value: loginInfo,
        child: MaterialApp.router(...),
      );
  ...
}

With the login info in the widget tree, you can easily implement your login page:

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: Text(_title(context))),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ElevatedButton(
            onPressed: () {
              // log a user in, letting all the listeners know
              context.read<LoginInfo>().login('test-user');

              // go home
              context.go('/');
            },
            child: const Text('Login'),
          ),
        ],
      ),
    ),
  );
}

In this case, we've logged the user in and manually redirected them to the home page. That's because the go_router doesn't know that the app's state has changed in a way that affects the route. If you'd like to have the app's state cause go_router to automatically redirect, you can use the refreshListenable argument of the GoRouter constructor:

class App extends StatelessWidget {
  final loginInfo = LoginInfo();
  ...
  late final _router = GoRouter(
    routes: ...,
    errorPageBuilder: ...,
    redirect: ...

    // changes on the listenable will cause the router to refresh it's route
    refreshListenable: loginInfo,
  );
}

Since the loginInfo is a ChangeNotifier, it will notify listeners when it changes. By passing it to the GoRouter constructor, the go_router will automatically refresh the route when the login info changes. This allows you to simplify the login logic in your app:

onPressed: () {
  // log a user in, letting all the listeners know
  context.read<LoginInfo>().login('test-user');

  // router will automatically redirect from /login to / because login info
  //context.go('/');
},

The use of the top-level redirect and refreshListenable together is recommended because it will handle the routing automatically for you when the app's data changes.

Route-level redirection

The top-level redirect handler passed to the GoRouter constructor is handy when you want a single function to be called whenever there's a new navigation event and to make some decisions based on the app's current state. However, in the case that you'd like to make a redirection decision for a specific route (or sub-route), you can do so by passing a redirect function to the GoRoute constructor:

final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      redirect: (_) => '/family/${Families.data[0].id}',
    ),
    GoRoute(
      path: '/family/:fid',
      pageBuilder: ...,
  ],
  errorPageBuilder: ...,
);

In this case, when the user navigates to /, the redirect function will be called to redirect to the first family's page. Redirection will only occur on the last sub-route matched, so you can't have to worry about redirecting in the middle of a location being parsed when you're already on your way to another page anyway.

Parameterized redirection

In some cases, a path is parameterized, and you'd like to redirect with those parameters in mind. You can do that with the params argument to the state object passed to the redirect function:

GoRoute(
  path: '/author/:authorId',
  redirect: (state) => '/authors/${state.params['authorId']}',
),

Multiple redirections

It's possible to redirect multiple times w/ a single navigation, e.g. / => /foo => /bar. This is handy because it allows you to build up a list of routes over time and not to worry so much about attempting to trim each of them to their direct route. Furthermore, it's possible to redirect at the top level and at the route level in any number of combinations.

The only trouble you need worry about is getting into a loop, e.g. / => /foo => /. If that happens, you'll get an exception with a message like this: Exception: Redirect loop detected: / => /foo => /.

Query Parameters

Sometimes you're doing deep linking and you'd like a user to first login before going to the location that represents the deep link. In that case, you can use query parameters in the redirect function:

class App extends StatelessWidget {
  final loginInfo = LoginInfo();
  ...
  late final _router = GoRouter(
    routes: ...,
    errorPageBuilder: ...,

    // redirect to the login page if the user is not logged in
    redirect: (state) {
      final loggedIn = loginInfo.loggedIn;

      // check just the subloc in case there are query parameters
      final goingToLogin = state.subloc == '/login';

      // the user is not logged in and not headed to /login, they need to login
      if (!loggedIn && !goingToLogin) return '/login?from=${state.subloc}';

      // the user is logged in and headed to /login, no need to login again
      if (loggedIn && goingToLogin) return '/';

      // no need to redirect at all
      return null;
    },

    // changes on the listenable will cause the router to refresh it's route
    refreshListenable: loginInfo,
  );
}

In this example, if the user isn't logged in, they're redirected to /login with a from query parameter set to the deep link. The state object has the location and the subloc to choose from. The location includes the query parameters whereas the subloc does not. Since the /login route may include query parameters, it's easiest to use the subloc in this case (and using the raw location will cause a stack overflow, an exercise that I'll leave to the reader).

Now, when the /login route is matched, we want to pull the from parameter out of the state object to pass along to the LoginPage:

GoRoute(
  path: '/login',
  pageBuilder: (context, state) => MaterialPage<void>(
    key: state.pageKey,
    // pass the original location to the LoginPage (if there is one)
    child: LoginPage(from: state.queryParams['from']),
  ),
),

In the LoginPage, if the from parameter was passed, we use it to go to the deep link location after a successful login:

class LoginPage extends StatelessWidget {
  final String? from;
  const LoginPage({this.from, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: Text(_title(context))),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ElevatedButton(
            onPressed: () {
              // log a user in, letting all the listeners know
              context.read<LoginInfo>().login('test-user');

              // if there's a deep link, go there
              if (from != null) context.go(from!);
            },
            child: const Text('Login'),
          ),
        ],
      ),
    ),
  );
}

It's still good practice to pass in the refreshListenable when manually redirecting, as we do in this case, to ensure any change to the login info causes the right routing to happen automatically, e.g. the user logging out will cause them to be routed back to the login page.

Named Routes

When you're navigating to a route with a location, you're hardcoding the URI construction into your app, e.g.

void _tap(BuildContext context, String fid, String pid) =>
  context.go('/family/$fid/person/$pid');

Not only is that error-prone, but the actual URI format of your app could change over time. Certainly redirection helps keep old URI formats working, but do you really want various versions of your location URIs lying willy-nilly around in your code?

Navigating to Named Routes

The idea of named routes is to make it easy to navigate to a route w/o knowing or caring what the URI format is. You can add a unique name to your route using the GoRoute.name parameter:

final _router = GoRouter(
  routes: [
    GoRoute(
      name: 'home',
      path: '/',
      pageBuilder: ...,
      routes: [
        GoRoute(
          name: 'family',
          path: 'family/:fid',
          pageBuilder: ...,
          routes: [
            GoRoute(
              name: 'person',
              path: 'person/:pid',
              pageBuilder: ...,
            ),
          ],
        ),
      ],
    ),
    GoRoute(
      name: 'login',
      path: '/login',
      pageBuilder: ...,
    ),
  ],

You don't need to name any of your routes but the ones that you do name, you can navigate to using the name and whatever params are needed:

void _tap(BuildContext context, String fid, String pid) =>
  context.go(context.namedLocation('person', params: {'fid': fid, 'pid': pid}));

The namedLocation method will look up the route by name in a case-insensitive way, construct the URI for you and fill in the params as appropriate. If you miss a param or pass in params that aren't part of the path, you'll get an error. Since it's somewhat inconvenient to have to dereference the context object twice, go_router provides a goNamed method that does the lookup and navigation in one step:

void _tap(BuildContext context, String fid, String pid) =>
  context.goNamed('person', params: {'fid': fid, 'pid': pid});

There is also a pushNamed method that will look up the route by name, pull the top page off of the generated match stack and push that onto the existing stack of pages.

Redirecting to Named Routes

In addition to navigation, you may also want to be able to redirect to a named route, which you can also do using the namedLocation method of either GoRouter or GoRouterState:

// redirect to the login page if the user is not logged in
redirect: (state) {
  final loggedIn = loginInfo.loggedIn;

  // check just the subloc in case there are query parameters
  final loginLoc = state.namedLocation('login');
  final goingToLogin = state.subloc == loginLoc;

  // the user is not logged in and not headed to /login, they need to login
  if (!loggedIn && !goingToLogin)
    return state.namedLocation('login', queryParams: {'from': state.subloc});

  // the user is logged in and headed to /login, no need to login again
  if (loggedIn && goingToLogin) return state.namedLocation('home');

  // no need to redirect at all
  return null;
},

In this example, we're using namedLocation to get the location for the named 'login' route and then comparing it to the current subloc to find out if the user is currently logging in or not. Furthermore, when we construct a location for redirection, we use namedLocation to pass in parameters to construct the location. All of this is done without hardcoding any URI formatting into your code.

Custom Transitions

As you transition between routes, you get transitions based on whether you're using MaterialPage or CupertinoPage; each of them implements the transitions as defined by the underlying platform. However, if you'd like to implement a custom transition, you can do so by using the CustomTransitionPage provided with go_router:

GoRoute(
  path: '/fade',
  pageBuilder: (context, state) => CustomTransitionPage<void>(
    key: state.pageKey,
    child: const TransitionsPage(kind: 'fade', color: Colors.red),
    transitionsBuilder: (context, animation, secondaryAnimation, child) =>
        FadeTransition(opacity: animation, child: child),
  ),
),

The transitionBuilder argument to the CustomTransitionPage is called when you're routing to a new route, and it's your chance to return a transition widget. The transitions sample shows off four different kind of transitions, but really you can do whatever you want.

custom transitions example

The CustomTransitionPage constructor also takes a transitionsDuration argument in case you'd like to customize the duration of the transition as well (it defaults to 300ms).

Async Data

Sometimes you'll want to load data asynchronously, and you'll need to wait for the data before showing content. Flutter provides a way to do this with the FutureBuilder widget that works just the same with the go_router as it always does in Flutter. For example, imagine you've got a Repository class that does network communication when it looks up data:

class Repository {
  Future<List<Family>> getFamilies() async { /* network comm */ }
  Future<Family> getFamily(String fid) async => { /* network comm */ }
  ...
}

Now you can use the FutureBuilder to show a loading indicator while the data is loading:

class App extends StatelessWidget {
  final repo = Repository();
  ...
  late final _router = GoRouter(
    routes: [
      GoRoute(
        path: '/',
        pageBuilder: (context, state) => NoTransitionPage<void>(
          key: state.pageKey,
          child: FutureBuilder<List<Family>>(
            future: repo.getFamilies(),
            pageBuilder: (context, snapshot) {
              if (snapshot.hasError)
                return ErrorPage(snapshot.error as Exception?);
              if (snapshot.hasData) return HomePage(families: snapshot.data!);
              return const Center(child: CircularProgressIndicator());
            },
          ),
        ),
        routes: [
          GoRoute(
            path: 'family/:fid',
            pageBuilder: (context, state) => NoTransitionPage<void>(
              key: state.pageKey,
              child: FutureBuilder<Family>(
                future: repo.getFamily(state.params['fid']!),
                pageBuilder: (context, snapshot) {
                  if (snapshot.hasError)
                    return ErrorPage(snapshot.error as Exception?);
                  if (snapshot.hasData)
                    return FamilyPage(family: snapshot.data!);
                  return const Center(child: CircularProgressIndicator());
                },
              ),
            ),
            routes: [
              GoRoute(
                path: 'person/:pid',
                pageBuilder: (context, state) => NoTransitionPage<void>(
                  key: state.pageKey,
                  child: FutureBuilder<FamilyPerson>(
                    future: repo.getPerson(
                      state.params['fid']!,
                      state.params['pid']!,
                    ),
                    pageBuilder: (context, snapshot) {
                      if (snapshot.hasError)
                        return ErrorPage(snapshot.error as Exception?);
                      if (snapshot.hasData)
                        return PersonPage(
                            family: snapshot.data!.family,
                            person: snapshot.data!.person);
                      return const Center(child: CircularProgressIndicator());
                    },
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    ],
    errorPageBuilder: (context, state) => MaterialPage<void>(
      key: state.pageKey,
      child: ErrorPage(state.error),
    ),
  );
}

class NoTransitionPage<T> extends CustomTransitionPage<T> {
  const NoTransitionPage({required Widget child, LocalKey? key})
      : super(transitionsBuilder: _transitionsBuilder, child: child, key: key);

  static Widget _transitionsBuilder(
          BuildContext context,
          Animation<double> animation,
          Animation<double> secondaryAnimation,
          Widget child) =>
      child;
}

This is a simple case that shows a circular progress indicator while the data is being loaded and before the page is shown.

async data example

The way transitions work, the outgoing page is shown for a little while before the incoming page is shown, which looks pretty terrible when your page is doing nothing but showing a circular progress indicator. I admit that I took the coward's way out and turned off the transitions so that things wouldn't look so bad in the animated screenshot. However, it would be nicer to keep the transition, navigate to the page showing as much as possible, e.g. the AppBar, and then show the loading indicator inside the page itself. In that case, you'll be on your own to show an error in the case that the data can't be loaded. Such polish is left as an exercise for the reader.

Nested Navigation

Sometimes you want to choose a page based on a route as well as the state of that page, e.g. the currently selected tab. In that case, you want to choose not just the page from a route but also the widgets nested inside the page. That's called "nested navigation". The key differentiator for "nested" navigation is that there's no transition on the part of the page that stays the same, e.g. the app bar stays the same as you navigate to different tabs on this TabView:

nested navigation example

Of course, you can easily do this using the TabView widget, but what makes this nested "navigation" is that the location of the page changes, i.e. notice the address bar as the user transitions from tab to tab. This makes it easy for the user to capture a dynamic link for any object in the app, enabling deep linking.

To use nested navigation using go_router, you can simply navigate to the same page via different paths or to the same path with different parameters, with the differences dictating the different state of the page. For example, to implement that page with the TabView above, you need a widget that changes the selected tab via a parameter:

class FamilyTabsPage extends StatefulWidget {
  final int index;
  FamilyTabsPage({required Family currentFamily, Key? key})
      : index = Families.data.indexWhere((f) => f.id == currentFamily.id),
        super(key: key) {
    assert(index != -1);
  }

  @override
  _FamilyTabsPageState createState() => _FamilyTabsPageState();
}

class _FamilyTabsPageState extends State<FamilyTabsPage>
    with TickerProviderStateMixin {
  late final TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TabController(
      length: Families.data.length,
      vsync: this,
      initialIndex: widget.index,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _controller.index = widget.index;
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text(_title(context)),
          bottom: TabBar(
            controller: _controller,
            tabs: [for (final f in Families.data) Tab(text: f.name)],
            onTap: (index) => _tap(context, index),
          ),
        ),
        body: TabBarView(
          controller: _controller,
          children: [for (final f in Families.data) FamilyView(family: f)],
        ),
      );

  void _tap(BuildContext context, int index) =>
      context.go('/family/${Families.data[index].id}');

  String _title(BuildContext context) =>
      (context as Element).findAncestorWidgetOfExactType<MaterialApp>()!.title;
}

The FamilyTabsPage is a stateful widget that takes the currently selected family as a parameter. It uses the index of that family in the list of families to set the currently selected tab. However, instead of switching the currently selected tab to whatever the user clicks on, it uses navigation to get to that index instead. It's the use of navigation that changes the address in the address bar. And, the way that the tab index is switched is via the call to didChangeDependencies. Because the FamilyTabsPage is a stateful widget, the widget itself can be changed but the state is kept. When that happens, the call to didChangeDependencies will change the index of the TabController to match the new navigation location.

To implement the navigation part of this example, we need a route that translates the location into an instance of FamilyTabsPage parameterized with the currently selected family:

final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      redirect: (_) => '/family/${Families.data[0].id}',
    ),
    GoRoute(
      path: '/family/:fid',
      pageBuilder: (context, state) {
        final fid = state.params['fid']!;
        final family = Families.data.firstWhere((f) => f.id == fid,
            orElse: () => throw Exception('family not found: $fid'));

        return MaterialPage<void>(
          key: state.pageKey,
          child: FamilyTabsPage(key: state.pageKey, currentFamily: family),
        );
      },
    ),
  ],
  
  errorPageBuilder: ...,
);

The / route is a redirect to the first family. The /family/:fid route is the one that sets up nested navigation. It does this by first creating an instance of FamilyTabsPage with the family that matches the fid parameter. And second, it uses state.pageKey to signal to Flutter that this is the same page as before. This combination is what causes the router to leave the page alone, to update the browser's address bar and to let the TabView navigate to the new selection.

This may seem like a lot, but in summary, you need to do three things with the page you create in your page builder to support nested navigation:

  1. Use a StatefulWidget as the base class of the thing you pass to MaterialPage (or whatever).

  2. Pass the same key value to the MaterialPage so that Flutter knows that you're keeping the same state for your StatefulWidget-derived page; state.pageKey is handy for this.

  3. As the user navigates, you'll create the same StatefulWidget-derived type, passing in new data, e.g. which tab is currently selected. Because you're using a widget with the same key, Flutter will keep the state but swap out the widget wrapping w/ the new data as constructor args. When that new widget wrapper is in place, Flutter will call didChangeDependencies so that you can use the new data to update the existing widgets, e.g. the selected tab.

This example shows off the selected tab on a TabView but you can use it for any nested content of a page your app navigates to.

Keeping State

When doing nested navigation, the user expects that widgets will be in the same state that they left them in when they navigated to a new page and return, e.g. scroll position, text input values, etc. You can enable support for this by using AutomaticKeepAliveClientMixin on a stateful widget. You can see this in action in the FamiliyView of the nested_nav.dart example:

class FamilyView extends StatefulWidget {
  const FamilyView({required this.family, Key? key}) : super(key: key);
  final Family family;

  @override
  State<FamilyView> createState() => _FamilyViewState();
}

/// Use the [AutomaticKeepAliveClientMixin] to keep the state.
class _FamilyViewState extends State<FamilyView>
    with AutomaticKeepAliveClientMixin {
      
  // Override `wantKeepAlive` when using `AutomaticKeepAliveClientMixin`.
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    // Call `super.build` when using `AutomaticKeepAliveClientMixin`.
    super.build(context);
    return ListView(
      children: [
        for (final p in widget.family.people)
          ListTile(
            title: Text(p.name),
            onTap: () =>
                context.go('/family/${widget.family.id}/person/${p.id}'),
          ),
      ],
    );
  }
}

To instruct the AutomaticKeepAliveClientMixin to keep the state, you need to override wantKeepAlive to return true and call super.build in the State class's build method, as show above.

keeping state example

Notice that after scrolling to the bottom of the long list of children in the Hunting family, then going to another tab and then going to another page, when you return to the list of Huntings that the scroll position is maintained.

Deep Linking

Flutter defines "deep linking" as "opening a URL displays that screen in your app." Anything that's listed as a GoRoute can be accessed via deep linking across Android, iOS and the web. Support works out of the box for the web, of course, via the address bar, but requires additional configuration for Android and iOS as described in the Flutter docs.

URL Path Strategy

By default, Flutter adds a hash (#) into the URL for web apps:

URL Strategy w/ Hash

The process for turning off the hash is documented but fiddly. The go_router has built-in support for setting the URL path strategy, however, so you can simply call GoRouter.setUrlPathStrategy before calling runApp and make your choice:

void main() {
  // turn on the # in the URLs on the web (default)
  // GoRouter.setUrlPathStrategy(UrlPathStrategy.hash);

  // turn off the # in the URLs on the web
  GoRouter.setUrlPathStrategy(UrlPathStrategy.path);

  runApp(App());
}

Setting the path instead of the hash strategy turns off the # in the URLs:

URL Strategy w/o Hash

If your router is created as part of the construction of the widget passed to the runApp method, you can use a shortcut to set the URL path strategy by using the urlPathStrategy parameter of the GoRouter constructor:

 // no need to call GoRouter.setUrlPathStrategy() here
 void main() => runApp(App());

/// sample app using the path URL strategy, i.e. no # in the URL path
class App extends StatelessWidget {
  ...
  final _router = GoRouter(
    routes: ...,
    errorPageBuilder: ...,

    // turn off the # in the URLs on the web
    urlPathStrategy: UrlPathStrategy.path,
  );
}

Finally, when you deploy your Flutter web app to a web server, it needs to be configured such that every URL ends up at your Flutter web app's index.html, otherwise Flutter won't be able to route to your pages. If you're using Firebase hosting, you can configure rewrites to cause all URLs to be rewritten to index.html.

If you'd like to test your release build locally before publishing, and get that cool redirect to index.html feature, you can use flutter run itself:

$ flutter run -d chrome --release lib/url_strategy.dart

Note that you have to run this command from a place where flutter run can find the web/index.html file.

Of course, any local web server that can be configured to redirect all traffic to index.html will do, e.g. live-server.

Debugging Your Routes

Because go_router asks that you provide a set of paths, sometimes as fragments to match just part of a location, it's hard to know just what routes you have in your app. In those cases, it's handy to be able to see the full paths of the routes you've created as a debugging tool, e.g.

GoRouter: known full paths for routes:
GoRouter:   => /
GoRouter:   =>   /family/:fid
GoRouter:   =>     /family/:fid/person/:pid
GoRouter: known full paths for route names:
GoRouter:   home => /
GoRouter:   family => /family/:fid
GoRouter:   person => /family/:fid/person/:pid

Likewise, there are multiple ways to navigate, e.g. context.go(), context.goNamed(), context.push(), context.pushNamed(), the Link widget, etc., as well as redirection, so it's handy to be able to see how that's going under the covers, e.g.

GoRouter: setting initial location /
GoRouter: location changed to /
GoRouter: getting location for name: "person", params: {fid: f2, pid: p1}
GoRouter: going to /family/f2/person/p1
GoRouter: location changed to /family/f2/person/p1

Finally, if there's an exception in your routing, you'll see that in the debug output, too, e.g.

GoRouter: Exception: no routes for location: /foobarquux

To enable this kind of output when your GoRouter is first created, you can use the debugLogDiagnostics argument:

final _router = GoRouter(
  routes: ...,
  errorPageBuilder: ...,

  // log diagnostic info for your routes
  debugLogDiagnostics: true,
);

This parameter defaults to false, which produces no output.

Examples

You can see the go_router in action via the following examples:

You can run these examples from the command line like so (from the example folder):

$ flutter run lib/main.dart

Or, if you're using Visual Studio Code, a launch.json file has been provided with these examples configured.

Issues

Do you have an issue with or feature request for go_router? Log it on the issue tracker.

GitHub

https://github.com/csells/go_router
Comments
  • 1. Add `navigatorBuilder` params to GoRoute

    While navigatorBuilder is useful for providing a single top-level scaffold or menu around your app, it forces you to use imperative logic in cases where you do not simply want to have a single wrapping scaffold.

    Consider a use-case, where I have a single login page with no scaffold, once logging in, I want to wrap my main menu around the inner routes.

    I could write it imperitively, with something like:

    navigatorBuilder: (nav){
      if(route == '/login') return nav;
      return AppMenu(child: nav);
    }
    

    Or, I could write it declaratively like this:

    routes: [
       GoRoute(path: '/login', ...);
       GoRoute(path: '/', 
           builder: (route) => AppMenu(route),
           routes: [ 
              ....
           ]
       )
    ]
    

    Both approaches are valid, but currently only the imperative is possible. Imperative works nice for simple use cases like the above, but has some problems expressing complex trees.

    Declarative allows much more flexibility for various navigation schemes. For example, it easily allows you to wrap secondary or tertiary menus around your routes by just declaring the builders around the routes that need them:

    routes: [
       GoRoute(path: '/', ... ); // no scaffold here
       GoRoute(path: '/app', 
           builder: (route) => AppMenu(route), // wrap an outer scaffold for all sub-routes of /app
           routes: [ 
                GoRoute(path: '/messages', 
                     builder: (route) => MessagesMenu(route), // wrap an inner scaffold for all sub-routes of /messages
                     routes: [ 
                          GoRoute(path: '/inbox', ...),
                          GoRoute(path: '/outbox', ...), 
                     ]
                 )
           ]
       )
    ]
    
    Reviewed by esDotDev at 2021-11-04 16:11
  • 2. Typing a route url in the url bar goes back to home for nested navigation with tabs and urlPathStrategy is not working

    I mimicked the example nested_nav.dart but instead of having the same UI with different data (families and persons), I have a totally different UI on each tab with different data. When I use the UI tabs to navigate the url does reflect the navigation but unlike the example if I type a route, although the url has changed, it does not navigate. Then when I push Enter in the url bar it goes back to my home page which is my initialLocation. I see in the example initialLocation is not used so I'll try redirecting but not sure why this would break it.
    Also the pressing the browser back button changes the url in the url bar but the tabs do not navigate, similar to the problem when typing the url.
    Also, I have urlPathStrategy: UrlPathStrategy.path but the hash is there.

    Reviewed by nyck33 at 2021-11-10 04:19
  • 3. [Proposal] [Enhancement] Add BuildContext to redirect

    https://github.com/csells/go_router/blob/master/lib/src/go_router_delegate.dart#L350

    When we use the redirect we might want to check for dependencies injected in the widget tree using the context.

    For instance, I have pages that are only accessible under certain user permissions. In Flutter web a user can simply type the url path to the page they want to see (navigate) but if the permission is not present the page should redirect.

    I have the permissions up in the tree as the state of a bloc (using flutter bloc) and it would be great if I could receive the context in the redirect and simply find the bloc's state.

    I believe this is possible, either passing the current navigator's state's context to the redirect callback or to the GoRouterState.

    Reviewed by nosmirck at 2021-11-23 05:54
  • 4. Any thoughts on implementing Router.neglect?

    We have a homegrown router that already that is remarkably similar to go_router, probably because we started with navigator 2.0 and never used the original navigator API, but it would be nice to eventually move to something someone else maintains :)

    On the web I often want to navigate with no browser history created, I get tired of fighting the browser navigation buttons! I still want the deep links and the browser URL changes though. I just want a bit more control over history.

    We solve this by wrapping notifyListeners with Router.neglect I did a quick POC to get it working with go_router.

    I'm wondering if this is a desirable feature for go_router? It wouldn't be a breaking change as implemented I suppose.

      // Add parameter to neglect history.
      void _safeNotifyListeners({bool neglectHistory = false}) {
        // this is a hack to fix the following error:
        // The following assertion was thrown while dispatching notifications for
        // GoRouterDelegate: setState() or markNeedsBuild() called during build.
        if (neglectHistory) {
          WidgetsBinding.instance == null
              ? Router.neglect(_aBuildContext, notifyListeners)
              : scheduleMicrotask(() => Router.neglect(_aBuildContext, notifyListeners));
        } else {
          WidgetsBinding.instance == null ? notifyListeners() : scheduleMicrotask(notifyListeners);
        }
      }
      
      
      // add flag to methods that call _safeNotifyListeners.
        /// Navigate to the given location.
      void go(String location, {Object? extra, bool neglectHistory = false}) {
        _log('going to $location');
        _go(location, extra: extra);
        _safeNotifyListeners(neglectHistory: neglectHistory);
      }
    

    The main issue is Router.neglect needing the build context in places where its not in scope yet, I just hacked something quick to grab it on build, but haven't reasoned out a better way yet.

      late BuildContext _aBuildContext;
    
      Widget _builder(BuildContext context, Iterable<GoRouteMatch> matches) {
        _aBuildContext = context;
       ...
       ...
    
    

    Edited this because the above isn't the full solution. You also have to wrap anywhere *pages[] is changed in Router.neglect. While I appreciate that go_router itself doesn't necessarily want to involve itself in managing history its implementation seems to be making it impossible to do outside of of go_router internals.

    Reviewed by nullrocket at 2021-12-08 18:43
  • 5. Add a locator function to GoRouterState

    This is the complete implementation of the solution suggested in #184. It would enable users to be able to get dependencies from any redirect callback through state.locator.

    Reviewed by letsar at 2021-11-23 15:13
  • 6. Generated extension methods for `goNamed` and `pushNamed`

    This is an exploratory feature request to add a build_runner layer for generating type-safe wrappers around goNamed and pushNamed.

    I'm imagining something like this:

    @Generate(int fid)
    GoRoute(
      name: 'family',
      path: '/family/:fid',
      pageBuilder: (context, state) {...},
    )
    

    Which would create something like this:

    extension on GoRouter {
      void pushFamily(int fid) =>
        push(_lookupNamedRoute('family', {'fid': fid}));
    
      void goFamily(int fid) =>
        go(_lookupNamedRoute('family', {'fid': fid}));
    }
    

    Thoughts?

    Reviewed by craiglabenz at 2021-10-07 14:01
  • 7. Using `navigatorBuilder` with a widget that contains Overlay results in crash

    Using GoRouter.navigatorBuilder with Overlay fails

    I was trying use the navigatorBuilder to always wrap the content in a complex responsice scaffold wrapper and noticed that it fails if there is an Overlay widget in the widget in the wrapping widget in the navigatorBuilder.

    We can use the repo example nested_nav.dart to demonstrate the error. Wrap the text in the navigatorBuilder in the example with a Tooltip, eg so:

        // show the current router location as the user navigates page to page; note
        // that this is not required for nested navigation but it is useful to show
        // the location as it changes
        navigatorBuilder: (context, child) => Material(
          child: Column(
            children: [
              Expanded(child: child!),
              Padding(
                padding: const EdgeInsets.all(8),
                child: Tooltip(  // <== This  will break the app.
                  message: _router.location,
                  child: Text(_router.location),
                ),
              ),
            ],
          ),
        ),
    
    

    This will result in this error: image

    Or in text form:

    ======== Exception caught by widgets library =======================================================
    The following assertion was thrown building Tooltip("/family/f1", dirty, state: _TooltipState#b100b(ticker inactive)):
    No Overlay widget found.
    
    Tooltip widgets require an Overlay widget ancestor for correct operation.
    
    The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.
    
    The specific widget that failed to find an overlay was: Tooltip
      "/family/f1"
    The relevant error-causing widget was: 
      Tooltip Tooltip:file:////code/flutter/csells/go_router/example/lib/nested_nav.dart:69:20
    

    The error is of course well explained, but first felt a bit surprising, it is saying it does not have an Overlay in any parent, which it would get via MaterialApp or a Navigator. Well we clearly have a MaterialApp above it in the three, The built widget in the navigatorBuilder even gets the theme from it. However, a MaterialApp created via the .router interface apparently does not have a Navigator a this stage, we don't see a Navigator until much later in the tree, and the Overlay is I think actually created by the Navigator not by the MaterialApp directly. (The error message is thus not entirely correct imo)

    image

    This is a bit of a complication, well for my use case anyway.

    This entire issue does of course go back to the not yet fully supported setup of nested navigation that I'm trying out different options for. I guess I in the navigatorBuilder I could include an extra Navigator that then become a higher root than the GoRouter's Navigator, thus introducing the real missing nested Navigator.

    With Navigator 1.0 and using imperative routing that is actually how I solved nested navigation. The part in the widget tree that needed nested navigation, just contained a new Navigator and inserted via it navigated to widgets in that point in the tree.

    It seems I really need support for that in GoRouter to be able to build the routing I need. But I will return in another issue on that topic and relate it to the open nested navigation issue as well.

    Conclusion

    The current version of the navigatorBuilder in GoRouter cannot be used to insert any widget that would work in a normal MaterialApp, this was a surprise and not expected.

    Reviewed by rydmike at 2021-11-28 22:48
  • 8. Query parameters persist after navigation

    Given some logic in the router's redirect handler that redirects the app to a path with some query parameters in it (E.g.: /view-invoice?url=$invoiceUrl)... Once the user navigates back from that route (via back swipe gesture / back button), the state.queryParams object still holds the parameters from the view-invoice page, even though the user was taken to a new route with no parameters.

    Example flow:

    1. Navigate to /.
    2. Navigate to /view-invoice?url=$invoiceUrl.
    3. Navigate to /. <-- When printing the state.queryParams in the redirect handler, the object still holds the old values.

    I'm not sure if this is by design or not, but if it is, there should at least be a way to clear the params manually. Maybe via an onBackNavigation handler?

    Reviewed by benPesso at 2021-11-18 16:51
  • 9. allow pages to stop from navigating away, e.g. if there are unsaved state changes

    Hello, I am going to start a web project with Flutter, for another project use the Fluro library, however, I could not solve the following problem: If a user is on a page that contains a form and is making modifications is said form, I would like that if he tries to exit without having saved the modifications (either by putting another url, with the browser's backbutton, ...) it would show him an alertdialog giving him the possibility of canceling the navigation to the new url or, on the contrary, of canceling the modifications and staying on the same page where he is. The question is, can I do this with go_router? go_router has any mechanism by which this functionality can be achieved? Please, if this functionality is possible with go_router could you give me some indications. Thank you so much for all the help. Regards, Jose

    Reviewed by jamolina2021 at 2021-11-05 10:05
  • 10. Query Params vs URL Params

    I find it a bit confusing that both are mapped to state.params. Is there a reason for that? Would you consider actually adding state.queryParams for query parameters?

    Reviewed by talamaska at 2021-10-01 20:30
  • 11. [Proposal] Add `onPop` to GoRoute

    Since the Navigator is hidden behind GoRouter.navigatorBuilder, we don't have a way to define the onPagePop handler. It would make sense to have a onPop for each GoRoute, so we can handle page popping per route.

    Reviewed by benPesso at 2021-11-22 01:05
  • 12. Chore/improve coverage

    :sparkles: What kind of change does this PR introduce? (Bug fix, feature, docs update...)

    This PR adds new tests, reaching code paths not previously covered as pointed by a coverage report.

    :arrow_heading_down: What is the current behavior?

    All tests are currently passing and result in 64.8% coverage.

    :new: What is the new behavior (if this is a feature change)?

    All tests are currently passing and result in 92.9% coverage.

    :boom: Does this PR introduce a breaking change?

    No. It does not change any lib/ file.

    :bug: Recommendations for testing

    If you want to see the coverage report I mentioned before you could use genhtml tool. Just run the following:

    flutter test --coverage && genhtml coverage/lcov.info -o coverage/report && open coverage/report/index.html
    

    :memo: Links to relevant issues/docs

    None.

    :thinking: Checklist before submitting

    • [X] I made sure all projects build.
    • [X] I updated CHANGELOG.md to add a description of the change.
    • [X] I updated the relevant documentation.
    • [X] I updated the relevant code example.
    • [X] I rebased onto current master.
    Reviewed by Ascenio at 2022-02-20 16:57
  • 13. added_barrier_dissmissable_pagebuilder_to_customtransitionpage

    :sparkles: What kind of change does this PR introduce? (Bug fix, feature, docs update...)

    The barrierdissmissable property of customtransitionpage was not working. this bug is fixed also introduced pageBuilder instead of child in customTransitionPage which gives more control for animation

    :arrow_heading_down: What is the current behavior?

    the dialog is not dissmissing after touching outside screen after making the barrierdissmissable property of customtransitionpage to true

    Also the child property of CustomTransitionPage dosent give more control over animation

    :new: What is the new behavior (if this is a feature change)?

    making the barrierDismmissable property to true makes the dialog dissmissable after pressing outside

    more control over animation

    :boom: Does this PR introduce a breaking change?

    no

    :bug: Recommendations for testing

    yes

    :memo: Links to relevant issues/docs

    :thinking: Checklist before submitting

    • [yes ] I made sure all projects build.
    • [ no] I updated CHANGELOG.md to add a description of the change.
    • [no ] I updated the relevant documentation.
    • [ yes] I updated the relevant code example.
    • [ no] I rebased onto current master.
    Reviewed by ismailfarisi at 2022-02-01 20:58
  • 14. Add tests to Redirection Example

    :sparkles: What kind of change does this PR introduce? (Bug fix, feature, docs update...)

    Add tests to Redirection Example and Mockito for creating a MockGoRouter

    :arrow_heading_down: What is the current behavior?

    No tests

    :new: What is the new behavior (if this is a feature change)?

    Test the redirection example

    :boom: Does this PR introduce a breaking change?

    No

    :bug: Recommendations for testing

    Run in example folder

    flutter test --no-pub --coverage --test-randomize-ordering-seed random
    

    To generate coverage. You can use LCOV to see the coverage of the example app. The redirection.dart is at 95.8% of Coverage with this PR.

    :memo: Links to relevant issues/docs

    Not sure if I should update changelog since it's only testing the examples.

    :thinking: Checklist before submitting

    • [X] I made sure all projects build.
    • [X] I updated CHANGELOG.md to add a description of the change.
    • [X] I updated the relevant documentation.
    • [X] I updated the relevant code example.
    • [X] I rebased onto current master.
    Reviewed by Lyokone at 2022-01-27 11:21
  • 15. Add example breadcrumbs

    :sparkles: What kind of change does this PR introduce? (Bug fix, feature, docs update...)

    Example of how Breadcrumbs could look like. There are still some changes needed as matches variable is marked as visibleForTesting

    https://user-images.githubusercontent.com/19904063/147143330-56a2d539-4ad8-4b20-8009-5b2686cfafad.mp4

    It also showcase how it would look the Cupertino Back Button with the ContextMenu that displays previous routes

    https://user-images.githubusercontent.com/19904063/147143764-090b227c-f410-4ec7-99a2-61a64e5f3684.mp4

    :arrow_heading_down: What is the current behavior?

    :new: What is the new behavior (if this is a feature change)?

    :boom: Does this PR introduce a breaking change?

    :bug: Recommendations for testing

    :memo: Links to relevant issues/docs

    :thinking: Checklist before submitting

    • [X] I made sure all projects build.
    • [ ] I updated CHANGELOG.md to add a description of the change.
    • [ ] I updated the relevant documentation.
    • [ ] I updated the relevant code example.
    • [X] I rebased onto current master.
    Reviewed by jamesblasco at 2021-12-22 19:09

Related

A streaming client for the Komga self-hosted comics/manga/BD server targeting Android/iOS written in Dart/Flutter
A streaming client for the Komga self-hosted comics/manga/BD server targeting Android/iOS written in Dart/Flutter

Klutter A streaming client for the Komga self-hosted comics/manga/BD server targeting Android/iOS written in Dart/Flutter Background This is a project

Jun 23, 2022
Purpose of this project is to create extendable architecture of making platform aware Widgets which automatically select platform specific implementation

Old good factory Main obstacle in creating native experience on Flutter is the fact that you are asked to rebuild two layouts using platform specific

Jun 25, 2022
Flet enables developers to easily build realtime web, mobile and desktop apps in Python. No frontend experience required.
Flet enables developers to easily build realtime web, mobile and desktop apps in Python. No frontend experience required.

Flet Flet is a framework that enables you to easily build realtime web, mobile and desktop apps in your favorite language and securely share them with

Jun 24, 2022
A Flutter plugin for iOS and Android allowing access to the device cameras.

Camera Plugin A Flutter plugin for iOS and Android allowing access to the device cameras. Note: This plugin is still under development, and some APIs

Mar 17, 2020
A Flutter plugin for Android and iOS allowing access to the device cameras, a bit deeper!!

flutter_cameraview A Flutter plugin for Android and iOS allowing access to the device cameras, a bit deeper!!. This plugin was created to offer more a

May 23, 2022
A Flutter plugin for iOS and Android allowing access to the device cameras.

A Flutter plugin for iOS and Android allowing access to the device cameras.

Jun 13, 2022
🧾 Flutter widget allowing easy cache-based data display in a ListView featuring pull-to-refresh and error banners.

Often, apps just display data fetched from some server. This package introduces the concept of fetchable streams. They are just like normal Streams, b

Jan 18, 2022
Flutter video compress - Generate a new file by compressed video, and provide metadata. Get video thumbnail from a video path, supports JPEG/GIF. To reduce app size not using FFmpeg in IOS.
Flutter video compress - Generate a new file by compressed video, and provide metadata. Get video thumbnail from a video path, supports JPEG/GIF. To reduce app size not using FFmpeg in IOS.

flutter_video_compress Generate a new path by compressed video, Choose to keep the source video or delete it by a parameter. Get video thumbnail from

May 24, 2022
A package that gives us a modern way to show animated border as a placeholder while loading our widget with easy customization and ready to use.
A package that gives us a modern way to show animated border as a placeholder while loading our widget with easy customization and ready to use.

A package that gives us a modern way to show animated border as a placeholder while loading our widget with easy customization and ready to use.

Jun 10, 2022
A flutter plugin to get facebook deep links and log app events using the latest Facebook SDK to include support for iOS 14

Facebook Sdk For Flutter LinkedIn GitHub facebook_sdk_flutter allows you to fetch deep links, deferred deep links and log facebook app events. This wa

Jun 23, 2022
Automatically generate profile picture with random first name and background color. But you can still provide pictures if you have them. As the default color, based on the name of the first letter. :fire: :fire: :fire:
Automatically generate profile picture with random first name and background color. But you can still provide pictures if you have them. As the default color, based on the name of the first letter. :fire: :fire: :fire:

FLUTTER PROFILE PICTURE Automatically generate profile picture with random first name and background color. But you can still provide pictures if you

May 3, 2022
Hassan uni links - A Flutter plugin project to help with App/Deep Links (Android) and Universal Links and Custom URL schemes

Uni Links A Flutter plugin project to help with App/Deep Links (Android) and Uni

Feb 12, 2022
As a beginner , this is my own side project by using flutter & dart , Firebase . This app still in progress .

helpful_app A new Flutter application. Getting Started This project is a starting point for a Flutter application. A few resources to get you started

Nov 23, 2021
Shoes online store, it's still unfinished

shamo A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is

Feb 10, 2022
App that simulates a flow of screens for the course of navigation and routes with nuvigator through Flutter
App that simulates a flow of screens for the course of navigation and routes with nuvigator through Flutter

Rotas app App que simula um fluxo de telas para o curso de navegação e rotas com

Dec 19, 2021
Pneumonia detection android app based on deep learning API

pneumonia A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if thi

Nov 7, 2021
An extension to the bloc state management library which lets you create State Machine using a declarative API

An extension to the bloc state management library which lets you create State Machine using a declarative API

May 29, 2022