Navigate and prosper 🖖
Voyager is a Widget router for Flutter with a dynamic navigation map and Provider based DI elements.
Features
- Support for Navigator 1.0/2.0 APIs
- YAML/JSON based Navigation Map
- support for query parameters (
?param=value
) - path subsections (
/user/:id
) - parameters interpolation in subsections (
Selected %{id}
) - serializable (can be delivered using e.g. Firebase remote config)
- fast code generator for paths & plugins
- schema validation (draft v7)
- support for query parameters (
- Highly customizable plugin architecture.
VoyagerWidget
to embed yourpath
mapping in any place
Getting started
Check the example app on github or see it in the browser.
- Add Voyager Dependency
- Define Navigation Map
- Instantiate Router
- Choose Your API
- Predefined Plugins
- Writing Custom Plugins
- Dependency Injection
- Code Generation
Add Voyager Dependency
To use this plugin, add voyager
as a dependency in your pubspec.yaml file.
You should ensure that you add the router as a dependency in your flutter project.
dependencies:
voyager: ^latest_release
Then in the code, make sure you import voyager package:
import 'package:voyager/voyager.dart';
Define Navigation Map
In an example below there are two paths defined - /home
and /other/:title
. These paths route to HomeWidget
and OtherWidget
respectively. A map can carry extra information along the way - e.g. title
will later be accessible in the routed widget.
---
'/home' :
widget: HomeWidget
title: "This is Home"
'/other/:thing' :
widget: OtherWidget
title: "This is %{thing}"
The second path has :thing
section parameter. This can be used to interpolate value of title dynamically at runtime, using %{key}
notation.
Load paths using method of your choosing:
final List<VoyagerPath> paths = loadPathsFromYamlSync(yaml_string);
Define Plugins
You need to tell router what kind of plugins you plan to use. Those depend on what you have written in the navigation file. In our example we need to provide mappings for widget
and title
.
final plugins = [
WidgetPluginBuilder() /// provide widget builders for expressions used in YAML
.add("HomeWidget", (context) => HomeWidget())
.add("OtherWidget", (context) => OtherWidget())
.build(),
TitlePlugin() /// custom plugin
];
Instantiate Router
Pass paths and plugins as constructor parameters to obtain VoyagerRouter
.
final router = VoyagerRouter.from(paths, plugins);
Choose Your API
Voyager offers different kind of APIs. Here's a quick overview what these are good for.
Voyager Widget
This is the most atomic use case. Simply embed your widget anywhere you want:
VoyagerWidget(path: "/home", router: router);
If Provider<VoyagerRouter>
is available in the widget tree, you can omit the router parameter.
Navigator 1.0
Simple, imperative navigation model. Following snippet illustrates intergration with MaterialApp and opening initialPath
:
final initalPath = "/my/fancy/super/path"
Provider<VoyagerRouter>.value(
value: router,
child: MaterialApp(
home: VoyagerWidget(path: initalPath),
onGenerateRoute: router.generator()
)
)
Navigation stack is handled via system, e.g. following will push a new page:
Navigator.of(context).pushNamed("/path/to/go");
Sidenote: You can try using MaterialApp.initalRoute
but please read this first if you find MaterialApp.initalRoute
is not working for you... that's because it's probably working as intended ¯\_(ツ)_/¯
Navigator 2.0
Navigator 2.0 gives you a full control over the navigation stack. This comes with a price of fairly complex API. Voyager builds on top of this API and introduces VoyagerStack
to hopefully make declarative navigation easier.
VoyagerStackApp(
router: router,
stack: VoyagerStack[
VoyagerPage("/my/fancy/super/path"),
],
createApp: (context, parser, delegate) => MaterialApp.router(
routeInformationParser: parser,
routerDelegate: delegate,
theme: themeData(),
),
);
With Navigator 2.0, you own the navigation stack.
If you wish to navigate to the other path, you'll need to update the stack the declarative way, e.g.:
// ...
stack: VoyagerStack[
VoyagerPage("/my/fancy/super/path"),
VoyagerPage("/path/to/go"),
],
// ...
Under the hood, VoyagerStack
uses .asPages(VoyagerRouter)
method that resolves the given stack to a List<Page<dynamic>>
instance which then is used with Navigator
. It's possible to use Navigator
and VoyagerStack
directly without VoyagerStackApp
wrapper. Additionally VoyagerInformationParser
and VoyagerDelegate
are easily instantiable in case you need to use them directly.
Predefined Plugins
Voyager comes with a few predefined plugins that are ready to setup and use:
Widget Plugin
This is a mandatory plugin that enables widget resolution. It converts a string value from navigation map e.g. "HomeWidget" into a usable Flutter widget:
WidgetPlugin({
"HomeWidget": (context) => HomeWidget()
});
You can enable code generation to avoid writing these mappings manually. Check the example app.
Page Plugin (a.k.a. transitions)
This plugin works only when using Voyager in Navigation 2.0 API. It enables specifying custom Page<dynamic>
appearance to be used for the given path, e.g.:
PagePlugin({
"slideFromTop": slideFromTop
});
Check slide_from_top_page.dart for a custom page implementation details
Code generation to avoid writing these mappings manually. Check the example app.
Redirect Plugin
Allows registering aliases for already defined paths:
---
'/home' :
widget: HomeWidget
title: "This is Home"
'/start' :
redirect: '/home'
Writing Custom Plugins
You can define as many router plugins as you want. Here's how you could handle the title
node from the example navigation yaml.
class TitlePlugin extends RouterPlugin {
TitlePlugin() : super("title"); // YAML node to intercept
@override
void outputFor(RouterContext context, dynamic config, Voyager voyager) {
// config can be anything that is passed from YAML
voyager["title"] = config.toString(); // output of this plugin
}
}
Sidenote: Above plugin is redundant. Voyager repackages the primitive types from configuration by default. Use plugins to resolve primitive types to custom types , e.g. take a look at IconPlugin from the example app.
Dependency Injection
If you use VoyagerWidget
to e.g. resolve to OtherWidget
, you can obtain Voyager
anywhere from BuildContext
using extension getter:
final voyager = context.voyager;
Voyager
is a key/value map, a composite output of the plugins setup for the given path.
Now going back to our mystery OtherWidget
class from the example navigation map, that widget's implementation could look something like this:
class OtherWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final voyager = context.voyager; // injecting voyager from build context
final title = voyager["title"];
return Scaffold(
appBar: AppBar(
title: Text(title), // et voilÃ
),
body: Center(
child: Text("Other Page"),
)
);
}
}
Code generation
Using code generation enables you to use some of the following features:
- Strong typed paths
"/other/:thing"
becomespathOther("thing")
- WidgetPlugin generation
- skip manual mapping, simply add
generatedVoyagerWidgetPlugin()
to list of plugins
- skip manual mapping, simply add
- PathPlugin generation
- skip manual mapping, simply add
generatedVoyagerPathPlugin()
to list of plugins
- skip manual mapping, simply add
- Schema validation & strong typed Voyager fields
Important: Code generation relies heavily on the type
value. It should be unique per path definition, also the values should_be_snake_case
. Since 3.0 type
is automatically generated based on the path - it still can be overridden manually.
Voyager supports generating dart code based on the navigation map yaml (or json) file. Simply run the following command and wait for the script to set it up for you.
flutter packages pub run voyager:codegen --run-once
This should create a voyager-codegen.yaml
file in the root of your project:
- name: Voyager # base name for generated classes
source: assets/navigation.yaml
target: lib/navigation.voyager.dart
widgetPlugin: true
pagePlugin: true
Then you need to create lib/navigation.dart
that will look something like this:
import 'package:voyager/voyager.dart';
part 'navigation.voyager.dart';
Now if you run again:
flutter packages pub run voyager:codegen --run-once
This should regenerate contents of lib/navigation.voyager.dart
. If compiler complains about unresolved symbols, make sure you add necessary imports to lib/navigation.dart
Dart Code Formatting: If you want to have Flutter's default code formatting for the generated code, make sure you have dart-sdk in you PATH. Dart SDK is included with the flutter sdk, so you can e.g.:
export PATH="$PATH:/path/to/flutter/bin/cache/dart-sdk/bin"
File Watching: If you omit --run-once
flag, the code generator will keep watching files and generating code in a loop.
You might want to add .jarCache/
to your .gitignore
to avoid checking in binary jars to your repo.
Schema Validation
Add your validation in voyager-codegen.yaml
, for instance to cover IconPlugin
you could do this:
- name: Voyager
source: lib/main.dart
target: lib/navigation.voyager.dart
schema:
icon:
pluginStub: true # add if you want to generate a plugin stub
output: Icon # associated Dart class produced by the plugin
input: # write schema for your the icon node (JSON Schema draft-07 layout)
type: string
pattern: "^[a-fA-F0-9]{4}$"
Now whenever you run voyager:codegen
you'll get an extra message stating all is fine:
✅ Schema validated properly
...or an error specific to your router navigation map, e.g.:
🚨 /fab@icon: #/icon: string [e88fd] does not match pattern ^[a-fA-F0-9]{4}$
Furthermore you gain strong typed reference to the plugin output in extended Voyager
instance:
assert(voyager.icon is Icon);
Finally, pluginStub: true
gets you an abstract plugin class, so that you can avoid typing voyager["node_name"]
manually. Just focus on parsing the node's config input and converting it into an expected output:
class IconPlugin extends IconPluginStub {
@override
Icon buildObject(RouterContext context, dynamic config) {
/// write your code here
}
}