pushNamed: type ‘MaterialPageRoute’ is not a subtype of type ‘Route’

We can use the Navigator to navigate from one widget (screen) to another. The caller might want to wait for the result that is returned from that navigation. If we want to stick to the static type system, we might run into some trouble here when named routes are being used.

The problem

At the time of writing (2015-11-18), when using named routes via Navigator.pushNamed() in combination with type safety on the caller side, the compiler throws an error.

Flutter pushNamed() route transition animation example
The intended screen transition

Let’s examine the possibilities we have when we want to navigate from one screen to another and observe the issues we are facing.

Using push()

Starting with push(), we consider this example to illustrate the problem:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        primarySwatch: Colors.lightBlue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: TestScreen(),
    );
  }
}

class TestScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First'),
      ),
      body: Center(
        child: TextButton(
          child: Text('NEXT'),
          onPressed: () async {
            final bool? result = await Navigator.push<bool>(
              context,
              MaterialPageRoute(
                builder: (BuildContext context) {
                  return TestScreen2();
                },
              ),
            );

            print(result);
          },
        ),
      ),
    );
  }
}

class TestScreen2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: Center(
        child: TextButton(
          child: Text('PREVIOUS'),
          onPressed: () {
            Navigator.pop(context, true);
          },
        ),
      ),
    );
  }
}

Basically, we have two screens here: one screen showing a centered button with a label “NEXT”. When the user taps the button, we navigate to the second screen directly using push() and MaterialPageRoute inside the builder function. Important here: we define the return type by specifying <bool>. The result is stored in a variable of type bool? because the signature requires an optional value. That’s because the user could also just navigate back using the app bar.

Using pushNamed()

Now let’s have a look at the equivalent approach using named routes:

import 'package:flutter/material.dart';

class MyAppNamed extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        primarySwatch: Colors.lightBlue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => TestScreen(),
        '/second': (context) => TestScreen2(),
      },
    );
  }
}

class TestScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First'),
      ),
      body: Center(
        child: TextButton(
          child: Text('NEXT'),
          onPressed: () async {
            final bool? result = await Navigator.pushNamed<bool>(
              context,
              '/second',
            );
            
            print(result);
          },
        ),
      ),
    );
  }
}

class TestScreen2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: Center(
        child: TextButton(
          child: Text('PREVIOUS'),
          onPressed: () {
            Navigator.pop(context, true);
          },
        ),
      ),
    );
  }
}

Instead of providing a value for the home property of the MaterialApp widget, we set the routes: / is the route for the first widget (TestScreen), while /second is the route for the second widget (TestScreen2).

We also need to omit the home property and instead set the initialRoute as the MaterialApp widget forbids both of the properties to bet set at the same time.

When the screen transition is about to happen (onPressed), we use pushNamed() instead of push() in order to navigate to the next screen.

The error

Now the counter-intuitive part: this produces the following error:

E/flutter ( 7570): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: type 'MaterialPageRoute<dynamic>' is not a subtype of type 'Route<bool>?' in type cast
E/flutter ( 7570): #0      NavigatorState._routeNamed (package:flutter/src/widgets/navigator.dart:4186:57)
E/flutter ( 7570): #1      NavigatorState.pushNamed (package:flutter/src/widgets/navigator.dart:4243:20)
E/flutter ( 7570): #2      Navigator.pushNamed (package:flutter/src/widgets/navigator.dart:1742:34)
...

It’s irritating that using generics to specify a return type for push() works, but produces an error when using pushNamed(). This is an error that’s already described in a Flutter GitHub issue.

One solution

There is a way around this: instead of providing the return type via generics, we can cast the return type to our desired bool. So instead of this:

final bool? result = await Navigator.pushNamed<bool>(
  context,
  '/second',
);

We can simply write this:

final bool? result = await Navigator.pushNamed(
  context,
  '/second',
) as bool?;

But casting does not seem so safe as it can lead to runtime errors if we cast to the wrong type. And the question remains: why is it even a problem to do it the like we did in the first approach?

The cause

When we use the routes property in the MaterialApp widget, there is something we implicitly decide along with that: that every route inside is a MaterialPageRoute<dynamic>. The problem here is that this can not be converted to typed routes like Route<bool>.

Using pushNamed() is actually the same as using pushNamed<dynamic>. The pushNamed method of Navigator looks like this:

  @optionalTypeArgs
  Future<T?> pushNamed<T extends Object?>(
    String routeName, {
    Object? arguments,
  }) {
    return push<T>(_routeNamed<T>(routeName, arguments: arguments)!);
  }

_routeNamed<T> is the key here as it leads to further calls that assume the same type argument for the route that is returned.

In the end, Dart is trying to cast MaterialPageRoute<dynamic> (defined by routes property) to Route<bool?> which is is problematic because at runtime, we can not be sure that dynamic is something bool is a subtype of.

A better solution

Flutter provides an alternative to using the routes parameter: the onGenerateRoute property:

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        primarySwatch: Colors.lightBlue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => TestScreen(),
      },
      onGenerateRoute: (RouteSettings settings) {
        final String routeName = settings.name ?? '';

        switch (routeName) {
          case '/second':
            return MaterialPageRoute<bool>(
              builder: (BuildContext context) => TestScreen2(),
              settings: settings,
            );
        }
      },
    );
  }

Instead of defining the second route in the routes parameter, we use onGenerateRoute – a callback function with RouteSettings as its only argument.

Now we can finally use the pushNamed() the way it’s intended:

onPressed: () async {
  final bool? result = await Navigator.pushNamed<bool>(
    context,
    '/second',
  );

  print(result);
},

Conclusion

It’s generally absolutely recommended to use named routes as a navigation concepts in every app that has more than a little complexity. If the routes property of MaterialApp, one has to be aware that static typing in combination with pushNamed either has to be abandoned or forced by type cast. A better solution is to make us of onGenerateRoute instead. It enables the developer to provide a type argument to pushNamed and expect the return type to be of that exact type.

If you like what you’ve read, feel free to support me:

🥗Buy me a salad

Leave a Comment