beamer 1.0.0-pre.15.0 beamer: ^1.0.0-pre.15.0 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".
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 RoutesLocationBuilder
which yields the least amount of code for a functioning application:
class MyApp extends StatelessWidget {
final routerDelegate = BeamerDelegate(
locationBuilder: RoutesLocationBuilder(
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,
);
}
}
RoutesLocationBuilder
will create a single BeamLocation
called RoutesBeamLocation
which will pick and sort routes
based on their paths, putt them into Navigator
and rebuild the page stack.
Beaming #
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. BeamLocation
s 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.
Beamer.of(context).beamToNamed('/books/2');
// or with an extension method on `BuildContext
context.beamToNamed('/books/2');
// or with some additional data
context.beamToNamed(
'/book/2',
data: {'note': 'this is my favorite book'},
);
Beaming Back #
Navigating to previous page in a page stack is done via Navigator.of(context).pop()
. This is 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 navigation history is kept in beamingHistory
. Therefore, there is an ability to beam back to a previous entry in beamingHistory
. 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();
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)
Accessing nearest Beamer #
Accessing route attributes in Widget
s (for example, bookId
for building BookDetailsScreen
) can be done with
final beamState = Beamer.of(context).currentBeamLocation.state as BeamState;
final bookId = beamState.pathParameters['bookId'];
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 BeamLocation
s.
BeamLocation
s cannot work by themselves. When the RouteInformation
comes into the app through deep-link, or as initial, 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
which will create and save its own 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:
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 BeamLocation
s 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<BeamState> {
BooksLocation(BeamState state) : super(state);
@override
List<Pattern> get pathPatterns => ['/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 pre-made state
that one can use for custom BeamLocation
s. Its role is to keep various URI attributes such as pathPatternSegments
(the segments of chosen path pattern, 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(
pathPatternSegments: ['books', ':bookId'],
pathParameters: {'bookId': '3'},
),
),
Customizing the state (advanced) #
Any class can be a state for BeamLocation
s, for example even a ChangeNotifier
. The only requirement is that a state for BeamLocation
mixes with RouteInformationSerializable
that will enforce the implementation of fromRouteInformation
and toRouteInformation
.
Custom state:
class BooksState extends ChangeNotifier with RouteInformationSerializable {
BooksState([
bool isBooksListOn = false,
int? selectedBookId,
]) : _isBooksListOn = isBooksListOn,
_selectedBookId = selectedBookId;
bool _isBooksListOn;
bool get isBooksListOn => _isBooksListOn;
set isBooksListOn(bool isOn) {
_isBooksListOn = isOn;
notifyListeners();
}
int? _selectedBookId;
int? get selectedBookId => _selectedBookId;
set selectedBookId(int? id) {
_selectedBookId = id;
notifyListeners();
}
@override
BooksState fromRouteInformation(RouteInformation routeInformation) {
final uri = Uri.parse(routeInformation.location ?? '/');
if (uri.pathSegments.isNotEmpty) {
_isBooksListOn = true;
if (uri.pathSegments.length > 1) {
_selectedBookId = int.parse(uri.pathSegments[1]);
}
}
return this;
}
@override
RouteInformation toRouteInformation() {
String uriString = '';
if (_isBooksListOn) {
uriString += '/books';
}
if (_selectedBookId != null) {
uriString += '/$_selectedBookId';
}
return RouteInformation(location: uriString.isEmpty ? '/' : uriString);
}
}
Custom BeamLocation
using the above state:
class BooksLocation extends BeamLocation<BooksState> {
BooksLocation(RouteInformation routeInformation) : super(routeInformation);
@override
createState(RouteInformation routeInformation) =>
BooksState().fromRouteInformation(routeInformation);
@override
List<Pattern> get pathPatterns => ['/books/:bookId'];
@override
List<BeamPage> buildPages(BuildContext context, BooksState state) => [
const BeamPage(
key: ValueKey('home'),
child: HomeScreen(),
),
if (state.isBooksListOn)
BeamPage(
key: ValueKey('books'),
child: BooksScreen(),
onPopPage: (context, delegate, state, page) {
(state as BooksState).isBooksListOn = false;
return true;
},
),
if (state.selectedBookId != null)
BeamPage(
key: ValueKey('book-${state.selectedBookId}'),
child: BookDetailsScreen(
bookDetails: books.firstWhere(
(book) => int.parse(book['id']!) == state.selectedBookId,
),
),
onPopPage: (context, delegate, state, page) {
(state as BooksState).selectedBookId = null;
return true;
},
),
];
}
Somewhere in the app:
onTap: () {
final state = context.currentBeamLocation.state as BooksState;
state.selectedBookId = int.parse(book['id']!);
},
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 ofNavigator
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: (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 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 BeamLocation
s. This builder will automatically select the correct location, based on the pathPatterns
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 RoutesLocationBuilder
with a map of routes, as mentioned in Quick Start. This completely removes the need for custom BeamLocation
s, 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: RoutesLocationBuilder(
routes: {
'/': (context, state) => HomeScreen(),
'/books': (context, state) => BooksScreen(),
'/books/:bookId': (context, state) =>
BookDetailsScreen(
bookId: state.pathParameters['bookId']
),
},
),
);
Guards #
To guard specific routes, e.g. from un-authenticated users, global BeamGuard
s 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',
)
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 Beamer
s 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: RoutesLocationBuilder(
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:pathPatterns
andbuildPages
.buildPages
returns a stack of pages that will be built byNavigator
when you beam there, andpathPatterns
is there for Beamer to decide whichBeamLocation
corresponds to which URI.BeamLocation
keeps query and path parameters from URI in itsBeamState
. The:
is necessary inpathPatterns
if you might get path parameter from browser.
-
BeamPage
's child is an arbitraryWidget
that represents your app screen / page.key
is important forNavigator
to optimize rebuilds. This should be a unique value for "page state".BeamPage
createsMaterialPageRoute
by default, but other transitions can be chosen by settingBeamPage.type
to one of availableBeamPageType
.
NOTE that "Navigator 1.0" can be used alongside Beamer. You can easily push
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 callingBeamer.setPathUrlStrategy()
beforerunApp()
. BeamPage.title
is used for setting the browser tab title by default and can be opt-out by settingBeamerDelegate.setBrowserTabTitle
tofalse
.- 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.
Advanced Books #
For a step further, we add more flows to demonstrate the power of Beamer. The full code is available here.
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
.
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.
// 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.
- 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 BeamGuard
s for an authentication flow. It uses flutter_bloc for state management. The code is available here.
Bottom Navigation #
An examples of putting Beamer
into the Widget tree is when using a bottom navigation bar. The code is available here.
Bottom Navigation Multiple Beamers #
The code for the bottom navigation example app with multiple beamers is available here
Nested Navigation #
NOTE: In all nested Beamer
s, full paths must be specified when defining BeamLocation
s and beaming. (support for relative paths is in progress)
The code for the nested navigation example app is available here
Integration with Navigation UI Packages #
- Animated Rail Example, with animated_rail package.
- ... (Contributions are very welcome! Add your suggestion here or make a PR.)
Migrating #
From 0.14 to 0.15 #
(TBA)
See CHANGELOG.
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
toBeamerDelegate
- rename
BeamerRouteInformationParser
toBeamerParser
- rename
pagesBuilder
tobuildPages
- rename
Beamer.of(context).currentLocation
toBeamer.of(context).currentBeamLocation
From 0.11 to 0.12 #
- There's no
RootRouterDelegate
anymore. Just rename it toBeamerDelegate
. If you were using itshomeBuilder
, useSimpleLocationBuilder
and thenroutes: {'/': (context) => HomeScreen()}
. - Behavior of
beamBack
was changed to go to previousBeamState
, notBeamLocation
. If this is not what you want, usepopBeamLocation()
that has the same behavior as oldbeamback
.
From 0.10 to 0.11 #
BeamerDelegate.beamLocations
is nowlocationBuilder
. SeeBeamerLocationBuilder
for easiest migration.Beamer
now takesBeamerDelegate
, notBeamLocations
directlybuildPages
now also bringsstate
From 0.9 to 0.10 #
BeamLocation
constructor now takes onlyBeamState state
. (there's no need to define special constructors and callsuper
if you usebeamToNamed
)- most of the attributes that were in
BeamLocation
are now inBeamLocation.state
. When accessing them throughBeamLocation
:pathParameters
is nowstate.pathParameters
queryParameters
is nowstate.queryParameters
data
is nowstate.data
pathSegments
is nowstate.pathBlueprintSegments
uri
is nowstate.uri
From 0.7 to 0.8 #
- rename
pages
tobuildPages
inBeamLocation
s - pass
beamLocations
toBeamerDelegate
instead ofBeamerParser
. See Usage
From 0.4 to 0.5 #
- instead of wrapping
MaterialApp
withBeamer
, use*App.router()
String BeamLocation.pathBlueprint
is nowList<String> BeamLocation.pathBlueprints
BeamLocation.withParameters
constructor is removed and all parameters are handled with 1 constructor. See example if you needsuper
.BeamPage.page
is now calledBeamPage.child
Help and Chat #
For any problems, questions, suggestions, fun,... join us at 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.