voyager 3.0.0 copy "voyager: ^3.0.0" to clipboard
voyager: ^3.0.0 copied to clipboard

The widget router and basic dependency injection library for Flutter. Define navigation paths in YAML and power them up with custom plugins.

banner

pub package Codemagic build status codecov Kotlinlang slack

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)
  • Highly customizable plugin architecture.
  • VoyagerWidget to embed your path mapping in any place

Getting started #

Check the example app on github or see it in the browser.

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
copied to clipboard

Then in the code, make sure you import voyager package:

import 'package:voyager/voyager.dart';
copied to clipboard

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}"
copied to clipboard

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);
copied to clipboard

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
];
copied to clipboard

Instantiate Router #

Pass paths and plugins as constructor parameters to obtain VoyagerRouter.

final router = VoyagerRouter.from(paths, plugins);
copied to clipboard

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);
copied to clipboard

If Provider<VoyagerRouter> is available in the widget tree, you can omit the router parameter.

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()
  )
)
copied to clipboard

Navigation stack is handled via system, e.g. following will push a new page:

Navigator.of(context).pushNamed("/path/to/go");
copied to clipboard

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 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(),
  ),
);
copied to clipboard

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"),
  ],
  // ...
copied to clipboard

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()
});
copied to clipboard

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
});
copied to clipboard

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'
copied to clipboard

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
  }
}
copied to clipboard

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;
copied to clipboard

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"),
      )
    );
  }
}
copied to clipboard

Code generation #

Using code generation enables you to use some of the following features:

  • Strong typed paths
    • "/other/:thing" becomes pathOther("thing")
  • WidgetPlugin generation
    • skip manual mapping, simply add generatedVoyagerWidgetPlugin() to list of plugins
  • PathPlugin generation
    • skip manual mapping, simply add generatedVoyagerPathPlugin() to list of plugins
  • 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
copied to clipboard

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
copied to clipboard

Then you need to create lib/navigation.dart that will look something like this:

import 'package:voyager/voyager.dart';

part 'navigation.voyager.dart';
copied to clipboard

Now if you run again:

flutter packages pub run voyager:codegen --run-once
copied to clipboard

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"
copied to clipboard

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}$"
copied to clipboard

Now whenever you run voyager:codegen you'll get an extra message stating all is fine:

✅ Schema validated properly
copied to clipboard

...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}$
copied to clipboard

Furthermore you gain strong typed reference to the plugin output in extended Voyager instance:

assert(voyager.icon is Icon);
copied to clipboard

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
  }
}
copied to clipboard
27
likes
130
points
196
downloads

Publisher

verified publishervishna.dev

Weekly Downloads

2024.09.14 - 2025.03.29

The widget router and basic dependency injection library for Flutter. Define navigation paths in YAML and power them up with custom plugins.

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

equatable, flutter, provider, sprintf, yaml

More

Packages that depend on voyager