Declarative Routing

One of the most powerful things we can do with route wrappers is to declaratively render our routes. For example, we could link a set of routes to state so that pages are automatically pushed and popped in response to the state - elminating the need to call router.push or router.pop altogether! This kind of routing is especially useful for flows (such as a Profile Information flow, or Sign Up flow), and hence may also be called Flow Routing.

In this section, we'll build upon our app with a simple Login Flow that uses declarative routing. In this flow, we have 2 pages: an email page and a password page. First, begin by adding our login routes to our app router.
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    // our new login routes are defined here!
      path: "/login",
      page: LoginWrapperPage, // we'll get to this LoginWrapperPage next
      children: [
        AutoRoute(page: EmailPage),
        AutoRoute(page: PasswordPage),
    ... // our other routes
class $AppRouter {}

And here is our LoginWrapperPage
class LoginWrapperPage extends StatefulWidget {
    final Function(bool isLoggedIn) onLogin;                        

    const LoginWrapperPage({Key key, this.onLogin}) : super(key: key);

class _LoginWrapperPageState extends State<LoginWrapperPagePage> { 
    String email = "";

    Widget build(context) => AutoRouter.declarative( // use AutoRouter.declarative  
      routes: (_, __) { 
        // Declaratively define your routes here
        return [
          EmailRoute(onNext: (result) {
            setState(() {
                email: result;
          if (email.isNotEmpty) PasswordRoute(onNext: (result) async {
            try {
              // validate the email and password
              await validateEmailAndPassword(email, result)
            } catch (e) {
              // do something with the error
There are several interesting things to point out here! First of all, we are returning 2 routes: our EmailRoute and PasswordRoute . And our PasswordRoute only gets pushed when our email variable is not empty.

Next, there is an onNext callback in the EmailRoute which is fired when the user enters their email and taps "next". This causes the email variable to be assigned to the input email that was just entered by the user.

When that happens, the email variable is no longer empty, which causes the PasswordRoute to get pushed. Note how we didn't need to use router.push at all!

Finally, when the user enters their password and taps "next", this fires the onNext callback of the PasswordRoute . Here, we validate the email and password input. If successful, we trigger the onLogin callback defined in the LoginWrapperPage . This is important, because the onLogin callback allows us to use the LoginWrapperPage result in other areas of our code!

Now, this is just a simple example of declarative routing, but you can get creative and use it however you want. Link your routes to a state management solution, or use a step count instead of checking if strings are empty - the possibilities are endless!


To use our new login flow, just simply push the LoginWrapperPage wherever you need it. This will open up the login flow, and once the flow is completed it will trigger onLogin which we can then use to log the user in

context.router.root.push(LoginWrapperPage(onLogin: (result) {
  // do something with the login result here, such as logging the user in
Note that we are using context.router.root . This is because our login flow was defined at the top level of the router, and not nested. It is generally good practice to put declarative flows at the root level.