1

Flutter Sailor Navigation Tutorial – The Simplest Navigator Library?

Flutter is known for its extensible built-in navigation. Even by by using the default Navigator, you have a couple of options:

  1. Instantiating the so called "page widgets" directly.
  2. Use simple mapped named routes.
  3. Set up more elaborate named routes with the ability to pass in custom arguments.

You can learn about all of these vanilla navigation methods from this tutorial.

Sometimes though, it's good to bring in more structure into navigation. Instead of inventing ways of how to safely pass values between routes, you can use parameters and arguments in Sailor. Instead of reinventing the wheel with custom route transitions and logging for debugging purposes, you can let Sailor to do the heavy lifting for you.

The app we will build

We're going to build a very simple app consisting of three pages. One of them is an "initial page" from which we can navigate to the rest. Values which are displayed in the individual pages are passed in from the initial page.

The starter project is an already fully working app with navigation. The only issue is that the starter navigation is done by using the default MaterialPageRoutes all over the place. We're going to take this navigation mess and turn it into a work of art with Sailor.

In addition to the above, the starter project has sailor dependency already added inside pubspec.yaml. To follow along precisely with this tutorial, make sure that you also import version 0.5.0.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  sailor: ^0.5.0

SecondPage takes its parameters in individually:

second_page.dart

class SecondPage extends StatelessWidget {
  final String productName;
  final double price;

  SecondPage({
    @required this.productName,
    @required this.price,
  });
...

While the ThirdPage takes in the same values wrapped in a ThirdPageArgs object.

third_page.dart

class ThirdPageArgs {
  final String productName;
  final double price;

  ThirdPageArgs({
    @required this.productName,
    @required this.price,
  });
}

class ThirdPage extends StatelessWidget {
  final ThirdPageArgs args;

  ThirdPage(this.args);
...

Adding routes to Sailor

The first step in letting Sailor help us with navigation is to define routes. It's always a good idea to follow the first SOLID principle of single responsibility, so we will center Sailor-related code into a Routes class. We will first create a static instance of Sailor and simply call addRoutes, passing in our page widgets wrapped in a SailorRoute instance.

main.dart

class Routes {
  static final sailor = Sailor();

  static void createRoutes() {
    sailor.addRoutes([
      // Just for good measure, we won't explicitly navigate to the InitialPage.
      SailorRoute(
        name: '/initial',
        builder: (context, args, params) {
          return InitialPage();
        },
      ),
      SailorRoute(
        name: '/second',
        builder: (context, args, params) {
          return SecondPage();
        },
      ),
      SailorRoute(
        name: '/third',
        builder: (context, args, params) {
          return ThirdPage();
        },
      ),
    ]);
  }
}
I recommend that you don't pass around "magical strings" but instead create constants for route names. This is left out from this tutorial for brevity.

Apart from the code having a bunch of errors because we're not yet passing any arguments into SecondPage and ThirdPage, there is a more pressing issue to deal with. We have to call createRoutes and also plug Sailor into the navigation flow.

Initializing Sailor

Let's first call createRoutes from the main method. This will allow Sailor to resolve route names to their respective widgets.

main.dart

void main() {
  Routes.createRoutes();
  runApp(MyApp());
}

Additionally, to plug Sailor into Flutter's navigation system, let's set the onGenerateRoute function and also the navigatorKey inside the Material/Cupertino/WidgetsApp widget.

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      home: InitialPage(),
      onGenerateRoute: Routes.sailor.generator(),
      navigatorKey: Routes.sailor.navigatorKey,
    );
  }
}

Passing in values

Sailor gives us two options in how to values into routes. Every builder function of a SailorRoute receives both args and params. So... what's the difference between them?

Parameters

These are represented by a Map which can contain a bunch of values, which is precisely what we need for the SecondPage.

Parameters for a route have to be first defined. Later on, we will implement the buttons inside the InitialPage to pass these parameters in. Once passed in, they will arrive in the builder function where we can extract them and put them into a new instance of SecondPage.

This approach is less maintainable than passing in arguments, which we're going to discuss next.

main.dart

SailorRoute(
  name: '/second',
  builder: (context, args, params) {
    return SecondPage(
      productName: params.param('productName'),
      // param is a generic method
      price: params.param<double>('price'),
    );
  },
  params: [
    SailorParam(name: 'productName', isRequired: true),
    SailorParam(name: 'price', defaultValue: 100.0),
  ],
),

Arguments

Whenever you can group multiple fields into a class, you should. More so when you're dealing with navigation where it's so easy to get lost. ThirdPage is an example of passing in a class. Sailor handles this with the args parameter of the builder function.

First, let's modify the ThirdPageArgs class to extend BaseArguments.

third_page.dart

class ThirdPageArgs extends BaseArguments {
  final String productName;
  final double price;

  ThirdPageArgs({
    @required this.productName,
    @required this.price,
  });
}

Now we can pass ThirdPageArgs in when navigating and use them from within the builder:

main.dart

SailorRoute(
  name: '/third',
  builder: (context, args, params) {
    return ThirdPage(args);
  },
),
You can also access args and params from widgets directly by calling static methods on the Sailor class. Learn more in the official docs.

Navigating

Now we're finally going to fit all of the pieces together and switch from the default navigation to Sailor in the onPressed button handlers in the initial page. Just for comparison, here's the old code which instantiates MaterialPageRoutes directly:

initial_page.dart

void navigateToSecond(BuildContext context) {
  Navigator.of(context).push(MaterialPageRoute(
    builder: (context) =&gt;
        SecondPage(price: 2000, productName: &#39;Laptop 2nd Gen&#39;),
  ));
}

void navigateToThird(BuildContext context) {
  Navigator.of(context).push(MaterialPageRoute(
    builder: (context) =&gt; ThirdPage(
      ThirdPageArgs(
        price: 3000,
        productName: &#39;Laptop 3rd Gen&#39;,
      ),
    ),
  ));
}

And the modified code using Sailor's named routes now looks like this:

initial_page.dart

// NOTICE: We no longer need the BuildContext instance
void navigateToSecond() {
  Routes.sailor.navigate(
    '/second',
    params: {
      // Make sure this is a double. We lose type safety by using params.
      'price': 2000.0,
      'productName': 'Laptop 2nd Gen',
    },
  );
}

void navigateToThird() {
  // Sailor is a callable class (can leave out 'navigate')
  Routes.sailor(
    '/third',
    args: ThirdPageArgs(
      // Decimal place is of no concern here.
      // 3000 (int) is automatically cast into a double.
      price: 3000,
      productName: 'Laptop 3rd Gen',
    ),
  );
}

Especially with the approach of passing in an arguments class, we didn't lose out on the type-safety, while we gained a ton with regards to readability and maintainability of the code.

Route transition animations

In addition to simplifying the flow of data between routes, Sailor also makes defining route transitions hassle free. It comes with a couple of pre-defined ones which we're going to use now. You can also create your own custom transitions - official docs cover this in detail.

Transitions can be defined either when defining a SailorRoute, when navigating to a route, or there can even be global transitions. The process of adding any of these transitions is practically the same. We're going to take a look at creating default transitions when we're adding new SailorRoutes.

main.dart

class Routes {
  static final sailor = Sailor();

  static void createRoutes() {
    sailor.addRoutes([
      ...
      SailorRoute(
        name: '/third',
        builder: (context, args, params) {
          return ThirdPage(args);
        },
        defaultTransitions: [
          SailorTransition.slide_from_bottom,
          SailorTransition.slide_from_left,
          SailorTransition.zoom_in,
        ],
        defaultTransitionCurve: Curves.easeInCirc,
        defaultTransitionDuration: Duration(seconds: 2),
      ),
    ]);
  }
}

Although I wouldn't necessarily call it a good user experience to have to wait through a two second over-the-top route transition, it shows what's possible in just a few lines of code.

What you learned

Sailor is the next step when you're fed up with manual handling of navigation through named routes. It supports a nice mechanism of passing data around and in addition, Sailor also simplifies applying route animation transitions.

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

  • Arvind says:

    What if Second Page Wants to call fourth page?

  • >