beamer 0.14.1
beamer: ^0.14.1 copied to clipboard

A routing package that lets you navigate through guarded page stacks and URLs using the Router and Navigator's Pages API, aka "Navigator 2.0".

pub style

GitHub commit activity GitHub Repo stars GitHub forks

GitHub closed issues GitHub closed pull requests

GitHub contributors Discord

Buy Me A Coffee

Handle your application routing, synchronize it with browser URL and more. Beamer uses the power of Router and implements all the underlying logic for you.


Quick Start #

The simplest setup is achieved by using the SimpleLocationBuilder which yields the least amount of code for a functioning application:

class MyApp extends StatelessWidget {
  final routerDelegate = BeamerDelegate(
    locationBuilder: SimpleLocationBuilder(
      routes: {
        // Return either Widgets or BeamPages if more customization is needed
        '/': (context, state) => HomeScreen(),
        '/books': (context, state) => BooksScreen(),
        '/books/:bookId': (context, state) {
          // Take the parameter of interest from BeamState
          final bookId = state.pathParameters['bookId']!;
          // Return a Widget or wrap it in a BeamPage for more flexibility
          return BeamPage(
            key: ValueKey('book-$bookId'),
            title: 'A Book #$bookId',
            popToNamed: '/',
            type: BeamPageType.scaleTransition,
            child: BookDetailsScreen(bookId),
          );
        }
      },
    ),
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: BeamerParser(),
      routerDelegate: routerDelegate,
    );
  }
}

Navigating through those routes can be done with

Beamer.of(context).beamToNamed('/books/2');

Accessing route attributes (for example, bookId for building BookDetailsScreen) can be done with

Beamer.of(context).currentBeamLocation.state.pathParameters['bookId'];

Passing additional arbitrary attributes that don't contribute to URI can be done via data;

Beamer.of(context).beamToNamed(
  '/book/2',
  data: {
    'note': 'this is my favorite book',
    'color': Colors.blue,
  },
);

For those who wish to have a full control over building a page stack, we now introduce some key concepts; BeamLocation and BeamState.

Key Concepts #

At the highest level, Beamer is a wrapper for Router and uses its own implementations for RouterDelegate and RouteInformationParser. The goal of beamer is to separate the responsibility of building a page stack for Navigator.pages into multiple classes with custom "states", instead of one global state.

For example, we would like to handle all the profile related page stacks such as

  • [ ProfilePage ],
  • [ ProfilePage, FriendsPage],
  • [ ProfilePage, FriendsPage, FriendPage ],
  • [ ProfilePage, SettingsPage ],
  • ...

with some "ProfileHandler" that knows which "state" corresponds to which page stack and updates this state as the page stack changes. Then similarly, we would like to have a "ShopHandler" for all the possible stacks of shop related pages such as

  • [ ShopPage ],
  • [ ShopPage, CategoriesPage ],
  • [ ShopPage, CategoriesPage, ItemsPage ],
  • [ ShopPage, CategoriesPage, ItemsPage, ItemPage ],
  • [ ShopPage, ItemsPage, ItemPage ],
  • [ ShopPage, CartPage ],
  • ...

These "Handlers" are called BeamLocations.

BeamLocations cannot work by themselves. When the URI comes into the app through deep-link, or as initial, there must be a decision which BeamLocation will further handle this URI and build pages for the Navigator. This is the job of BeamerDelegate.locationBuilder that will take the "global state" and give it to appropriate BeamLocation which will create and save its own "local state" from it to use it to build pages.

BeamLocation #

The most important construct in Beamer is a BeamLocation which represents a stack of one or more pages.
BeamLocation has 3 important roles:

  • know which URIs it can handle: pathBlueprints
  • know how to build a stack of pages: buildPages
  • keep a state that provides a link between the first 2

BeamLocation is an abstract class which needs to be extended. The purpose of having multiple BeamLocations is to architecturally separate unrelated "places" in an application.

For example, BooksLocation can handle all the pages related to books and ArticlesLocation everything related to articles. In the light of this scoping, BeamLocation also has a builder for wrapping an entire stack of its pages with some Provider so the similar data can be shared between similar pages.

This is an example of BeamLocation:

class BooksLocation extends BeamLocation {
  BooksLocation(BeamState state) : super(state);

  @override
  List<String> get pathBlueprints => ['/books/:bookId'];

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) => [
        BeamPage(
          key: ValueKey('home'),
          child: HomeScreen(),
        ),
        if (state.uri.pathSegments.contains('books'))
          BeamPage(
            key: ValueKey('books'),
            child: BooksScreen(),
          ),
        if (state.pathParameters.containsKey('bookId'))
          BeamPage(
            key: ValueKey('book-${state.pathParameters['bookId']}'),
            child: BookDetailsScreen(
              bookId: state.pathParameters['bookId'],
            ),
          ),
      ];
}

BeamState #

This is the above-mentioned state of BeamLocation. Its role is to keep various URI attributes such as pathBlueprintSegments (the segments of chosen pathBlueprint, as each BeamLocation supports many of those), pathParameters, queryParameters and arbitrary key-value data. Those attributes are important while building pages and for BeamState to create an uri that will be consumed by the browser.

Besides purely imperative navigation via e.g. beamToNamed('/books/3'), this also provides a method to have declarative navigation by changing the state of BeamLocation. For example:

Beamer.of(context).currentBeamLocation.update(
  (state) => state.copyWith(
    pathBlueprintSegments: ['books', ':bookId'],
    pathParameters: {'bookId': '3'},
  ),
),

BeamState can be extended with a completely custom state which can be used for BeamLocation, for example:

class BooksLocation extends BeamLocation<MyState> {...}

It is important in this case that MyState has an uri getter which is needed for browser's URL bar.

Beaming #

Navigating between or within BeamLocations is achieved by "beaming". You can think of it as teleporting (beaming) to another place in your app. Similar to Navigator.of(context).pushReplacementNamed('/my-route'), but Beamer is not limited to a single page, nor to a push per se. BeamLocations hold an arbitrary stack of pages that get built when you beam there. Using Beamer can feel like using many of Navigator's push/pop methods at once.

Examples of beaming:

Beamer.of(context).beamTo(MyLocation());

// or with an extension on BuildContext
context.beamTo(MyLocation());
context.beamToNamed('/books/2');

// this is equivalent to
context.beamTo(
  BooksLocation(
    BeamState(
      pathBlueprintSegments: ['books', ':bookId'],
      pathParameters: {'bookId': '2'},
    ),
  ),
),
context.beamToNamed(
  '/book/2',
  data: {'note': 'this is my favorite book'},
);

Updating #

Once at a BeamLocation, it is preferable to update the current location's state. For example, for going from /books to /books/3 (which are both handled by BooksLocation):

context.currentBeamLocation.update(
  (state) => state.copyWith(
    pathBlueprintSegments: ['books', ':bookId'],
    pathParameters: {'bookId': '3'},
  ),
),

To get from one BeamLocation's stack to another BeamLocation's stack, an update can be invoked on BeamerDelegate itself:

Beamer.of(context).update(
  state: BeamState(
    pathBlueprintSegments: ['articles', ':articleId'],
    pathParameters: {'articleId': '1'},
  ),
);

NOTE that every beaming function (beamTo, beamToNamed,...) will have the same effect as update either on BeamLocation or on BeamerDelegate.

Beaming Back #

NOTE: Navigating to previous page in a page stack is done via Navigator.of(context).pop(). This is also what the default AppBar's BackButton will call. If you beamed to the current page stack from some different page stack, then consider beamBack to return to your previous configuration.

All BeamStates that were visited are kept in beamStateHistory. Therefore, there is an ability to beam back to whichever BeamLocation is responsible for previous BeamState. For example, after spending some time on /books and /books/3, say you beam to /articles. From there, you can get back to your previous location as it were when you left, i.e. /books/3.

context.beamBack();

NOTE that Beamer can integrate Android's back button to do beamBack if possible when all the pages from current BeamLocation have been popped. This is achieved by setting a back button dispatcher in MaterialApp.router.

backButtonDispatcher: BeamerBackButtonDispatcher(delegate: routerDelegate)

Guards #

To guard specific routes, e.g. from un-authenticated users, global BeamGuards can be set up via BeamerDelegate.guards attribute. A most common example would be the BeamGuard that guards any route that is not /login and redirects to /login if the user is not authenticated:

BeamGuard(
  pathBlueprints: ['/login'],
  guardNonMatching: true,
  check: (context, location) => context.isUserAuthenticated(),
  beamToNamed: '/login',
)

Note the usage of guardNonMatching in this example. This is important because guards (there can be many of them, each guarding different aspects) will run in recursion on the output of previously applied guard until a "safe" route is reached. A common mistake is to setup a guard with pathBlueprints: ['*'] to guard everything, but everything also includes /login (which should be a "safe" route) and this leads to an infinite loop:

  • check /login
  • user not authenticated
  • beam to /login
  • check /login
  • user not authenticated
  • beam to /login
  • ...

Of course, guardNonMatching needs not to be used always. Sometimes we wish to guard just a few routes that can be specified. Here is an example of a guard that has the same role as above, implemented with guardNonMatching: false (default):

BeamGuard(
  pathBlueprints: ['/profile/*', '/orders/*'],
  check: (context, location) => context.isUserAuthenticated(),
  beamToNamed: '/login',
)

Usage #

To use the full-featured Beamer in your app, you must (as per official documentation) construct your *App widget with .router constructor to which (along with all your regular *App attributes) you provide

  • routeInformationParser that parses an incoming URI.
  • routerDelegate that controls (re)building of Navigator

Here you use the Beamer implementation of those - BeamerParser and BeamerDelegate, to which you pass your LocationBuilder.

In the simplest form, LocationBuilder is just a function which takes the current BeamState and returns a custom BeamLocation based on the URI or other state properties.

class MyApp extends StatelessWidget {
  final routerDelegate = BeamerDelegate(
    locationBuilder: (state) {
      if (state.uri.pathSegments.contains('books')) {
        return BooksLocation(state);
      }
      return HomeLocation(state);
    },
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: routerDelegate,
      routeInformationParser: BeamerParser(),
      backButtonDispatcher:
          BeamerBackButtonDispatcher(delegate: routerDelegate),
    );
  }
}

There are also two other options available, if you don't want to define a custom LocationBuilder function.

With a List of BeamLocations #

You can use the BeamerLocationBuilder with a list of BeamLocations. This builder will automatically select the correct location, based on the pathBlueprints of each BeamLocation. In this case, define your BeamerDelegate like this:

final routerDelegate = BeamerDelegate(
  locationBuilder: BeamerLocationBuilder(
    beamLocations: [
      HomeLocation(),
      BooksLocation(),
    ],
  ),
);

With a Map of Routes #

You can use the SimpleLocationBuilder with a map of routes, as mentioned in Quick Start. This completely removes the need for custom BeamLocations, but also gives you the least amount of customizability. Still, wildcards and path parameters in your paths are supported as with all the other options.

final routerDelegate = BeamerDelegate(
  locationBuilder: SimpleLocationBuilder(
    routes: {
      '/': (context, state) => HomeScreen(),
      '/books': (context, state) => BooksScreen(),
      '/books/:bookId': (context, state) =>
        BookDetailsScreen(bookId: state.pathParameters['bookId']),
    },
  ),
);

Nested Navigation #

When nested navigation is needed, you can just put Beamer anywhere in the Widget tree where this navigation will take place. There is no limit on how many Beamers an app can have. Common use case is a bottom navigation bar (see example), something like this:

class MyApp extends StatelessWidget {
  final routerDelegate = BeamerDelegate(
    initialPath: '/books',
    locationBuilder: SimpleLocationBuilder(
      routes: {
        '/*': (context, state) {
          final beamerKey = GlobalKey<BeamerState>();

          return Scaffold(
            body: Beamer(
              key: beamerKey,
              routerDelegate: BeamerDelegate(
                locationBuilder: BeamerLocationBuilder(
                  beamLocations: [
                    BooksLocation(),
                    ArticlesLocation(),
                  ],
                ),
              ),
            ),
            bottomNavigationBar: BottomNavigationBarWidget(
              beamerKey: beamerKey,
            ),
          );
        }
      },
    ),
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: routerDelegate,
      routeInformationParser: BeamerParser(),
    );
  }
}

General Notes #

  • When extending BeamLocation, two methods need to be implemented: pathBlueprints and buildPages.

    • buildPages returns a stack of pages that will be built by Navigator when you beam there, and pathBlueprints is there for Beamer to decide which BeamLocation corresponds to which URI.
    • BeamLocation keeps query and path parameters from URI in its BeamState. The : is necessary in pathBlueprints if you might get path parameter from browser.
  • BeamPage's child is an arbitrary Widget that represents your app screen / page.

    • key is important for Navigator to optimize rebuilds. This should be a unique value for "page state".
    • BeamPage creates MaterialPageRoute by default, but other transitions can be chosen by setting BeamPage.type to one of available BeamPageType.

NOTE that "Navigator 1.0" can be used alongside Beamer. You can easily push or pop pages with Navigator.of(context), but those will not be contributing to the URI. This is often needed when some info/helper page needs to be shown that doesn't influence the browser's URL. And of course, when using Beamer on mobile, this is a non-issue as there is no URL.

Tips and Common Issues #

  • removing the # from URL can be done by calling Beamer.setPathUrlStrategy() before runApp().
  • BeamPage.title is used for setting the browser tab title by default and can be opt-out by setting BeamerDelegate.setBrowserTabTitle to false.
  • Losing state on hot reload

Examples #

Check out all examples here.

Location Builders #

Here is a recreation of the example app from this article where you can learn a lot about Navigator 2.0. It contains three different options of building the locations. The full code is available here.

example-location-builders

Advanced Books #

For a step further, we add more flows to demonstrate the power of Beamer. The full code is available here.

example-advanced-books

Deep Location #

You can instantly beam to a location in your app that has many pages stacked (deep linking) and then pop them one by one or simply beamBack to where you came from. The full code is available here. Note that beamBackOnPop parameter of beamToNamed might be useful here to override AppBar's pop with beamBack.

example-deep-location

ElevatedButton(
    onPressed: () => context.beamToNamed('/a/b/c/d'),
    //onPressed: () => context.beamToNamed('/a/b/c/d', beamBackOnPop: true),
    child: Text('Beam deep'),
),

Provider #

You can override BeamLocation.builder to provide some data to the entire location, i.e. to all the pages. The full code is available here.

example-provider

// In your location implementation
@override
Widget builder(BuildContext context, Navigator navigator) {
  return MyProvider<MyObject>(
    create: (context) => MyObject(),
    child: navigator,
  );
}

Guards #

You can define global guards (for example, authentication guard) or location guards that keep a specific location safe. The full code is available here.

example-guards

  • Global Guards
BeamerDelegate(
  guards: [
    // Guard /books and /books/* by beaming to /login if the user is unauthenticated:
    BeamGuard(
      pathBlueprints: ['/books', '/books/*'],
      check: (context, location) => context.isAuthenticated,
      beamToNamed: '/login',
    ),
  ],
  ...
),
  • Location (local) Guards
// in your location implementation
@override
List<BeamGuard> get guards => [
  // Show forbiddenPage if the user tries to enter books/2:
  BeamGuard(
    pathBlueprints: ['/books/2'],
    check: (context, location) => false,
    showPage: forbiddenPage,
  ),
];

Authentication Bloc #

Here is an example on how to use BeamGuards for an authentication flow. It uses flutter_bloc for state management. The code is available here.

example-authentication-bloc

Bottom Navigation #

An examples of putting Beamer into the Widget tree is when using a bottom navigation bar. The code is available here.

example-bottom-navigation

Bottom Navigation Multiple Beamers #

The code for the bottom navigation example app with multiple beamers is available here

example-bottom-navigation-multiple-beamers

Nested Navigation #

NOTE: In all nested Beamers, full paths must be specified when defining BeamLocations and beaming. (support for relative paths is in progress)

The code for the nested navigation example app is available here

example-nested-navigation

Integration with Navigation UI Packages #

example-animated-rail

Migrating #

From 0.13 to 0.14 #

Instead of

locationBuilder: SimpleLocationBuilder(
  routes: {
    '/': (context) => MyWidget(),
    '/another': (context) {
      final state = context.currentBeamLocation.state;
      return AnotherThatNeedsState(state);
    }
  }
)

now we have

locationBuilder: SimpleLocationBuilder(
  routes: {
    '/': (context, state) => MyWidget(),
    '/another': (context, state) => AnotherThatNeedsState(state)
  }
)

From 0.12 to 0.13 #

  • rename BeamerRouterDelegate to BeamerDelegate
  • rename BeamerRouteInformationParser to BeamerParser
  • rename pagesBuilder to buildPages
  • rename Beamer.of(context).currentLocation to Beamer.of(context).currentBeamLocation

From 0.11 to 0.12 #

  • There's no RootRouterDelegate anymore. Just rename it to BeamerDelegate. If you were using its homeBuilder, use SimpleLocationBuilder and then routes: {'/': (context) => HomeScreen()}.
  • Behavior of beamBack was changed to go to previous BeamState, not BeamLocation. If this is not what you want, use popBeamLocation() that has the same behavior as old beamback.

From 0.10 to 0.11 #

  • BeamerDelegate.beamLocations is now locationBuilder. See BeamerLocationBuilder for easiest migration.
  • Beamer now takes BeamerDelegate, not BeamLocations directly
  • buildPages now also brings state

From 0.9 to 0.10 #

  • BeamLocation constructor now takes only BeamState state. (there's no need to define special constructors and call super if you use beamToNamed)
  • most of the attributes that were in BeamLocation are now in BeamLocation.state. When accessing them through BeamLocation:
    • pathParameters is now state.pathParameters
    • queryParameters is now state.queryParameters
    • data is now state.data
    • pathSegments is now state.pathBlueprintSegments
    • uri is now state.uri

From 0.7 to 0.8 #

  • rename pages to buildPages in BeamLocations
  • pass beamLocations to BeamerDelegate instead of BeamerParser. See Usage

From 0.4 to 0.5 #

  • instead of wrapping MaterialApp with Beamer, use *App.router()
  • String BeamLocation.pathBlueprint is now List<String> BeamLocation.pathBlueprints
  • BeamLocation.withParameters constructor is removed and all parameters are handled with 1 constructor. See example if you need super.
  • BeamPage.page is now called BeamPage.child

Help and Chat #

For any problems, questions, suggestions, fun,... join us at Discord Discord

Contributing #

This package is still in early stages. To see the upcoming features, check the Issue board.

If you notice any bugs not present in issues, please file a new issue. If you are willing to fix or enhance things yourself, you are very welcome to make a pull request. Before making a pull request:

  • if you wish to solve an existing issue, please let us know in issue comments first.
  • if you have another enhancement in mind, create an issue for it first, so we can discuss your idea.

Also, you can Buy Me A Coffee to speed up the development.

350
likes
130
pub points
93%
popularity

Publisher

beamer.dev

A routing package that lets you navigate through guarded page stacks and URLs using the Router and Navigator's Pages API, aka "Navigator 2.0".

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (LICENSE)

Dependencies

flutter, flutter_web_plugins

More

Packages that depend on beamer