favorite

beamer.dev

pub Awesome Flutter

GitHub commit activity GitHub Repo stars GitHub forks

GitHub closed issues GitHub closed pull requests

GitHub contributors Discord

Buy Me A Coffee

Beamer uses the power of Router and implements all the underlying logic for you, letting you explore arbitrarily complex navigation scenarios with ease.

Nested navigation Bottom navigation Multiple Beamers


Quick Start

For most navigation scenarios, using the RoutesLocationBuilder is a great choice which yields the least amount of code. After setting up our App like this, we can start navigating with Beamer.of(context).beamToNamed('/books/2').

class MyApp extends StatelessWidget {
  final routerDelegate = BeamerDelegate(
    locationBuilder: RoutesLocationBuilder(
      routes: {
        // Return either Widgets or BeamPages if more customization is needed
        '/': (context, state, data) => HomeScreen(),
        '/books': (context, state, data) => BooksScreen(),
        '/books/:bookId': (context, state, data) {
          // Take the path parameter of interest from BeamState
          final bookId = state.pathParameters['bookId']!;
          // Collect arbitrary data that persists throughout navigation
          final info = (data as MyObject).info;
          // Use BeamPage to define custom behavior
          return BeamPage(
            key: ValueKey('book-$bookId'),
            title: 'A Book #$bookId',
            popToNamed: '/',
            type: BeamPageType.scaleTransition,
            child: BookDetailsScreen(bookId, info),
          );
        }
      },
    ),
  );

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

RoutesLocationBuilder will pick and sort routes based on their paths.
For example;

  • navigating to /books/1 will match all 3 entries from routes and stack them on top of each other
  • navigating to /books will match the first 2 entries from routes

The corresponding pages are put into Navigator.pages and BeamerDelegate (re)builds the Navigator, showing the selected stack of pages on the screen.

Why do we have a locationBuilder and what is a BeamLocation, the output of it?

BeamLocation is an entity which, based on its state, decides what pages should go into Navigator.pages. locationBuilder chooses the appropriate BeamLocation that should further handle the incoming RouteInformation. This is most commonly achieved by examining BeamLocation.pathPatterns.

RoutesLocationBuilder returns a special type of BeamLocation - RoutesBeamLocation, that has opinionated implementation for most common navigation use-cases. If RoutesLocationBuilder doesn't provide desired behavior or enough customization, one can extend BeamLocation to define and organize the behavior for any number of page stacks that can go into Navigator.pages.

Further reading: BeamLocation, BeamState.

Navigation is done by "beaming". One 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 produce a stack of pages that get built when you beam there. Beaming can feel like using many of Navigator's push/pop methods at once.

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

// Beaming with an extension method on BuildContext
context.beamToNamed('/books/2');

// Beaming with additional data that persist 
// throughout navigation within the same BeamLocation
context.beamToNamed('/book/2', data: MyObject());

There are 2 types of going back, i.e. reverse navigation; upward and reverse chronological.

Upward (popping a page from stack)

Upward navigation is navigating to a previous page in the current page stack. This is better known as "pop" and is done through Navigator's pop/maybePop methods. The default AppBar's BackButton will call this if nothing else is specified.

Navigator.of(context).maybePop();

Reverse Chronological (beaming to previous state)

Reverse chronological navigation is navigating to wherever we were before. In case of deep-linking (e.g. coming to /books/2 from /authors/3 instead of from /books), this will not be the same as pop. Beamer keeps navigation history in beamingHistory so there is an ability to navigate chronologically to a previous entry in beamingHistory. This is called "beaming back". Reverse chronological navigation is also what the browser's back button does, although not via beamBack, but through its internal mechanics.

Beamer.of(context).beamBack();

Android back button

Integration of Android's back button with beaming is achieved by setting a backButtonDispatcher in MaterialApp.router. This dispatcher needs a reference to the same BeamerDelegate that is set for routerDelegate.

MaterialApp.router(
  ...
  routerDelegate: beamerDelegate,
  backButtonDispatcher: BeamerBackButtonDispatcher(delegate: beamerDelegate),
)

BeamerBackButtonDispatcher will try to pop first and fallback to beamBack if pop is not possible. If beamBack returns false (there is nowhere to beam back to), Android's back button will close the app, possibly opening a previously used app that was responsible for opening this app via deep-link. BeamerBackButtonDispatcher can be configured to alwaysBeamBack (meaning it won't attempt pop) or to not fallbackToBeamBack (meaning it won't attempt beamBack).

Accessing nearest Beamer

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

@override
Widget build(BuildContext context) {
  final beamState = Beamer.of(context).currentBeamLocation.state as BeamState;
  final bookId = beamState.pathParameters['bookId'];
  ...
}

Using "Navigator 1.0"

Note that "Navigator 1.0" (i.e. imperative push/pop and friends) can be used alongside Beamer. We already saw that Navigator.pop is used for upward navigation. This tells us that we are using the same Navigator, but just with a different API.

Pages pushed with Navigator.of(context).push (or any similar action) will not be contributing to BeamLocation's state, meaning the browser's URL will not change. One can update just the URL via Beamer.of(context).updateRouteInformation(...). Of course, when using Beamer on mobile, this is a non-issue as there is no URL to be seen.

In general, every navigation scenario should be implementable declaratively (defining page stacks) instead of imperatively (pushing), but the difficulty to do so may vary.

Resources

Here's a list of some useful articles and videos about Beamer:

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 different states, instead of one global state for all page stacks.

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

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

with some "ProfileHandler" that knows which "state" corresponds to which page stack. 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, ItemDetailsPage ],
  • [ ShopPage, ItemsPage, ItemDetailsPage ],
  • [ ShopPage, CartPage ],
  • ...

These "Handlers" are called BeamLocations.

BeamLocations cannot work by themselves. When the RouteInformation comes into the app via deep-link, as initial or as a result of beaming, there must be a decision which BeamLocation will further handle this RouteInformation and build pages for the Navigator. This is the job of BeamerDelegate.locationBuilder that will take the RouteInformation and give it to appropriate BeamLocation based on pathPatterns it supports. BeamLocation will then create and save its own state from it to use for building a page stack.

BeamLocation

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

  • know which URIs it can handle: pathPatterns
  • 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.

This is an example of a BeamLocation:

class BooksLocation extends BeamLocation<BeamState> {
  @override
  List<Pattern> get pathPatterns => ['/books/:bookId'];

  @override
  List<BeamPage> buildPages(BuildContext context, BeamState state) {
    final pages = [
      const BeamPage(
        key: ValueKey('home'),
        child: HomeScreen(),
      ),
      if (state.uri.pathSegments.contains('books'))
        const BeamPage(
          key: ValueKey('books'),
          child: BooksScreen(),
        ),
    ];
    final String? bookIdParameter = state.pathParameters['bookId'];
    if (bookIdParameter != null) {
      final bookId = int.tryParse(bookIdParameter);
      pages.add(
        BeamPage(
          key: ValueKey('book-$bookIdParameter'),
          title: 'Book #$bookIdParameter',
          child: BookDetailsScreen(bookId: bookId),
        ),
      );
    }
    return pages;
  }
}

BeamState

BeamState is a pre-made state that can be used for custom BeamLocations. It keeps various URI attributes such as pathPatternSegments (the segments of chosen path pattern, as each BeamLocation supports many of those), pathParameters and queryParameters.

Custom State

Any class can be used as state for a BeamLocation, e.g. ChangeNotifier. The only requirement is that a state for BeamLocation mixes with RouteInformationSerializable that will enforce the implementation of fromRouteInformation and toRouteInformation.

Full example app can be seen here.

When using a custom MyState that can notify its BeamLocation when it updates, we can also navigate like this

onTap: () {
  final state = context.currentBeamLocation.state as MyState;
  state.selectedBookId = 3;
},

Note that Beamer.of(context).beamToNamed('/books/3') would produce the same result.

Usage

To use Beamer (or any Router), one must construct the *App widget with .router constructor (read more at Router documentation). Along with all the regular *App attributes, we must also provide

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

Here we use Beamer's implementation of those - BeamerParser and BeamerDelegate, to which we pass the desired LocationBuilder. In the simplest form, LocationBuilder is just a function which takes the current RouteInformation and returns a BeamLocation based on the URI or other state properties. The returned BeamLocation will then provide a list of pages for the Navigator.

class MyApp extends StatelessWidget {
  final routerDelegate = BeamerDelegate(
    locationBuilder: (routeInformation, _) {
      if (routeInformation.location!.contains('books')) {
        return BooksLocation(routeInformation);
      }
      return HomeLocation(routeInformation);
    },
  );

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

There are two other options available, if we don't want to define a custom locationBuilder ourselves.

With a List of BeamLocations

BeamerLocationBuilder can be used with a list of BeamLocations. This builder will automatically select the correct BeamLocation based on its pathPatterns or return a NotFound if none matches.

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

With a Map of Routes

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

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

Nested Navigation

When nested navigation (i.e. nested Navigator) is needed, one can just put Beamer anywhere in the Widget tree where this nested navigation will take place. There is no limit on how many Beamers an app can have. Common use case is a bottom navigation, where the BottomNavigationBar should not be affected by navigation transitions, i.e. all the navigation is happening above it, inside another Beamer. In the snippet below, we also use a key on Beamer so that a BottomNavigationBar can manipulate just the navigation within that Beamer. We have a lot of examples for nested navigation:

There are some special attributes of BeamerDelegate that are used only when there are multiple Beamers in the tree. Their function is quite complicated so we try to have sensible defaults (all true) that work well for most use cases;

  • initializeFromParent: This is used to initialize the BeamLocation of child BeamerDelegate when its Beamer gets built in the Widget tree. It is true by default, meaning that the BeamerDelegate will create its BeamLocation from the configuration of the parent BeamerDelegate. If false, the BeamerDelegate will use its initialPath for initialization, regardless of what is the current configuration on parent.
  • updateFromParent: Similar to initializeFromParent, but this, if true (default), sets up a listener that calls BeamerDelegate.update with parent's configuration whenever the parent updates, regardless of whether this caused a rebuild of its (child's) Beamer.
  • updateParent: This is used for updating the other way around, for the child BeamerDelegate to update its parent BeamerDelegate. Most notably, it keeps the beamingHistory of both in sync. This is important for BeamerBackButtonDispatcher which is connected to just the root BeamerDelegate and may want to execute beamBack(). Also, the Widget tree will get built as it were when we do a global rebuild.
class HomeScreen extends StatelessWidget {
  HomeScreen({Key? key}) : super(key: key);

  final _beamerKey = GlobalKey<BeamerState>();
  final _routerDelegate = BeamerDelegate(
    locationBuilder: BeamerLocationBuilder(
      beamLocations: [
        BooksLocation(),
        ArticlesLocation(),
      ],
    ),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Beamer(
        key: _beamerKey,
        routerDelegate: _routerDelegate,
      ),
      bottomNavigationBar: BottomNavigationBarWidget(
        beamerKey: _beamerKey,
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  final routerDelegate = BeamerDelegate(
    initialPath: '/books',
    locationBuilder: RoutesLocationBuilder(
      routes: {
        '*': (context, state, data) => HomeScreen(),
      },
    ),
  );

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

Guards

To guard specific routes, e.g. from un-authenticated users, global BeamGuards can be set up via BeamerDelegate.guards property. 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(
  // on which path patterns (from incoming routes) to perform the check
  pathPatterns: ['/login'],
  // perform the check on all patterns that **don't** have a match in pathPatterns
  guardNonMatching: true,
  // return false to redirect
  check: (context, location) => context.isUserAuthenticated(),
  // where to redirect on a false check
  beamToNamed: (origin, target) => '/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. Sometimes we wish to guard just a few routes that can be specified explicitly. 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: (origin, target) => '/login',
)

General Notes

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

    • buildPages returns a stack of pages that will be built by Navigator when you beam there, and pathPatterns is there for Beamer to decide which BeamLocation handles which URI.
    • BeamLocation keeps query and path parameters from URI in its BeamState. The : is necessary in pathPatterns 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 needs to be a unique value (e.g. ValueKey) for "page state". (see Page Keys)
    • BeamPage creates MaterialPageRoute by default, but other transitions can be chosen by setting BeamPage.type to one of available BeamPageType.

Page Keys

When we beam somewhere, we are putting a new list of pages into Navigator.pages. Now the Navigator has to decide on the transition between the old list of pages and the new list of pages.

In order to know which pages changed and which pages stayed the same, Navigator looks at the pages' keys. If the keys of 2 pages that are compared are equal (important here: null == null), Navigator treats them as the same page and does not rebuild nor replace that page.

One should always set a BeamPage.key (most likely a ValueKey).
If keys are not set, after beaming somewhere via e.g. Beamer.of(context).beamToNamed('/somewhere'), no change will happen in the UI. The new BeamPage doesn't build since Navigator thinks it is the same as the already displayed one.

Tips and Common Issues

  • Removing the # from URL can be done by calling Beamer.setPathUrlStrategy() before runApp().
  • Using beamToReplacement* will replace the current entry in beamingHistory and browser history.
  • 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 (with gifs) here.

  • Location Builders: a recreation of the example app from this article where you can learn a lot about Navigator 2.0. This example showcases all 3 options of using locationBuilder.

  • Advanced Books: for a step further, we add more flows to demonstrate the power of Beamer.

  • 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. Note that beamBackOnPop parameter of beamToNamed might be useful here to override AppBar's pop with beamBack.

  • Provider: you can override BeamLocation.builder to provide some data to the entire location, i.e. to all the pages.

  • Guards: you can define global guards (for example, authentication guard) or BeamLocation.guards that keep a specific stack safe.

  • Authentication Bloc: an example on how to use BeamGuards for an authentication flow with flutter_bloc for state management.

  • Bottom Navigation: an examples of putting Beamer into the Widget tree when using a bottom navigation bar.

  • Bottom Navigation 2: the same as above, but with routes for details pushed on top of everything.

  • Bottom Navigation With Multiple Beamers: having Beamer in each tab.

  • Nested Navigation: nested navigation drawer

  • Multiple Beamers: multiple sibling Beamers working independently side by side.

NOTE: In all nested Beamers, full paths must be specified when defining BeamLocations and beaming.

Migrating

From 0.14 to 1.X

An article explaning changes and providing a migration guide is available here at Medium. Most notable breaking changes:

  • If using a SimpleLocationBuilder:

Instead of

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

now we have

locationBuilder: RoutesLocationBuilder(
  routes: {
    '/': (context, state, data) => MyWidget(),
    '/another': (context, state, data) => AnotherThatNeedsState(state)
  }
)
  • If using a custom BeamLocation:

Instead of

class BooksLocation extends BeamLocation {
  @override
  List<Pattern> get pathBlueprints => ['/books/:bookId'];

  ...
}

now we have

class BooksLocation extends BeamLocation<BeamState> {
  @override
  List<Pattern> get pathPatterns => ['/books/:bookId'];

  ...
}

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

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.

See you at our list of awesome contributors!

  1. devj3ns
  2. ggirotto
  3. matuella
  4. youssefali424
  5. schultek
  6. hatem-u
  7. jmysliv
  8. jeduden
  9. omacranger
  10. samdogg7
  11. Goddchen
  12. spicybackend
  13. cedvdb
  14. Shiba-Kar
  15. gabriel-mocioaca
  16. AdamBuchweitz
  17. nikitadol
  18. ened
  19. luketg8
  20. Zambrella
  21. piyushchauhan
  22. marcguilera
  23. mat100payette
  24. Lorenzohidalgo
  25. timshadel
  26. definev
  27. britannio
  28. satyajitghana
  29. jpangburn

Libraries

beamer